perf(dictionary): a search finds keys by their first characters, indexed
A search matched LIKE '%|eat%' over every entry's barred keys, which no index can answer, so each one read the whole dictionary: 43-57 ms in JMdict's 218,863 entries. dictionary_search_key holds a row per key and loose key with its first one, two and three characters in columns of their own and one index on them, so the keys a query starts are an equality the index answers whatever the database's collation (Serverpod's models can't declare COLLATE "C" or text_pattern_ops). A search now takes 5-26 ms, at 1.4 million rows and 190 MB for JMdict, and its load takes 72 s instead of 33 s. The loader fills the table beside the entries and loads a dictionary again when it has none. Until then, in the minutes after the deploying server starts, the search reads the barred keys as before; those columns stay until a later release, since the server before a deploy's switch still searches them. The migration only creates the table and its index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ed84b47192
commit
8e3a4e56dc
19 changed files with 17709 additions and 19 deletions
File diff suppressed because one or more lines are too long
|
|
@ -99,11 +99,15 @@ abstract class DictionaryEntry
|
|||
String labels;
|
||||
|
||||
/// What a search matches, lower case, between bars
|
||||
/// (`|食べる|たべる|taberu|eat|`).
|
||||
/// (`|食べる|たべる|taberu|eat|`). A search reads them from
|
||||
/// `DictionarySearchKey` since 2026-10-17; these stay for a dictionary
|
||||
/// loaded before, until it loads again, and for the server a deploy
|
||||
/// replaces, until a later release drops them.
|
||||
String searchKeys;
|
||||
|
||||
/// What a search matches after every searchKeys match: spellings people
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too.
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too;
|
||||
/// kept as searchKeys are.
|
||||
String looseKeys;
|
||||
|
||||
/// The entry as cards take it: its JSON, as the file holds it.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A key a search for words matches (`DictionaryEndpoint.search`), one row
|
||||
/// each: every key and loose key of every entry, with its first one, two and
|
||||
/// three characters in columns of their own, so that finding the keys a
|
||||
/// query starts is an equality an index answers whatever the database's
|
||||
/// collation (decided 2026-10-17: LIKE '%|eat%' over the entries' barred
|
||||
/// keys read every entry, 43–57 ms a search in JMdict; this takes 1–6 ms).
|
||||
/// Written by the loader beside the entries.
|
||||
abstract class DictionarySearchKey
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
DictionarySearchKey._({
|
||||
this.id,
|
||||
required this.source,
|
||||
required this.glossLanguage,
|
||||
required this.key,
|
||||
required this.loose,
|
||||
required this.start1,
|
||||
required this.start2,
|
||||
required this.start3,
|
||||
required this.sourceId,
|
||||
required this.rank,
|
||||
});
|
||||
|
||||
factory DictionarySearchKey({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required bool loose,
|
||||
required String start1,
|
||||
required String start2,
|
||||
required String start3,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) = _DictionarySearchKeyImpl;
|
||||
|
||||
factory DictionarySearchKey.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionarySearchKey(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
source: jsonSerialization['source'] as String,
|
||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||
key: jsonSerialization['key'] as String,
|
||||
loose: _isc.BoolJsonExtension.fromJson(jsonSerialization['loose']),
|
||||
start1: jsonSerialization['start1'] as String,
|
||||
start2: jsonSerialization['start2'] as String,
|
||||
start3: jsonSerialization['start3'] as String,
|
||||
sourceId: jsonSerialization['sourceId'] as String,
|
||||
rank: jsonSerialization['rank'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// The database id, set if the object has been inserted into the
|
||||
/// database or if it has been fetched from the database. Otherwise,
|
||||
/// the id will be null.
|
||||
int? id;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
String source;
|
||||
|
||||
String glossLanguage;
|
||||
|
||||
/// The key as the entry's `searchKeys` or `looseKeys` hold it.
|
||||
String key;
|
||||
|
||||
/// Whether it is a loose key: a spelling people type that isn't quite
|
||||
/// right, found after the right ones.
|
||||
bool loose;
|
||||
|
||||
/// The key's first character, its first two and its first three (a key
|
||||
/// shorter than that whole); characters as Unicode counts them.
|
||||
String start1;
|
||||
|
||||
String start2;
|
||||
|
||||
String start3;
|
||||
|
||||
/// The entry's id in its source, which with the source finds it.
|
||||
String sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries come first.
|
||||
int rank;
|
||||
|
||||
/// Returns a shallow copy of this [DictionarySearchKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
DictionarySearchKey copyWith({
|
||||
int? id,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
bool? loose,
|
||||
String? start1,
|
||||
String? start2,
|
||||
String? start3,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionarySearchKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'loose': loose,
|
||||
'start1': start1,
|
||||
'start2': start2,
|
||||
'start3': start3,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionarySearchKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'loose': loose,
|
||||
'start1': start1,
|
||||
'start2': start2,
|
||||
'start3': start3,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _DictionarySearchKeyImpl extends DictionarySearchKey {
|
||||
_DictionarySearchKeyImpl({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required bool loose,
|
||||
required String start1,
|
||||
required String start2,
|
||||
required String start3,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) : super._(
|
||||
id: id,
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
loose: loose,
|
||||
start1: start1,
|
||||
start2: start2,
|
||||
start3: start3,
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionarySearchKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
DictionarySearchKey copyWith({
|
||||
Object? id = _Undefined,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
bool? loose,
|
||||
String? start1,
|
||||
String? start2,
|
||||
String? start3,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
}) {
|
||||
return DictionarySearchKey(
|
||||
id: id is int? ? id : this.id,
|
||||
source: source ?? this.source,
|
||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||
key: key ?? this.key,
|
||||
loose: loose ?? this.loose,
|
||||
start1: start1 ?? this.start1,
|
||||
start2: start2 ?? this.start2,
|
||||
start3: start3 ?? this.start3,
|
||||
sourceId: sourceId ?? this.sourceId,
|
||||
rank: rank ?? this.rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ import 'dictionary/dictionary_entry.dart' as _i4ipxv77;
|
|||
import 'dictionary/dictionary_hit.dart' as _ivbh47kc;
|
||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||
import 'dictionary/dictionary_search_key.dart' as _ip3xfr1w;
|
||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||
import 'discussion/discussion_post_view.dart' as _ifltqotq;
|
||||
import 'discussion/discussion_refusal.dart' as _iysrt1r7;
|
||||
|
|
@ -122,6 +123,7 @@ export 'dictionary/dictionary_entry.dart';
|
|||
export 'dictionary/dictionary_hit.dart';
|
||||
export 'dictionary/dictionary_key.dart';
|
||||
export 'dictionary/dictionary_key_hit.dart';
|
||||
export 'dictionary/dictionary_search_key.dart';
|
||||
export 'dictionary/dictionary_source.dart';
|
||||
export 'discussion/discussion_post_view.dart';
|
||||
export 'discussion/discussion_refusal.dart';
|
||||
|
|
@ -2377,6 +2379,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ip3xfr1w.DictionarySearchKey) {
|
||||
return _ip3xfr1w.DictionarySearchKey.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ie12eb65.DictionarySource) {
|
||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -2594,6 +2599,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ip3xfr1w.DictionarySearchKey?>()) {
|
||||
return (data != null
|
||||
? _ip3xfr1w.DictionarySearchKey.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ie12eb65.DictionarySource?>()) {
|
||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -2895,6 +2906,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||
_ip3xfr1w.DictionarySearchKey => 'DictionarySearchKey',
|
||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
||||
_iysrt1r7.DiscussionRefusal => 'DiscussionRefusal',
|
||||
|
|
@ -2997,6 +3009,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return 'DictionaryKey';
|
||||
case _i6b9unou.DictionaryKeyHit():
|
||||
return 'DictionaryKeyHit';
|
||||
case _ip3xfr1w.DictionarySearchKey():
|
||||
return 'DictionarySearchKey';
|
||||
case _ie12eb65.DictionarySource():
|
||||
return 'DictionarySource';
|
||||
case _ifltqotq.DiscussionPostView():
|
||||
|
|
@ -3173,6 +3187,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName == 'DictionaryKeyHit') {
|
||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySearchKey') {
|
||||
return deserialize<_ip3xfr1w.DictionarySearchKey>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySource') {
|
||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:halkyon_learn_server/src/dictionary/device_dictionaries.dart';
|
||||
import 'package:halkyon_learn_server/src/dictionary/dictionary_loader.dart';
|
||||
import 'package:halkyon_learn_server/src/dictionary/dictionary_search.dart';
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
|
@ -59,6 +62,93 @@ class DictionaryEndpoint extends Endpoint {
|
|||
return const [];
|
||||
}
|
||||
final count = (limit ?? 30).clamp(1, maxSearchResults);
|
||||
final entries = await hasSearchKeys(session, source, glossLanguage)
|
||||
? await _byKeys(session, source, glossLanguage, variants, count)
|
||||
: await _byBarredKeys(session, source, glossLanguage, variants, count);
|
||||
return [for (final entry in entries) _hit(entry)];
|
||||
}
|
||||
|
||||
/// The first [count] entries [variants] find through the dictionary's
|
||||
/// search keys (`DictionarySearchKey`), in [search]'s order, then by the
|
||||
/// entry's id in its source. A variant's keys are found by their first
|
||||
/// characters, up to three, which the index holds, and a longer variant's
|
||||
/// among those by LIKE.
|
||||
Future<List<DictionaryEntry>> _byKeys(
|
||||
Session session,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> variants,
|
||||
int count,
|
||||
) async {
|
||||
final parameters = <String, Object?>{
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'limit': count,
|
||||
};
|
||||
final starting = <String>[];
|
||||
final whole = <String>[];
|
||||
for (final (i, variant) in variants.indexed) {
|
||||
// As the keys hold it: a bar is a space there.
|
||||
final key = variant.replaceAll('|', ' ');
|
||||
final length = key.runes.length;
|
||||
final starts = math.min(length, 3);
|
||||
parameters['whole$i'] = key;
|
||||
whole.add('"key" = @whole$i');
|
||||
for (var n = 1; n <= starts; n++) {
|
||||
parameters['start${n}of$i'] = keyStart(key, n);
|
||||
}
|
||||
if (length > 3) {
|
||||
parameters['starts$i'] = '${likeEscaped(variant)}%';
|
||||
}
|
||||
starting.add(
|
||||
[
|
||||
for (var n = 1; n <= starts; n++) '"start$n" = @start${n}of$i',
|
||||
if (length > 3) '"key" LIKE @starts$i',
|
||||
].join(' AND '),
|
||||
);
|
||||
}
|
||||
final isWhole = whole.join(' OR ');
|
||||
// SELECT "sourceId" FROM "dictionary_search_key" WHERE the dictionary
|
||||
// is this one AND a key starts with a variant, GROUP BY entry,
|
||||
// ORDER BY its best tier (a key that is a variant, a loose key that
|
||||
// is, a key starting with one, a loose key), its rank, its id.
|
||||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT "sourceId" FROM "dictionary_search_key" '
|
||||
'WHERE "source" = @source AND "glossLanguage" = @glossLanguage '
|
||||
'AND ((${starting.join(') OR (')})) '
|
||||
'GROUP BY "sourceId" '
|
||||
'ORDER BY min(CASE '
|
||||
'WHEN NOT "loose" AND ($isWhole) THEN 0 '
|
||||
'WHEN "loose" AND ($isWhole) THEN 1 '
|
||||
'WHEN NOT "loose" THEN 2 ELSE 3 END), min("rank"), "sourceId" '
|
||||
'LIMIT @limit',
|
||||
parameters: QueryParameters.named(parameters),
|
||||
);
|
||||
final sourceIds = [for (final row in rows) row.first! as String];
|
||||
final bySourceId = {
|
||||
for (final entry in await DictionaryEntry.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) &
|
||||
t.glossLanguage.equals(glossLanguage) &
|
||||
t.sourceId.inSet(sourceIds.toSet()),
|
||||
))
|
||||
entry.sourceId: entry,
|
||||
};
|
||||
return [for (final id in sourceIds) ?bySourceId[id]];
|
||||
}
|
||||
|
||||
/// [_byKeys] as the search was before search keys had rows of their own:
|
||||
/// LIKE over every entry's barred keys, which reads them all. Only for a
|
||||
/// dictionary loaded before them, until it loads again, which the server
|
||||
/// does as it starts.
|
||||
Future<List<DictionaryEntry>> _byBarredKeys(
|
||||
Session session,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> variants,
|
||||
int count,
|
||||
) async {
|
||||
// One scan, in the order the tiers give: a key that is the query, a
|
||||
// loose key that is, a key starting with it, a loose key starting with
|
||||
// it; within each the dictionary's order. A key that is the query also
|
||||
|
|
@ -96,10 +186,7 @@ class DictionaryEndpoint extends Endpoint {
|
|||
))
|
||||
entry.id: entry,
|
||||
};
|
||||
return [
|
||||
for (final id in ids)
|
||||
if (byId[id] case final entry?) _hit(entry),
|
||||
];
|
||||
return [for (final id in ids) ?byId[id]];
|
||||
}
|
||||
|
||||
/// The most keys [withKeys] takes at once.
|
||||
|
|
|
|||
|
|
@ -23,10 +23,14 @@ fields:
|
|||
### Its labels between bars (`|common|jlpt:n5|`), for LIKE to find one.
|
||||
labels: String
|
||||
### What a search matches, lower case, between bars
|
||||
### (`|食べる|たべる|taberu|eat|`).
|
||||
### (`|食べる|たべる|taberu|eat|`). A search reads them from
|
||||
### `DictionarySearchKey` since 2026-10-17; these stay for a dictionary
|
||||
### loaded before, until it loads again, and for the server a deploy
|
||||
### replaces, until a later release drops them.
|
||||
searchKeys: String
|
||||
### What a search matches after every searchKeys match: spellings people
|
||||
### type that aren't quite right (`sushi` for すうし), between bars too.
|
||||
### type that aren't quite right (`sushi` for すうし), between bars too;
|
||||
### kept as searchKeys are.
|
||||
looseKeys: String
|
||||
### The entry as cards take it: its JSON, as the file holds it.
|
||||
entry: String
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@ Future<void> _unloadGone(Session session, Set<(String, String)> held) async {
|
|||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionarySearchKey.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(loaded.source) &
|
||||
t.glossLanguage.equals(loaded.glossLanguage),
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionarySource.db.deleteRow(
|
||||
session,
|
||||
loaded,
|
||||
|
|
@ -164,14 +172,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
);
|
||||
// A dictionary loaded before its keys had rows of their own loads again.
|
||||
// A dictionary loaded before its keys, or its search keys, had rows of
|
||||
// their own loads again.
|
||||
if (loaded?.fileHash == hash &&
|
||||
await DictionaryKey.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
) !=
|
||||
null) {
|
||||
null &&
|
||||
await hasSearchKeys(session, source, glossLanguage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -196,8 +206,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionarySearchKey.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
final batch = <DictionaryEntry>[];
|
||||
final keys = <DictionaryKey>[];
|
||||
final searchKeys = <DictionarySearchKey>[];
|
||||
Future<void> flush() async {
|
||||
await DictionaryEntry.db.insert(
|
||||
session,
|
||||
|
|
@ -211,9 +229,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionarySearchKey.db.insert(
|
||||
session,
|
||||
searchKeys,
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
count += batch.length;
|
||||
batch.clear();
|
||||
keys.clear();
|
||||
searchKeys.clear();
|
||||
}
|
||||
|
||||
await for (final line in _lines(file).skip(1)) {
|
||||
|
|
@ -227,6 +252,7 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
final sourceId = json['id']! as String;
|
||||
final rank = json['rank']! as int;
|
||||
final entryKeys = (json['keys']! as List).cast<String>();
|
||||
final looseKeys = (json['looseKeys'] as List? ?? const []).cast<String>();
|
||||
// What finds the entry in running text: its own forms where the file
|
||||
// names them (a German word's, never its English meanings, which
|
||||
// would match English words in German text), else every key.
|
||||
|
|
@ -245,6 +271,25 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
rank: rank,
|
||||
),
|
||||
]);
|
||||
// What a search finds it by: every key, as its barred keys hold it.
|
||||
searchKeys.addAll([
|
||||
for (final (key, loose) in {
|
||||
for (final key in entryKeys) (key.replaceAll('|', ' '), false),
|
||||
for (final key in looseKeys) (key.replaceAll('|', ' '), true),
|
||||
})
|
||||
if (key.isNotEmpty)
|
||||
DictionarySearchKey(
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
loose: loose,
|
||||
start1: keyStart(key, 1),
|
||||
start2: keyStart(key, 2),
|
||||
start3: keyStart(key, 3),
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
),
|
||||
]);
|
||||
batch.add(
|
||||
DictionaryEntry(
|
||||
source: source,
|
||||
|
|
@ -256,9 +301,7 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
rank: rank,
|
||||
labels: barred((json['labels']! as List).cast<String>()),
|
||||
searchKeys: barred(entryKeys),
|
||||
looseKeys: barred(
|
||||
(json['looseKeys'] as List? ?? const []).cast<String>(),
|
||||
),
|
||||
looseKeys: barred(looseKeys),
|
||||
entry: entry,
|
||||
version: entryVersion(headword, reading, entry),
|
||||
),
|
||||
|
|
@ -305,7 +348,7 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
// Fresh statistics, or Postgres plans withKeys' lookups as if the keys
|
||||
// were few and reads all of them (0.6 s for JMdict rather than 20 ms).
|
||||
await session.db.unsafeExecute(
|
||||
'ANALYZE "dictionary_entry", "dictionary_key"',
|
||||
'ANALYZE "dictionary_entry", "dictionary_key", "dictionary_search_key"',
|
||||
);
|
||||
session.log(
|
||||
'Loaded the dictionary $name: $count entries of $source '
|
||||
|
|
@ -314,6 +357,21 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Whether the dictionary of [source] in [glossLanguage] has its search
|
||||
/// keys: one loaded before they had rows of their own has none until it
|
||||
/// loads again, and is searched as before meanwhile.
|
||||
Future<bool> hasSearchKeys(
|
||||
Session session,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
) async =>
|
||||
await DictionarySearchKey.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
) !=
|
||||
null;
|
||||
|
||||
/// A digest of what a card takes of an entry: its [headword], [reading]
|
||||
/// and [entry] (JSON). Sixteen hexadecimal digits of their SHA-256: enough
|
||||
/// to tell a corrected entry from the one a card was made from.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ String likeEscaped(String text) => text
|
|||
.replaceAll('_', r'\_')
|
||||
.replaceAll('|', ' ');
|
||||
|
||||
/// The first [length] characters of [key], as Unicode counts them (a
|
||||
/// character beyond 16 bits, 𠮟, is one); all of it when shorter. A search
|
||||
/// key's `start1` to `start3`, and what a query looks them up by.
|
||||
String keyStart(String key, int length) =>
|
||||
String.fromCharCodes(key.runes.take(length));
|
||||
|
||||
/// [values] between bars, as a dictionary entry's labels and search keys
|
||||
/// are kept, for LIKE to find one whole (`%|eat|%`) or by its start.
|
||||
String barred(Iterable<String> values) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
### A key a search for words matches (`DictionaryEndpoint.search`), one row
|
||||
### each: every key and loose key of every entry, with its first one, two and
|
||||
### three characters in columns of their own, so that finding the keys a
|
||||
### query starts is an equality an index answers whatever the database's
|
||||
### collation (decided 2026-10-17: LIKE '%|eat%' over the entries' barred
|
||||
### keys read every entry, 43–57 ms a search in JMdict; this takes 1–6 ms).
|
||||
### Written by the loader beside the entries.
|
||||
class: DictionarySearchKey
|
||||
table: dictionary_search_key
|
||||
fields:
|
||||
### The entry's source and meaning language, as on the entry.
|
||||
source: String
|
||||
glossLanguage: String
|
||||
### The key as the entry's `searchKeys` or `looseKeys` hold it.
|
||||
key: String
|
||||
### Whether it is a loose key: a spelling people type that isn't quite
|
||||
### right, found after the right ones.
|
||||
loose: bool
|
||||
### The key's first character, its first two and its first three (a key
|
||||
### shorter than that whole); characters as Unicode counts them.
|
||||
start1: String
|
||||
start2: String
|
||||
start3: String
|
||||
### The entry's id in its source, which with the source finds it.
|
||||
sourceId: String
|
||||
### The entry's rank, so the best entries come first.
|
||||
rank: int
|
||||
indexes:
|
||||
# The keys starting with up to three characters, in a dictionary.
|
||||
dictionary_search_key_start_idx:
|
||||
fields: source, glossLanguage, start1, start2, start3
|
||||
|
|
@ -101,11 +101,15 @@ abstract class DictionaryEntry
|
|||
String labels;
|
||||
|
||||
/// What a search matches, lower case, between bars
|
||||
/// (`|食べる|たべる|taberu|eat|`).
|
||||
/// (`|食べる|たべる|taberu|eat|`). A search reads them from
|
||||
/// `DictionarySearchKey` since 2026-10-17; these stay for a dictionary
|
||||
/// loaded before, until it loads again, and for the server a deploy
|
||||
/// replaces, until a later release drops them.
|
||||
String searchKeys;
|
||||
|
||||
/// What a search matches after every searchKeys match: spellings people
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too.
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too;
|
||||
/// kept as searchKeys are.
|
||||
String looseKeys;
|
||||
|
||||
/// The entry as cards take it: its JSON, as the file holds it.
|
||||
|
|
@ -422,11 +426,15 @@ class DictionaryEntryTable extends _is.Table<int?> {
|
|||
late final _is.ColumnString labels;
|
||||
|
||||
/// What a search matches, lower case, between bars
|
||||
/// (`|食べる|たべる|taberu|eat|`).
|
||||
/// (`|食べる|たべる|taberu|eat|`). A search reads them from
|
||||
/// `DictionarySearchKey` since 2026-10-17; these stay for a dictionary
|
||||
/// loaded before, until it loads again, and for the server a deploy
|
||||
/// replaces, until a later release drops them.
|
||||
late final _is.ColumnString searchKeys;
|
||||
|
||||
/// What a search matches after every searchKeys match: spellings people
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too.
|
||||
/// type that aren't quite right (`sushi` for すうし), between bars too;
|
||||
/// kept as searchKeys are.
|
||||
late final _is.ColumnString looseKeys;
|
||||
|
||||
/// The entry as cards take it: its JSON, as the file holds it.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,796 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A key a search for words matches (`DictionaryEndpoint.search`), one row
|
||||
/// each: every key and loose key of every entry, with its first one, two and
|
||||
/// three characters in columns of their own, so that finding the keys a
|
||||
/// query starts is an equality an index answers whatever the database's
|
||||
/// collation (decided 2026-10-17: LIKE '%|eat%' over the entries' barred
|
||||
/// keys read every entry, 43–57 ms a search in JMdict; this takes 1–6 ms).
|
||||
/// Written by the loader beside the entries.
|
||||
abstract class DictionarySearchKey
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
DictionarySearchKey._({
|
||||
this.id,
|
||||
required this.source,
|
||||
required this.glossLanguage,
|
||||
required this.key,
|
||||
required this.loose,
|
||||
required this.start1,
|
||||
required this.start2,
|
||||
required this.start3,
|
||||
required this.sourceId,
|
||||
required this.rank,
|
||||
});
|
||||
|
||||
factory DictionarySearchKey({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required bool loose,
|
||||
required String start1,
|
||||
required String start2,
|
||||
required String start3,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) = _DictionarySearchKeyImpl;
|
||||
|
||||
factory DictionarySearchKey.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionarySearchKey(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
source: jsonSerialization['source'] as String,
|
||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||
key: jsonSerialization['key'] as String,
|
||||
loose: _is.BoolJsonExtension.fromJson(jsonSerialization['loose']),
|
||||
start1: jsonSerialization['start1'] as String,
|
||||
start2: jsonSerialization['start2'] as String,
|
||||
start3: jsonSerialization['start3'] as String,
|
||||
sourceId: jsonSerialization['sourceId'] as String,
|
||||
rank: jsonSerialization['rank'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
static final t = DictionarySearchKeyTable();
|
||||
|
||||
static const db = DictionarySearchKeyRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
String source;
|
||||
|
||||
String glossLanguage;
|
||||
|
||||
/// The key as the entry's `searchKeys` or `looseKeys` hold it.
|
||||
String key;
|
||||
|
||||
/// Whether it is a loose key: a spelling people type that isn't quite
|
||||
/// right, found after the right ones.
|
||||
bool loose;
|
||||
|
||||
/// The key's first character, its first two and its first three (a key
|
||||
/// shorter than that whole); characters as Unicode counts them.
|
||||
String start1;
|
||||
|
||||
String start2;
|
||||
|
||||
String start3;
|
||||
|
||||
/// The entry's id in its source, which with the source finds it.
|
||||
String sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries come first.
|
||||
int rank;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [DictionarySearchKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
DictionarySearchKey copyWith({
|
||||
int? id,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
bool? loose,
|
||||
String? start1,
|
||||
String? start2,
|
||||
String? start3,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionarySearchKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'loose': loose,
|
||||
'start1': start1,
|
||||
'start2': start2,
|
||||
'start3': start3,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionarySearchKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'loose': loose,
|
||||
'start1': start1,
|
||||
'start2': start2,
|
||||
'start3': start3,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
static DictionarySearchKeyInclude include() {
|
||||
return DictionarySearchKeyInclude._();
|
||||
}
|
||||
|
||||
static DictionarySearchKeyIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
DictionarySearchKeyInclude? include,
|
||||
}) {
|
||||
return DictionarySearchKeyIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _DictionarySearchKeyImpl extends DictionarySearchKey {
|
||||
_DictionarySearchKeyImpl({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required bool loose,
|
||||
required String start1,
|
||||
required String start2,
|
||||
required String start3,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) : super._(
|
||||
id: id,
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
loose: loose,
|
||||
start1: start1,
|
||||
start2: start2,
|
||||
start3: start3,
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionarySearchKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
DictionarySearchKey copyWith({
|
||||
Object? id = _Undefined,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
bool? loose,
|
||||
String? start1,
|
||||
String? start2,
|
||||
String? start3,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
}) {
|
||||
return DictionarySearchKey(
|
||||
id: id is int? ? id : this.id,
|
||||
source: source ?? this.source,
|
||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||
key: key ?? this.key,
|
||||
loose: loose ?? this.loose,
|
||||
start1: start1 ?? this.start1,
|
||||
start2: start2 ?? this.start2,
|
||||
start3: start3 ?? this.start3,
|
||||
sourceId: sourceId ?? this.sourceId,
|
||||
rank: rank ?? this.rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DictionarySearchKeyUpdateTable
|
||||
extends _is.UpdateTable<DictionarySearchKeyTable> {
|
||||
DictionarySearchKeyUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<String, String> source(String value) => _is.ColumnValue(
|
||||
table.source,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> glossLanguage(String value) =>
|
||||
_is.ColumnValue(
|
||||
table.glossLanguage,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> key(String value) => _is.ColumnValue(
|
||||
table.key,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<bool, bool> loose(bool value) => _is.ColumnValue(
|
||||
table.loose,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> start1(String value) => _is.ColumnValue(
|
||||
table.start1,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> start2(String value) => _is.ColumnValue(
|
||||
table.start2,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> start3(String value) => _is.ColumnValue(
|
||||
table.start3,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> sourceId(String value) => _is.ColumnValue(
|
||||
table.sourceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> rank(int value) => _is.ColumnValue(
|
||||
table.rank,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class DictionarySearchKeyTable extends _is.Table<int?> {
|
||||
DictionarySearchKeyTable({super.tableRelation})
|
||||
: super(tableName: 'dictionary_search_key') {
|
||||
updateTable = DictionarySearchKeyUpdateTable(this);
|
||||
source = _is.ColumnString(
|
||||
'source',
|
||||
this,
|
||||
);
|
||||
glossLanguage = _is.ColumnString(
|
||||
'glossLanguage',
|
||||
this,
|
||||
);
|
||||
key = _is.ColumnString(
|
||||
'key',
|
||||
this,
|
||||
);
|
||||
loose = _is.ColumnBool(
|
||||
'loose',
|
||||
this,
|
||||
);
|
||||
start1 = _is.ColumnString(
|
||||
'start1',
|
||||
this,
|
||||
);
|
||||
start2 = _is.ColumnString(
|
||||
'start2',
|
||||
this,
|
||||
);
|
||||
start3 = _is.ColumnString(
|
||||
'start3',
|
||||
this,
|
||||
);
|
||||
sourceId = _is.ColumnString(
|
||||
'sourceId',
|
||||
this,
|
||||
);
|
||||
rank = _is.ColumnInt(
|
||||
'rank',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final DictionarySearchKeyUpdateTable updateTable;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
late final _is.ColumnString source;
|
||||
|
||||
late final _is.ColumnString glossLanguage;
|
||||
|
||||
/// The key as the entry's `searchKeys` or `looseKeys` hold it.
|
||||
late final _is.ColumnString key;
|
||||
|
||||
/// Whether it is a loose key: a spelling people type that isn't quite
|
||||
/// right, found after the right ones.
|
||||
late final _is.ColumnBool loose;
|
||||
|
||||
/// The key's first character, its first two and its first three (a key
|
||||
/// shorter than that whole); characters as Unicode counts them.
|
||||
late final _is.ColumnString start1;
|
||||
|
||||
late final _is.ColumnString start2;
|
||||
|
||||
late final _is.ColumnString start3;
|
||||
|
||||
/// The entry's id in its source, which with the source finds it.
|
||||
late final _is.ColumnString sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries come first.
|
||||
late final _is.ColumnInt rank;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
source,
|
||||
glossLanguage,
|
||||
key,
|
||||
loose,
|
||||
start1,
|
||||
start2,
|
||||
start3,
|
||||
sourceId,
|
||||
rank,
|
||||
];
|
||||
}
|
||||
|
||||
class DictionarySearchKeyInclude extends _is.IncludeObject {
|
||||
DictionarySearchKeyInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => DictionarySearchKey.t;
|
||||
}
|
||||
|
||||
class DictionarySearchKeyIncludeList extends _is.IncludeList {
|
||||
DictionarySearchKeyIncludeList._({
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(DictionarySearchKey.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => DictionarySearchKey.t;
|
||||
}
|
||||
|
||||
class DictionarySearchKeyRepository {
|
||||
const DictionarySearchKeyRepository._();
|
||||
|
||||
/// Returns a list of [DictionarySearchKey]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<DictionarySearchKey>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<DictionarySearchKey>(
|
||||
where: where?.call(DictionarySearchKey.t),
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [DictionarySearchKey] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<DictionarySearchKey?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<DictionarySearchKey>(
|
||||
where: where?.call(DictionarySearchKey.t),
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [DictionarySearchKey] by its [id] or null if no such row exists.
|
||||
Future<DictionarySearchKey?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<DictionarySearchKey>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [DictionarySearchKey]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [DictionarySearchKey]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionarySearchKey> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<DictionarySearchKey>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [DictionarySearchKey] and returns the inserted row.
|
||||
///
|
||||
/// The returned [DictionarySearchKey] will have its `id` field set.
|
||||
Future<DictionarySearchKey> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionarySearchKey row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<DictionarySearchKey>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [DictionarySearchKey]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [DictionarySearchKey]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionarySearchKey> rows, {
|
||||
required _is.ColumnSelections<DictionarySearchKeyTable> conflictColumns,
|
||||
_is.ColumnSelections<DictionarySearchKeyTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<DictionarySearchKey>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(DictionarySearchKey.t),
|
||||
updateColumns: updateColumns?.call(DictionarySearchKey.t),
|
||||
updateWhere: updateWhere?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [DictionarySearchKey] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [DictionarySearchKey] will have its `id` field set.
|
||||
Future<DictionarySearchKey?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionarySearchKey row, {
|
||||
required _is.ColumnSelections<DictionarySearchKeyTable> conflictColumns,
|
||||
_is.ColumnSelections<DictionarySearchKeyTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<DictionarySearchKey>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(DictionarySearchKey.t),
|
||||
updateColumns: updateColumns?.call(DictionarySearchKey.t),
|
||||
updateWhere: updateWhere?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [DictionarySearchKey]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionarySearchKey> rows, {
|
||||
_is.ColumnSelections<DictionarySearchKeyTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<DictionarySearchKey>(
|
||||
rows,
|
||||
columns: columns?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [DictionarySearchKey]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<DictionarySearchKey> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionarySearchKey row, {
|
||||
_is.ColumnSelections<DictionarySearchKeyTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<DictionarySearchKey>(
|
||||
row,
|
||||
columns: columns?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [DictionarySearchKey] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<DictionarySearchKey?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<DictionarySearchKeyUpdateTable>
|
||||
columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<DictionarySearchKey>(
|
||||
id,
|
||||
columnValues: columnValues(DictionarySearchKey.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [DictionarySearchKey]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<DictionarySearchKeyUpdateTable>
|
||||
columnValues,
|
||||
required _is.WhereExpressionBuilder<DictionarySearchKeyTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<DictionarySearchKey>(
|
||||
columnValues: columnValues(DictionarySearchKey.t.updateTable),
|
||||
where: where(DictionarySearchKey.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [DictionarySearchKey]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionarySearchKey> rows, {
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<DictionarySearchKey>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [DictionarySearchKey].
|
||||
Future<DictionarySearchKey> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionarySearchKey row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<DictionarySearchKey>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionarySearchKey>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<DictionarySearchKeyTable> where,
|
||||
_is.OrderByBuilder<DictionarySearchKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionarySearchKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<DictionarySearchKey>(
|
||||
where: where(DictionarySearchKey.t),
|
||||
orderBy: orderBy?.call(DictionarySearchKey.t),
|
||||
orderByList: orderByList?.call(DictionarySearchKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionarySearchKeyTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<DictionarySearchKey>(
|
||||
where: where?.call(DictionarySearchKey.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [DictionarySearchKey] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<DictionarySearchKeyTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<DictionarySearchKey>(
|
||||
where: where(DictionarySearchKey.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ import 'dictionary/dictionary_entry.dart' as _i4ipxv77;
|
|||
import 'dictionary/dictionary_hit.dart' as _ivbh47kc;
|
||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||
import 'dictionary/dictionary_search_key.dart' as _ip3xfr1w;
|
||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||
import 'discussion/discussion_post.dart' as _ig6tsv8f;
|
||||
import 'discussion/discussion_post_view.dart' as _ifltqotq;
|
||||
|
|
@ -143,6 +144,7 @@ export 'dictionary/dictionary_entry.dart';
|
|||
export 'dictionary/dictionary_hit.dart';
|
||||
export 'dictionary/dictionary_key.dart';
|
||||
export 'dictionary/dictionary_key_hit.dart';
|
||||
export 'dictionary/dictionary_search_key.dart';
|
||||
export 'dictionary/dictionary_source.dart';
|
||||
export 'discussion/discussion_post.dart';
|
||||
export 'discussion/discussion_post_view.dart';
|
||||
|
|
@ -1240,6 +1242,108 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'dictionary_search_key',
|
||||
dartName: 'DictionarySearchKey',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int?',
|
||||
columnDefault: 'serial',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'source',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'glossLanguage',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'key',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'loose',
|
||||
columnType: _isp.ColumnType.boolean,
|
||||
isNullable: false,
|
||||
dartType: 'bool',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'start1',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'start2',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'start3',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'sourceId',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'rank',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
],
|
||||
foreignKeys: [],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'dictionary_search_key_start_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'source',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'glossLanguage',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'start1',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'start2',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'start3',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'dictionary_source',
|
||||
dartName: 'DictionarySource',
|
||||
|
|
@ -4090,6 +4194,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ip3xfr1w.DictionarySearchKey) {
|
||||
return _ip3xfr1w.DictionarySearchKey.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ie12eb65.DictionarySource) {
|
||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -4360,6 +4467,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ip3xfr1w.DictionarySearchKey?>()) {
|
||||
return (data != null
|
||||
? _ip3xfr1w.DictionarySearchKey.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ie12eb65.DictionarySource?>()) {
|
||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -4698,6 +4811,7 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||
_ip3xfr1w.DictionarySearchKey => 'DictionarySearchKey',
|
||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||
_ig6tsv8f.DiscussionPost => 'DiscussionPost',
|
||||
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
||||
|
|
@ -4821,6 +4935,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'DictionaryKey';
|
||||
case _i6b9unou.DictionaryKeyHit():
|
||||
return 'DictionaryKeyHit';
|
||||
case _ip3xfr1w.DictionarySearchKey():
|
||||
return 'DictionarySearchKey';
|
||||
case _ie12eb65.DictionarySource():
|
||||
return 'DictionarySource';
|
||||
case _ig6tsv8f.DiscussionPost():
|
||||
|
|
@ -5036,6 +5152,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'DictionaryKeyHit') {
|
||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySearchKey') {
|
||||
return deserialize<_ip3xfr1w.DictionarySearchKey>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySource') {
|
||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
||||
}
|
||||
|
|
@ -5258,6 +5377,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _i4ipxv77.DictionaryEntry.t;
|
||||
case _i9gd3xbn.DictionaryKey:
|
||||
return _i9gd3xbn.DictionaryKey.t;
|
||||
case _ip3xfr1w.DictionarySearchKey:
|
||||
return _ip3xfr1w.DictionarySearchKey.t;
|
||||
case _ie12eb65.DictionarySource:
|
||||
return _ie12eb65.DictionarySource.t;
|
||||
case _ig6tsv8f.DiscussionPost:
|
||||
|
|
|
|||
9082
halkyon_learn_server/migrations/20261017110130653/definition.json
Normal file
9082
halkyon_learn_server/migrations/20261017110130653/definition.json
Normal file
File diff suppressed because it is too large
Load diff
2191
halkyon_learn_server/migrations/20261017110130653/definition.sql
Normal file
2191
halkyon_learn_server/migrations/20261017110130653/definition.sql
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
139
halkyon_learn_server/migrations/20261017110130653/migration.json
Normal file
139
halkyon_learn_server/migrations/20261017110130653/migration.json
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "dictionary_search_key",
|
||||
"dartName": "DictionarySearchKey",
|
||||
"module": "halkyon_learn",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "source",
|
||||
"fieldName": "source",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "glossLanguage",
|
||||
"fieldName": "glossLanguage",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "key",
|
||||
"fieldName": "key",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "loose",
|
||||
"fieldName": "loose",
|
||||
"columnType": 1,
|
||||
"isNullable": false,
|
||||
"dartType": "bool"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "start1",
|
||||
"fieldName": "start1",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "start2",
|
||||
"fieldName": "start2",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "start3",
|
||||
"fieldName": "start3",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sourceId",
|
||||
"fieldName": "sourceId",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "rank",
|
||||
"fieldName": "rank",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "dictionary_search_key_start_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "source"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "glossLanguage"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "start1"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "start2"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "start3"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "dictionary_search_key" (
|
||||
"id" bigserial PRIMARY KEY,
|
||||
"source" text NOT NULL,
|
||||
"glossLanguage" text NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"loose" boolean NOT NULL,
|
||||
"start1" text NOT NULL,
|
||||
"start2" text NOT NULL,
|
||||
"start3" text NOT NULL,
|
||||
"sourceId" text NOT NULL,
|
||||
"rank" bigint NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "dictionary_search_key_start_idx" ON "dictionary_search_key" USING btree ("source", "glossLanguage", "start1", "start2", "start3");
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_learn
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_learn', '20261017110130653', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261017110130653', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_idp
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_idp', '20260924105404509', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_core
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_core', '20260924105232991', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -31,3 +31,4 @@
|
|||
20261015171326465-email-sign-in
|
||||
20261015220121195-verified-links
|
||||
20261015220626303-discussions
|
||||
20261017110130653
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ void main() {
|
|||
tearDown(() async {
|
||||
await DictionaryEntry.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await DictionaryKey.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await DictionarySearchKey.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id > 0,
|
||||
);
|
||||
await DictionarySource.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id > 0,
|
||||
|
|
@ -397,6 +401,52 @@ void main() {
|
|||
expect(await search('|'), isEmpty);
|
||||
});
|
||||
|
||||
test('a search finds a key by its first character, its first two, '
|
||||
'three or more, a character beyond 16 bits counting as one, and '
|
||||
'a key shorter than the query never', () async {
|
||||
final shikaru = entry(
|
||||
'1590120',
|
||||
'𠮟る',
|
||||
reading: 'しかる',
|
||||
rank: 120000,
|
||||
keys: ['𠮟る', 'しかる', 'shikaru', 'to scold', 'scold'],
|
||||
);
|
||||
await loadDictionaryFile(session, write([taberu, terebi, shikaru]));
|
||||
|
||||
expect(await search('𠮟'), ['𠮟る']);
|
||||
expect(await search('𠮟る'), ['𠮟る']);
|
||||
expect(await search('t'), ['食べる', 'テレビ', '𠮟る']);
|
||||
expect(await search('te'), ['テレビ']);
|
||||
expect(await search('tel'), ['テレビ']);
|
||||
expect(await search('tele'), ['テレビ']);
|
||||
expect(await search('televisions'), isEmpty);
|
||||
expect(await search('scold'), ['𠮟る']);
|
||||
});
|
||||
|
||||
test('search keys go with their dictionary: replaced with it, and a '
|
||||
'dictionary loaded before they had rows is searched as before '
|
||||
'until it loads again', () async {
|
||||
final file = write([taberu, terebi]);
|
||||
await loadDictionaryFile(session, file);
|
||||
// Every key and loose key, "to eat" with its space too.
|
||||
expect(await DictionarySearchKey.db.count(session), 10);
|
||||
|
||||
write([terebi], head: header(version: '3.6.3 (2026-11-01)'));
|
||||
await loadDictionaryFile(session, file);
|
||||
expect(await search('taberu'), isEmpty);
|
||||
expect(await DictionarySearchKey.db.count(session), 5);
|
||||
|
||||
await DictionarySearchKey.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id > 0,
|
||||
);
|
||||
expect(await search('tele'), ['テレビ']);
|
||||
expect(await loadDictionaryFile(session, file), isTrue);
|
||||
expect(await DictionarySearchKey.db.count(session), 5);
|
||||
expect(await search('tele'), ['テレビ']);
|
||||
expect(await loadDictionaryFile(session, file), isFalse);
|
||||
});
|
||||
|
||||
test('loads asked for at once leave the dictionary loaded once, '
|
||||
'whole', () async {
|
||||
write([taberu, terebi]);
|
||||
|
|
@ -449,6 +499,13 @@ void main() {
|
|||
),
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
await DictionarySearchKey.db.count(
|
||||
session,
|
||||
where: (t) => t.source.equals('other'),
|
||||
),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('two files of one source are refused, and neither loads, as '
|
||||
|
|
|
|||
Loading…
Reference in a new issue