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;
|
String labels;
|
||||||
|
|
||||||
/// What a search matches, lower case, between bars
|
/// 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;
|
String searchKeys;
|
||||||
|
|
||||||
/// What a search matches after every searchKeys match: spellings people
|
/// 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;
|
String looseKeys;
|
||||||
|
|
||||||
/// The entry as cards take it: its JSON, as the file holds it.
|
/// 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_hit.dart' as _ivbh47kc;
|
||||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||||
|
import 'dictionary/dictionary_search_key.dart' as _ip3xfr1w;
|
||||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||||
import 'discussion/discussion_post_view.dart' as _ifltqotq;
|
import 'discussion/discussion_post_view.dart' as _ifltqotq;
|
||||||
import 'discussion/discussion_refusal.dart' as _iysrt1r7;
|
import 'discussion/discussion_refusal.dart' as _iysrt1r7;
|
||||||
|
|
@ -122,6 +123,7 @@ export 'dictionary/dictionary_entry.dart';
|
||||||
export 'dictionary/dictionary_hit.dart';
|
export 'dictionary/dictionary_hit.dart';
|
||||||
export 'dictionary/dictionary_key.dart';
|
export 'dictionary/dictionary_key.dart';
|
||||||
export 'dictionary/dictionary_key_hit.dart';
|
export 'dictionary/dictionary_key_hit.dart';
|
||||||
|
export 'dictionary/dictionary_search_key.dart';
|
||||||
export 'dictionary/dictionary_source.dart';
|
export 'dictionary/dictionary_source.dart';
|
||||||
export 'discussion/discussion_post_view.dart';
|
export 'discussion/discussion_post_view.dart';
|
||||||
export 'discussion/discussion_refusal.dart';
|
export 'discussion/discussion_refusal.dart';
|
||||||
|
|
@ -2377,6 +2379,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
||||||
if (t == _i6b9unou.DictionaryKeyHit) {
|
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
if (t == _ip3xfr1w.DictionarySearchKey) {
|
||||||
|
return _ip3xfr1w.DictionarySearchKey.fromJson(data) as T;
|
||||||
|
}
|
||||||
if (t == _ie12eb65.DictionarySource) {
|
if (t == _ie12eb65.DictionarySource) {
|
||||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
|
@ -2594,6 +2599,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
||||||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
}
|
}
|
||||||
|
if (t == _isc.getType<_ip3xfr1w.DictionarySearchKey?>()) {
|
||||||
|
return (data != null
|
||||||
|
? _ip3xfr1w.DictionarySearchKey.fromJson(data)
|
||||||
|
: null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
if (t == _isc.getType<_ie12eb65.DictionarySource?>()) {
|
if (t == _isc.getType<_ie12eb65.DictionarySource?>()) {
|
||||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
|
|
@ -2895,6 +2906,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
||||||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||||
|
_ip3xfr1w.DictionarySearchKey => 'DictionarySearchKey',
|
||||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||||
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
||||||
_iysrt1r7.DiscussionRefusal => 'DiscussionRefusal',
|
_iysrt1r7.DiscussionRefusal => 'DiscussionRefusal',
|
||||||
|
|
@ -2997,6 +3009,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
||||||
return 'DictionaryKey';
|
return 'DictionaryKey';
|
||||||
case _i6b9unou.DictionaryKeyHit():
|
case _i6b9unou.DictionaryKeyHit():
|
||||||
return 'DictionaryKeyHit';
|
return 'DictionaryKeyHit';
|
||||||
|
case _ip3xfr1w.DictionarySearchKey():
|
||||||
|
return 'DictionarySearchKey';
|
||||||
case _ie12eb65.DictionarySource():
|
case _ie12eb65.DictionarySource():
|
||||||
return 'DictionarySource';
|
return 'DictionarySource';
|
||||||
case _ifltqotq.DiscussionPostView():
|
case _ifltqotq.DiscussionPostView():
|
||||||
|
|
@ -3173,6 +3187,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
||||||
if (dataClassName == 'DictionaryKeyHit') {
|
if (dataClassName == 'DictionaryKeyHit') {
|
||||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||||
}
|
}
|
||||||
|
if (dataClassName == 'DictionarySearchKey') {
|
||||||
|
return deserialize<_ip3xfr1w.DictionarySearchKey>(data['data']);
|
||||||
|
}
|
||||||
if (dataClassName == 'DictionarySource') {
|
if (dataClassName == 'DictionarySource') {
|
||||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
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/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/dictionary/dictionary_search.dart';
|
||||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
|
|
@ -59,6 +62,93 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
final count = (limit ?? 30).clamp(1, maxSearchResults);
|
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
|
// 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
|
// 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
|
// 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,
|
entry.id: entry,
|
||||||
};
|
};
|
||||||
return [
|
return [for (final id in ids) ?byId[id]];
|
||||||
for (final id in ids)
|
|
||||||
if (byId[id] case final entry?) _hit(entry),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The most keys [withKeys] takes at once.
|
/// The most keys [withKeys] takes at once.
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,14 @@ fields:
|
||||||
### Its labels between bars (`|common|jlpt:n5|`), for LIKE to find one.
|
### Its labels between bars (`|common|jlpt:n5|`), for LIKE to find one.
|
||||||
labels: String
|
labels: String
|
||||||
### What a search matches, lower case, between bars
|
### 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
|
searchKeys: String
|
||||||
### What a search matches after every searchKeys match: spellings people
|
### 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
|
looseKeys: String
|
||||||
### The entry as cards take it: its JSON, as the file holds it.
|
### The entry as cards take it: its JSON, as the file holds it.
|
||||||
entry: String
|
entry: String
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,14 @@ Future<void> _unloadGone(Session session, Set<(String, String)> held) async {
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
noReturn: true,
|
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(
|
await DictionarySource.db.deleteRow(
|
||||||
session,
|
session,
|
||||||
loaded,
|
loaded,
|
||||||
|
|
@ -164,14 +172,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
where: (t) =>
|
where: (t) =>
|
||||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
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 &&
|
if (loaded?.fileHash == hash &&
|
||||||
await DictionaryKey.db.findFirstRow(
|
await DictionaryKey.db.findFirstRow(
|
||||||
session,
|
session,
|
||||||
where: (t) =>
|
where: (t) =>
|
||||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||||
) !=
|
) !=
|
||||||
null) {
|
null &&
|
||||||
|
await hasSearchKeys(session, source, glossLanguage)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,8 +206,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
noReturn: true,
|
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 batch = <DictionaryEntry>[];
|
||||||
final keys = <DictionaryKey>[];
|
final keys = <DictionaryKey>[];
|
||||||
|
final searchKeys = <DictionarySearchKey>[];
|
||||||
Future<void> flush() async {
|
Future<void> flush() async {
|
||||||
await DictionaryEntry.db.insert(
|
await DictionaryEntry.db.insert(
|
||||||
session,
|
session,
|
||||||
|
|
@ -211,9 +229,16 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
noReturn: true,
|
noReturn: true,
|
||||||
);
|
);
|
||||||
|
await DictionarySearchKey.db.insert(
|
||||||
|
session,
|
||||||
|
searchKeys,
|
||||||
|
transaction: transaction,
|
||||||
|
noReturn: true,
|
||||||
|
);
|
||||||
count += batch.length;
|
count += batch.length;
|
||||||
batch.clear();
|
batch.clear();
|
||||||
keys.clear();
|
keys.clear();
|
||||||
|
searchKeys.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
await for (final line in _lines(file).skip(1)) {
|
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 sourceId = json['id']! as String;
|
||||||
final rank = json['rank']! as int;
|
final rank = json['rank']! as int;
|
||||||
final entryKeys = (json['keys']! as List).cast<String>();
|
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
|
// What finds the entry in running text: its own forms where the file
|
||||||
// names them (a German word's, never its English meanings, which
|
// names them (a German word's, never its English meanings, which
|
||||||
// would match English words in German text), else every key.
|
// would match English words in German text), else every key.
|
||||||
|
|
@ -245,6 +271,25 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
rank: rank,
|
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(
|
batch.add(
|
||||||
DictionaryEntry(
|
DictionaryEntry(
|
||||||
source: source,
|
source: source,
|
||||||
|
|
@ -256,9 +301,7 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
rank: rank,
|
rank: rank,
|
||||||
labels: barred((json['labels']! as List).cast<String>()),
|
labels: barred((json['labels']! as List).cast<String>()),
|
||||||
searchKeys: barred(entryKeys),
|
searchKeys: barred(entryKeys),
|
||||||
looseKeys: barred(
|
looseKeys: barred(looseKeys),
|
||||||
(json['looseKeys'] as List? ?? const []).cast<String>(),
|
|
||||||
),
|
|
||||||
entry: entry,
|
entry: entry,
|
||||||
version: entryVersion(headword, reading, 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
|
// 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).
|
// were few and reads all of them (0.6 s for JMdict rather than 20 ms).
|
||||||
await session.db.unsafeExecute(
|
await session.db.unsafeExecute(
|
||||||
'ANALYZE "dictionary_entry", "dictionary_key"',
|
'ANALYZE "dictionary_entry", "dictionary_key", "dictionary_search_key"',
|
||||||
);
|
);
|
||||||
session.log(
|
session.log(
|
||||||
'Loaded the dictionary $name: $count entries of $source '
|
'Loaded the dictionary $name: $count entries of $source '
|
||||||
|
|
@ -314,6 +357,21 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
return true;
|
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]
|
/// A digest of what a card takes of an entry: its [headword], [reading]
|
||||||
/// and [entry] (JSON). Sixteen hexadecimal digits of their SHA-256: enough
|
/// and [entry] (JSON). Sixteen hexadecimal digits of their SHA-256: enough
|
||||||
/// to tell a corrected entry from the one a card was made from.
|
/// 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('_', r'\_')
|
||||||
.replaceAll('|', ' ');
|
.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
|
/// [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.
|
/// are kept, for LIKE to find one whole (`%|eat|%`) or by its start.
|
||||||
String barred(Iterable<String> values) =>
|
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;
|
String labels;
|
||||||
|
|
||||||
/// What a search matches, lower case, between bars
|
/// 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;
|
String searchKeys;
|
||||||
|
|
||||||
/// What a search matches after every searchKeys match: spellings people
|
/// 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;
|
String looseKeys;
|
||||||
|
|
||||||
/// The entry as cards take it: its JSON, as the file holds it.
|
/// 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;
|
late final _is.ColumnString labels;
|
||||||
|
|
||||||
/// What a search matches, lower case, between bars
|
/// 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;
|
late final _is.ColumnString searchKeys;
|
||||||
|
|
||||||
/// What a search matches after every searchKeys match: spellings people
|
/// 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;
|
late final _is.ColumnString looseKeys;
|
||||||
|
|
||||||
/// The entry as cards take it: its JSON, as the file holds it.
|
/// 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_hit.dart' as _ivbh47kc;
|
||||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||||
|
import 'dictionary/dictionary_search_key.dart' as _ip3xfr1w;
|
||||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||||
import 'discussion/discussion_post.dart' as _ig6tsv8f;
|
import 'discussion/discussion_post.dart' as _ig6tsv8f;
|
||||||
import 'discussion/discussion_post_view.dart' as _ifltqotq;
|
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_hit.dart';
|
||||||
export 'dictionary/dictionary_key.dart';
|
export 'dictionary/dictionary_key.dart';
|
||||||
export 'dictionary/dictionary_key_hit.dart';
|
export 'dictionary/dictionary_key_hit.dart';
|
||||||
|
export 'dictionary/dictionary_search_key.dart';
|
||||||
export 'dictionary/dictionary_source.dart';
|
export 'dictionary/dictionary_source.dart';
|
||||||
export 'discussion/discussion_post.dart';
|
export 'discussion/discussion_post.dart';
|
||||||
export 'discussion/discussion_post_view.dart';
|
export 'discussion/discussion_post_view.dart';
|
||||||
|
|
@ -1240,6 +1242,108 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
],
|
],
|
||||||
managed: true,
|
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(
|
_isp.TableDefinition(
|
||||||
name: 'dictionary_source',
|
name: 'dictionary_source',
|
||||||
dartName: 'DictionarySource',
|
dartName: 'DictionarySource',
|
||||||
|
|
@ -4090,6 +4194,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
if (t == _i6b9unou.DictionaryKeyHit) {
|
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
if (t == _ip3xfr1w.DictionarySearchKey) {
|
||||||
|
return _ip3xfr1w.DictionarySearchKey.fromJson(data) as T;
|
||||||
|
}
|
||||||
if (t == _ie12eb65.DictionarySource) {
|
if (t == _ie12eb65.DictionarySource) {
|
||||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
|
@ -4360,6 +4467,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
}
|
}
|
||||||
|
if (t == _is.getType<_ip3xfr1w.DictionarySearchKey?>()) {
|
||||||
|
return (data != null
|
||||||
|
? _ip3xfr1w.DictionarySearchKey.fromJson(data)
|
||||||
|
: null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
if (t == _is.getType<_ie12eb65.DictionarySource?>()) {
|
if (t == _is.getType<_ie12eb65.DictionarySource?>()) {
|
||||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
|
|
@ -4698,6 +4811,7 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||||
|
_ip3xfr1w.DictionarySearchKey => 'DictionarySearchKey',
|
||||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||||
_ig6tsv8f.DiscussionPost => 'DiscussionPost',
|
_ig6tsv8f.DiscussionPost => 'DiscussionPost',
|
||||||
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
|
||||||
|
|
@ -4821,6 +4935,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
return 'DictionaryKey';
|
return 'DictionaryKey';
|
||||||
case _i6b9unou.DictionaryKeyHit():
|
case _i6b9unou.DictionaryKeyHit():
|
||||||
return 'DictionaryKeyHit';
|
return 'DictionaryKeyHit';
|
||||||
|
case _ip3xfr1w.DictionarySearchKey():
|
||||||
|
return 'DictionarySearchKey';
|
||||||
case _ie12eb65.DictionarySource():
|
case _ie12eb65.DictionarySource():
|
||||||
return 'DictionarySource';
|
return 'DictionarySource';
|
||||||
case _ig6tsv8f.DiscussionPost():
|
case _ig6tsv8f.DiscussionPost():
|
||||||
|
|
@ -5036,6 +5152,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
if (dataClassName == 'DictionaryKeyHit') {
|
if (dataClassName == 'DictionaryKeyHit') {
|
||||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||||
}
|
}
|
||||||
|
if (dataClassName == 'DictionarySearchKey') {
|
||||||
|
return deserialize<_ip3xfr1w.DictionarySearchKey>(data['data']);
|
||||||
|
}
|
||||||
if (dataClassName == 'DictionarySource') {
|
if (dataClassName == 'DictionarySource') {
|
||||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
||||||
}
|
}
|
||||||
|
|
@ -5258,6 +5377,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
return _i4ipxv77.DictionaryEntry.t;
|
return _i4ipxv77.DictionaryEntry.t;
|
||||||
case _i9gd3xbn.DictionaryKey:
|
case _i9gd3xbn.DictionaryKey:
|
||||||
return _i9gd3xbn.DictionaryKey.t;
|
return _i9gd3xbn.DictionaryKey.t;
|
||||||
|
case _ip3xfr1w.DictionarySearchKey:
|
||||||
|
return _ip3xfr1w.DictionarySearchKey.t;
|
||||||
case _ie12eb65.DictionarySource:
|
case _ie12eb65.DictionarySource:
|
||||||
return _ie12eb65.DictionarySource.t;
|
return _ie12eb65.DictionarySource.t;
|
||||||
case _ig6tsv8f.DiscussionPost:
|
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
|
20261015171326465-email-sign-in
|
||||||
20261015220121195-verified-links
|
20261015220121195-verified-links
|
||||||
20261015220626303-discussions
|
20261015220626303-discussions
|
||||||
|
20261017110130653
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,10 @@ void main() {
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await DictionaryEntry.db.deleteWhere(session, where: (t) => t.id > 0);
|
await DictionaryEntry.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||||
await DictionaryKey.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(
|
await DictionarySource.db.deleteWhere(
|
||||||
session,
|
session,
|
||||||
where: (t) => t.id > 0,
|
where: (t) => t.id > 0,
|
||||||
|
|
@ -397,6 +401,52 @@ void main() {
|
||||||
expect(await search('|'), isEmpty);
|
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, '
|
test('loads asked for at once leave the dictionary loaded once, '
|
||||||
'whole', () async {
|
'whole', () async {
|
||||||
write([taberu, terebi]);
|
write([taberu, terebi]);
|
||||||
|
|
@ -449,6 +499,13 @@ void main() {
|
||||||
),
|
),
|
||||||
0,
|
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 '
|
test('two files of one source are refused, and neither loads, as '
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue