feat: dictionaries hold words or characters, and are asked by their source
A dictionary source now says what it holds (`kind`: `words`, the default for files from before, or `characters`), a string so that an app older than a new kind leaves it out. Search, countLabelled and labelled take the dictionary by its source and meaning language, as the unique index names it, rather than by the language: with KANJIDIC2 beside JMdict, a search for words would otherwise bring kanji, and JLPT N5 would hold both. The search reads the language for its query variants from the source's row; a dictionary the instance doesn't hold finds nothing. The rank index goes by source too. Migration: drops and recreates dictionary_entry_rank_idx on (source, glossLanguage, rank), and adds dictionary_source.kind, text NOT NULL DEFAULT 'words', so a JMdict loaded before is a dictionary of words. Both tables are reference data, never synced; the old server keeps working until the switch (its queries by language just lose the index). In the app, a studyset's dictionary is one of words unless a kind is asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e42cdfbfcb
commit
a6699b30cb
23 changed files with 11756 additions and 84 deletions
|
|
@ -144,7 +144,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Tree, tags, modes, settings and files** (ADR 0004): a studyset's `parent` and sibling `position` form the tree, and studysets in a cycle or without their parent show at the top level (`topLevel`); its `archivedAt`, `starredAt` and `updatedAt` are columns, and every change of a studyset writes `updatedAt` with it (the library lists starred studysets first, then the most recently changed); a card's tags are `card_tag` rows, one per assignment, never updated, and a duplicate counts once; `review_log.mode` is `review`, `reverse` (its own schedule) or `cram` (never moves a schedule), and a schedule replays the reviews of one mode; `person_settings` is meant as one row per person, and extra rows fold into the one with the smallest id; an `asset` row stands for a file in private cloud storage, found by its hash. **Never derive a row's id from a person or anything else guessable**: ids are unique across the whole table, so someone could claim it first in their own space and break the owner's sync.
|
- **Tree, tags, modes, settings and files** (ADR 0004): a studyset's `parent` and sibling `position` form the tree, and studysets in a cycle or without their parent show at the top level (`topLevel`); its `archivedAt`, `starredAt` and `updatedAt` are columns, and every change of a studyset writes `updatedAt` with it (the library lists starred studysets first, then the most recently changed); a card's tags are `card_tag` rows, one per assignment, never updated, and a duplicate counts once; `review_log.mode` is `review`, `reverse` (its own schedule) or `cram` (never moves a schedule), and a schedule replays the reviews of one mode; `person_settings` is meant as one row per person, and extra rows fold into the one with the smallest id; an `asset` row stands for a file in private cloud storage, found by its hash. **Never derive a row's id from a person or anything else guessable**: ids are unique across the whole table, so someone could claim it first in their own space and break the owner's sync.
|
||||||
- **Files** (ADR 0004, rule 5): `AssetEndpoint` (`requireLogin`) takes uploads into the caller's own space by SHA-256 (`startUpload` gives Serverpod's upload description, or null when the file is there already and claims it anew; `finishUpload` keeps it only if its content has that hash) and serves an asset row's file to its space's owner and members only, `NotFoundException` otherwise, also for a deleted row (the sync engine hides tombstones). Files live in the private database storage at `spaces/<space>/<sha256>`, at most 20 MB each, and within limits (`file_limits.dart`, from the environment: an anonymous person 100 MB, an identified one 500 MB or what a `file_allowance` row grants them, the instance 15 GB), checked when an upload starts (unfinished uploads count, or they could fill the disk unnoticed) and again when it finishes (only what is kept counts), refused with `FileLimitExceededException`; `deletePerson` deletes the person's files and allowance, and `FilePruningFutureCall` deletes daily those no live row refers to (or never finished), 7 days after they were last claimed. In the app, `AssetRepository` (`core/asset/`) keeps files by hash in the person's folder (`files/`, uploads waiting marked in `files/uploads/`), uploads after each sync that went through, and downloads on demand, checking the hash; every hash is checked to be one before it becomes part of a path. Content names a file by the URL `asset:<asset row id>` (`assetUrl`, `RichContent.assetIds`): images are the editor's `image` block, sounds an `audio` block (`core/editor/file_blocks.dart`), and any other URL is never fetched. `StoredFileImage` loads through the repository like Flutter's `NetworkImage`, keyed by the person's repository too, so no image crosses people.
|
- **Files** (ADR 0004, rule 5): `AssetEndpoint` (`requireLogin`) takes uploads into the caller's own space by SHA-256 (`startUpload` gives Serverpod's upload description, or null when the file is there already and claims it anew; `finishUpload` keeps it only if its content has that hash) and serves an asset row's file to its space's owner and members only, `NotFoundException` otherwise, also for a deleted row (the sync engine hides tombstones). Files live in the private database storage at `spaces/<space>/<sha256>`, at most 20 MB each, and within limits (`file_limits.dart`, from the environment: an anonymous person 100 MB, an identified one 500 MB or what a `file_allowance` row grants them, the instance 15 GB), checked when an upload starts (unfinished uploads count, or they could fill the disk unnoticed) and again when it finishes (only what is kept counts), refused with `FileLimitExceededException`; `deletePerson` deletes the person's files and allowance, and `FilePruningFutureCall` deletes daily those no live row refers to (or never finished), 7 days after they were last claimed. In the app, `AssetRepository` (`core/asset/`) keeps files by hash in the person's folder (`files/`, uploads waiting marked in `files/uploads/`), uploads after each sync that went through, and downloads on demand, checking the hash; every hash is checked to be one before it becomes part of a path. Content names a file by the URL `asset:<asset row id>` (`assetUrl`, `RichContent.assetIds`): images are the editor's `image` block, sounds an `audio` block (`core/editor/file_blocks.dart`), and any other URL is never fetched. `StoredFileImage` loads through the repository like Flutter's `NetworkImage`, keyed by the person's repository too, so no image crosses people.
|
||||||
- **PDFs** (`feature/pdf_source/`): a `pdf_source` row per PDF in a studyset (payload: title, file name, the asset of its file, page count, reading deadline; last writer wins) with its `position` among the studyset's files in a column of its own (decided 2026-10-08; the Files tab's default order, Custom order, which Reorder changes; PDFs from before it stand at `a0`, in the order they were added) and an append-only `pdf_reading` log (`reached` with a page, `reset`): the furthest page is the furthest reached since the last reset, so devices never conflict. A PDF's file is an asset of up to 50 MB (other files 20 MB): the server takes up to 50 MB and deletes a stored file over 20 MB that doesn't start with `%PDF-`. Deleting a PDF deletes its asset row too unless another PDF has the same file (asset rows are otherwise never deleted). Cards and notes tied to a page keep it in their payload (`source`, `core/content/pdf_page_ref.dart`; card schema 4, note schema 2); a screenshot is an `image` block with `pdf` and `page` attributes.
|
- **PDFs** (`feature/pdf_source/`): a `pdf_source` row per PDF in a studyset (payload: title, file name, the asset of its file, page count, reading deadline; last writer wins) with its `position` among the studyset's files in a column of its own (decided 2026-10-08; the Files tab's default order, Custom order, which Reorder changes; PDFs from before it stand at `a0`, in the order they were added) and an append-only `pdf_reading` log (`reached` with a page, `reset`): the furthest page is the furthest reached since the last reset, so devices never conflict. A PDF's file is an asset of up to 50 MB (other files 20 MB): the server takes up to 50 MB and deletes a stored file over 20 MB that doesn't start with `%PDF-`. Deleting a PDF deletes its asset row too unless another PDF has the same file (asset rows are otherwise never deleted). Cards and notes tied to a page keep it in their payload (`source`, `core/content/pdf_page_ref.dart`; card schema 4, note schema 2); a screenshot is an `image` block with `pdf` and `page` attributes.
|
||||||
- **Dictionaries** (decided 2026-10-08): reference data the server holds for everyone, owned by no one, never synced (`dictionary_entry`, `dictionary_source`, `lib/src/dictionary/`). `halkyon_learn_curator` (AGPL, no Serverpod, so the server never links its code) turns each source's download into one format, a dictionary file (its README is the contract), with an adapter per source; everything specific to a language (kana, romaji, meanings without "to") goes into each entry's `keys` and `looseKeys`, so the server's search knows no language: exact key, exact loose key, key starting with the query, loose key starting with it, each by the source's `rank` (`DictionaryEndpoint.search`, LIKE on barred keys, no extension needed). The server loads the files in its `dictionaries` folder at start, those whose SHA-256 changed, each source replaced in one transaction (`dictionary_loader.dart`). A card made from an entry copies what it needs and keeps `(source, sourceId)` as a soft reference only (`CardContent.entry`, card schema 6), so loading a newer version never touches cards. Every file carries its credits, which the app shows (About, and at the foot of the dictionary). In the app, a studyset's dictionary follows from its front language (`StudysetDictionary`, through `StudysetLanguageScope.of`): no attaching or detaching, and nothing dictionary-shaped shows without one (Add material › From the dictionary; Look up beside a vocabulary card's front). A word's example sentences (Tatoeba, through JMdict's examples edition) become its card's hints, up to three, the word in bold; Look up gives them to a card without hints of its own. `DictionaryAdder` makes the cards, each entry once per studyset, tagged `JLPT-NX` and with its word class by the first sense (`Noun`, `Verb`, `na-Adjective`); Look up in the composer gives a card the same tags (`DictionaryWord.tagNames`), and a word looked up again on that card swaps the tags the word before brought.
|
- **Dictionaries** (decided 2026-10-08): reference data the server holds for everyone, owned by no one, never synced (`dictionary_entry`, `dictionary_source`, `lib/src/dictionary/`). `halkyon_learn_curator` (AGPL, no Serverpod, so the server never links its code) turns each source's download into one format, a dictionary file (its README is the contract), with an adapter per source; a dictionary is one source in one meaning language (`jmdict` in `en`), of words or of characters (`kind`: JMdict, KANJIDIC2 with the kana), and the endpoints take it by those two, so a search for words never brings kanji; everything specific to a language (kana, romaji, meanings without "to") goes into each entry's `keys` and `looseKeys`, so the server's search knows no language: exact key, exact loose key, key starting with the query, loose key starting with it, each by the source's `rank` (`DictionaryEndpoint.search`, LIKE on barred keys, no extension needed). The server loads the files in its `dictionaries` folder at start, those whose SHA-256 changed, each source replaced in one transaction (`dictionary_loader.dart`). A card made from an entry copies what it needs and keeps `(source, sourceId)` as a soft reference only (`CardContent.entry`, card schema 6), so loading a newer version never touches cards. Every file carries its credits, which the app shows (About, and at the foot of the dictionary). In the app, a studyset's dictionary follows from its front language (`StudysetDictionary`, through `StudysetLanguageScope.of`): no attaching or detaching, and nothing dictionary-shaped shows without one (Add material › From the dictionary; Look up beside a vocabulary card's front). A word's example sentences (Tatoeba, through JMdict's examples edition) become its card's hints, up to three, the word in bold; Look up gives them to a card without hints of its own. `DictionaryAdder` makes the cards, each entry once per studyset, tagged `JLPT-NX` and with its word class by the first sense (`Noun`, `Verb`, `na-Adjective`); Look up in the composer gives a card the same tags (`DictionaryWord.tagNames`), and a word looked up again on that card swaps the tags the word before brought.
|
||||||
- **The Pomodoro timer** (`feature/pomodoro/`): the profiles follow the person (`PersonSettings.pomodoro`, settings schema 4), the running timer is the device's own (`PomodoroRun` in `<person folder>/pomodoro.json`, `PomodoroRunStore`). `PomodoroRun` is a pure state machine on the time: a phase that ends leads to the next, running if its profile starts it by itself, else paused at its full length; a timer kept while the app was closed catches up (`caughtUpTo`). `PomodoroBloc` sits above the router (`PersonApp`), and `PomodoroPresenter` hands every change to `PomodoroPlatform` as a `PomodoroNotice` in the app's language, and its buttons back. **Android:** the app's own Kotlin (`PomodoroService.kt`; MethodChannel `app.halkyon.learn/pomodoro`, EventChannel `…/buttons`): a `specialUse` foreground service with a countdown notification (chronometer), Pause/Resume, Skip, Stop, on Android 16 a Live Update (`NotificationCompat.ProgressStyle`, the cycle's phases as segments, `setRequestPromotedOngoing`); an exact alarm (`USE_EXACT_ALARM`) alerts at the phase's end with the app closed; Do Not Disturb through the interruption filter, the one before kept in shared preferences and restored. With the app swiped away its buttons open it (a service may not start an activity from a notification's button since Android 12, so the notification is posted again with activity intents), and Stop works alone, leaving a mark the app takes at its next start (`takeStoppedOutside`) to forget its timer; a profile's next phase only starts by itself while the app runs. Dart's timers sleep with the device, so `PomodoroBloc` also looks every 15 s while a phase runs, and the alarm's receiver wakes it (`phaseEnds`); a kept timer is caught up before it first shows, or its ended phase would alert again. No notification plugin: flutter_local_notifications had Live Updates only in a prerelease needing compileSdk 37 (checked 2026-10-07). **Linux:** `LinuxPomodoroPlatform`, over package:dbus: where the top bar shows icons (GNOME with the AppIndicator extension, built into Ubuntu; KDE) the timer counts down there with its buttons in the icon's menu (`StatusNotifierItem`, written in Dart and laid out as libayatana-appindicator lays itself out, within the snap's rules, `desktop-legacy` of snapd v2.77: the item at `/org/ayatana/NotificationItem/<id>`, the only path the label's signal `XAyatanaNewLabel` may come from, registered with the watcher by that path, since a snap may bind no `org.freedesktop.StatusNotifierItem-*` name; the menu at `/MenuBar`; registered again when the watcher restarts); elsewhere the timer shows in the app alone, and on GNOME the Pomodoro card suggests the extension until dismissed, asking GNOME Shell to install or switch it on (`InstallRemoteExtension`, `EnableExtension`) or opening its page (decided 2026-10-07: no notification stands in; xdg-desktop-portal's Background status is for Flatpaks only). An alert with sound comes through `org.freedesktop.Notifications` when a phase ends while the app runs, and GNOME's `show-banners` goes off while focusing, through `gsettings`; the value before and the dismissed suggestion are kept in `<app support>/`. Calls to the bus go one at a time (`SerialQueue`).
|
- **The Pomodoro timer** (`feature/pomodoro/`): the profiles follow the person (`PersonSettings.pomodoro`, settings schema 4), the running timer is the device's own (`PomodoroRun` in `<person folder>/pomodoro.json`, `PomodoroRunStore`). `PomodoroRun` is a pure state machine on the time: a phase that ends leads to the next, running if its profile starts it by itself, else paused at its full length; a timer kept while the app was closed catches up (`caughtUpTo`). `PomodoroBloc` sits above the router (`PersonApp`), and `PomodoroPresenter` hands every change to `PomodoroPlatform` as a `PomodoroNotice` in the app's language, and its buttons back. **Android:** the app's own Kotlin (`PomodoroService.kt`; MethodChannel `app.halkyon.learn/pomodoro`, EventChannel `…/buttons`): a `specialUse` foreground service with a countdown notification (chronometer), Pause/Resume, Skip, Stop, on Android 16 a Live Update (`NotificationCompat.ProgressStyle`, the cycle's phases as segments, `setRequestPromotedOngoing`); an exact alarm (`USE_EXACT_ALARM`) alerts at the phase's end with the app closed; Do Not Disturb through the interruption filter, the one before kept in shared preferences and restored. With the app swiped away its buttons open it (a service may not start an activity from a notification's button since Android 12, so the notification is posted again with activity intents), and Stop works alone, leaving a mark the app takes at its next start (`takeStoppedOutside`) to forget its timer; a profile's next phase only starts by itself while the app runs. Dart's timers sleep with the device, so `PomodoroBloc` also looks every 15 s while a phase runs, and the alarm's receiver wakes it (`phaseEnds`); a kept timer is caught up before it first shows, or its ended phase would alert again. No notification plugin: flutter_local_notifications had Live Updates only in a prerelease needing compileSdk 37 (checked 2026-10-07). **Linux:** `LinuxPomodoroPlatform`, over package:dbus: where the top bar shows icons (GNOME with the AppIndicator extension, built into Ubuntu; KDE) the timer counts down there with its buttons in the icon's menu (`StatusNotifierItem`, written in Dart and laid out as libayatana-appindicator lays itself out, within the snap's rules, `desktop-legacy` of snapd v2.77: the item at `/org/ayatana/NotificationItem/<id>`, the only path the label's signal `XAyatanaNewLabel` may come from, registered with the watcher by that path, since a snap may bind no `org.freedesktop.StatusNotifierItem-*` name; the menu at `/MenuBar`; registered again when the watcher restarts); elsewhere the timer shows in the app alone, and on GNOME the Pomodoro card suggests the extension until dismissed, asking GNOME Shell to install or switch it on (`InstallRemoteExtension`, `EnableExtension`) or opening its page (decided 2026-10-07: no notification stands in; xdg-desktop-portal's Background status is for Flatpaks only). An alert with sound comes through `org.freedesktop.Notifications` when a phase ends while the app runs, and GNOME's `show-banners` goes off while focusing, through `gsettings`; the value before and the dismissed suggestion are kept in `<app support>/`. Calls to the bus go one at a time (`SerialQueue`).
|
||||||
- **Citations** (`core/citation/`): a `Source` is plain text parts as the person typed them (authors as full names, one per line; year; title; `container`, what it is a part of; publisher; ISBN; link; `raw`, a citation a file brought, shown while nothing else is cited), and `formatCitation` cites it in the device's `CitationStyle` (APA 7, MLA 9, Chicago 18 notes and bibliography, decided 2026-10-07): it splits and abbreviates the names itself (inverted names, particles, suffixes; names written with `&` or "et al." stay as written), sets a work of its own in italics and a part of something apart from the whole by `container`, and writes a missing year as "n.d." in APA and Chicago. A step's sources (step schema 2) and a studyset's bibliography (`bibliography`, studyset schema 4, last writer wins like the rest of its payload) are lists of them, shown and changed through `SourceList` and `showSourcesSheet`, in reference-list order (alphabetical, whatever order they were added in); the path import reads them too, text as `raw` and the old format's `citation` as `container`.
|
- **Citations** (`core/citation/`): a `Source` is plain text parts as the person typed them (authors as full names, one per line; year; title; `container`, what it is a part of; publisher; ISBN; link; `raw`, a citation a file brought, shown while nothing else is cited), and `formatCitation` cites it in the device's `CitationStyle` (APA 7, MLA 9, Chicago 18 notes and bibliography, decided 2026-10-07): it splits and abbreviates the names itself (inverted names, particles, suffixes; names written with `&` or "et al." stay as written), sets a work of its own in italics and a part of something apart from the whole by `container`, and writes a missing year as "n.d." in APA and Chicago. A step's sources (step schema 2) and a studyset's bibliography (`bibliography`, studyset schema 4, last writer wins like the rest of its payload) are lists of them, shown and changed through `SourceList` and `showSourcesSheet`, in reference-list order (alphabetical, whatever order they were added in); the path import reads them too, text as `raw` and the old format's `citation` as `container`.
|
||||||
- **What a card carries beside its type's fields** (card schema 5): up to six `hints` and an `explanation` (rich text, any type), a `report` that something on it is wrong (category, note, when; studied as before, its badge in the list), and a word's `senses` (glosses, label, JMdict's grammar codes, whose English short forms and meanings are data in `feature/card/model/jmdict_tags.dart`; with senses a word's meanings are their glosses, and `uk` on its first sense shows its reading from the start). Each merges as a field of its own. Deleting a card goes through `CardDeletions` (a `DelayedDeletions`, as notes and studysets), so the list and the session offer Undo. A study session's Undo takes back its steps last first (ratings, burials and deletions alike, across its rounds), so it holds the cards it deletes (`hold`) until it ends, when they go after the usual time to undo; a card deleted in a session that never ended (the app killed) stays. Vocabulary can be quizzed as multiple choice (`quizVocabulary`, `quizAspect` in the study options, settings schema 5; `vocabularyQuiz` draws the wrong answers from the other words studied).
|
- **What a card carries beside its type's fields** (card schema 5): up to six `hints` and an `explanation` (rich text, any type), a `report` that something on it is wrong (category, note, when; studied as before, its badge in the list), and a word's `senses` (glosses, label, JMdict's grammar codes, whose English short forms and meanings are data in `feature/card/model/jmdict_tags.dart`; with senses a word's meanings are their glosses, and `uk` on its first sense shows its reading from the start). Each merges as a field of its own. Deleting a card goes through `CardDeletions` (a `DelayedDeletions`, as notes and studysets), so the list and the session offer Undo. A study session's Undo takes back its steps last first (ratings, burials and deletions alike, across its rounds), so it holds the cards it deletes (`hold`) until it ends, when they go after the usual time to undo; a card deleted in a session that never ended (the app killed) stays. Vocabulary can be quizzed as multiple choice (`quizVocabulary`, `quizAspect` in the study options, settings schema 5; `vocabularyQuiz` draws the wrong answers from the other words studied).
|
||||||
|
|
|
||||||
|
|
@ -225,8 +225,9 @@ class EndpointGitHubUpgrade extends _isc.EndpointRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The dictionaries the instance holds, to look words up in and to take
|
/// The dictionaries the instance holds, to look words up in and to take
|
||||||
/// words from by a label (all JLPT N5 words). Reference data: any signed-in
|
/// words from by a label (all JLPT N5 words). A dictionary is one source
|
||||||
/// person may read all of it.
|
/// in one meaning language (`jmdict` in `en`). Reference data: any
|
||||||
|
/// signed-in person may read all of it.
|
||||||
/// {@category Endpoint}
|
/// {@category Endpoint}
|
||||||
class EndpointDictionary extends _isc.EndpointRef {
|
class EndpointDictionary extends _isc.EndpointRef {
|
||||||
EndpointDictionary(_isc.EndpointCaller caller) : super(caller);
|
EndpointDictionary(_isc.EndpointCaller caller) : super(caller);
|
||||||
|
|
@ -242,13 +243,14 @@ class EndpointDictionary extends _isc.EndpointRef {
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The entries of the dictionary of [language] with meanings in
|
/// The entries of the dictionary of [source] with meanings in
|
||||||
/// [glossLanguage] that [query] finds, best first: those with a key that
|
/// [glossLanguage] that [query] finds, best first: those with a key that
|
||||||
/// is the query, then a loose key that is (`sushi` for すうし), then a key
|
/// is the query, then a loose key that is (`sushi` for すうし), then a key
|
||||||
/// starting with it, then a loose key; each in the dictionary's order. At
|
/// starting with it, then a loose key; each in the dictionary's order. At
|
||||||
/// most [limit] (30 unless given).
|
/// most [limit] (30 unless given). None for a dictionary the instance
|
||||||
|
/// doesn't hold.
|
||||||
_ida.Future<List<_iqbqiphu.DictionaryHit>> search(
|
_ida.Future<List<_iqbqiphu.DictionaryHit>> search(
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String query, {
|
String query, {
|
||||||
int? limit,
|
int? limit,
|
||||||
|
|
@ -256,24 +258,24 @@ class EndpointDictionary extends _isc.EndpointRef {
|
||||||
'dictionary',
|
'dictionary',
|
||||||
'search',
|
'search',
|
||||||
{
|
{
|
||||||
'language': language,
|
'source': source,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'query': query,
|
'query': query,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// How many entries of the dictionary of [language] with meanings in
|
/// How many entries of the dictionary of [source] with meanings in
|
||||||
/// [glossLanguage] carry [label].
|
/// [glossLanguage] carry [label].
|
||||||
_ida.Future<int> countLabelled(
|
_ida.Future<int> countLabelled(
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String label,
|
String label,
|
||||||
) => caller.callServerEndpoint<int>(
|
) => caller.callServerEndpoint<int>(
|
||||||
'dictionary',
|
'dictionary',
|
||||||
'countLabelled',
|
'countLabelled',
|
||||||
{
|
{
|
||||||
'language': language,
|
'source': source,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'label': label,
|
'label': label,
|
||||||
},
|
},
|
||||||
|
|
@ -282,7 +284,7 @@ class EndpointDictionary extends _isc.EndpointRef {
|
||||||
/// The entries that carry [label], in the dictionary's order: [limit] of
|
/// The entries that carry [label], in the dictionary's order: [limit] of
|
||||||
/// them (100 unless given, at most [maxPage]) after the first [offset].
|
/// them (100 unless given, at most [maxPage]) after the first [offset].
|
||||||
_ida.Future<List<_iqbqiphu.DictionaryHit>> labelled(
|
_ida.Future<List<_iqbqiphu.DictionaryHit>> labelled(
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String label, {
|
String label, {
|
||||||
int? offset,
|
int? offset,
|
||||||
|
|
@ -291,7 +293,7 @@ class EndpointDictionary extends _isc.EndpointRef {
|
||||||
'dictionary',
|
'dictionary',
|
||||||
'labelled',
|
'labelled',
|
||||||
{
|
{
|
||||||
'language': language,
|
'source': source,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'label': label,
|
'label': label,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ abstract class DictionarySource
|
||||||
this.id,
|
this.id,
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.sourceVersion,
|
required this.sourceVersion,
|
||||||
|
String? kind,
|
||||||
required this.language,
|
required this.language,
|
||||||
required this.glossLanguage,
|
required this.glossLanguage,
|
||||||
required this.license,
|
required this.license,
|
||||||
|
|
@ -31,12 +32,13 @@ abstract class DictionarySource
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.fileHash,
|
required this.fileHash,
|
||||||
required this.loadedAt,
|
required this.loadedAt,
|
||||||
});
|
}) : kind = kind ?? 'words';
|
||||||
|
|
||||||
factory DictionarySource({
|
factory DictionarySource({
|
||||||
int? id,
|
int? id,
|
||||||
required String source,
|
required String source,
|
||||||
required String sourceVersion,
|
required String sourceVersion,
|
||||||
|
String? kind,
|
||||||
required String language,
|
required String language,
|
||||||
required String glossLanguage,
|
required String glossLanguage,
|
||||||
required String license,
|
required String license,
|
||||||
|
|
@ -52,6 +54,7 @@ abstract class DictionarySource
|
||||||
id: jsonSerialization['id'] as int?,
|
id: jsonSerialization['id'] as int?,
|
||||||
source: jsonSerialization['source'] as String,
|
source: jsonSerialization['source'] as String,
|
||||||
sourceVersion: jsonSerialization['sourceVersion'] as String,
|
sourceVersion: jsonSerialization['sourceVersion'] as String,
|
||||||
|
kind: jsonSerialization['kind'] as String?,
|
||||||
language: jsonSerialization['language'] as String,
|
language: jsonSerialization['language'] as String,
|
||||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||||
license: jsonSerialization['license'] as String,
|
license: jsonSerialization['license'] as String,
|
||||||
|
|
@ -79,6 +82,11 @@ abstract class DictionarySource
|
||||||
/// Its version (`3.6.2 (2026-10-05)`).
|
/// Its version (`3.6.2 (2026-10-05)`).
|
||||||
String sourceVersion;
|
String sourceVersion;
|
||||||
|
|
||||||
|
/// What it holds: `words` (JMdict) or `characters` (KANJIDIC2). A string
|
||||||
|
/// rather than an enum, so an app older than a new kind can leave out
|
||||||
|
/// what it doesn't know.
|
||||||
|
String kind;
|
||||||
|
|
||||||
/// The words' language (`ja`).
|
/// The words' language (`ja`).
|
||||||
String language;
|
String language;
|
||||||
|
|
||||||
|
|
@ -109,6 +117,7 @@ abstract class DictionarySource
|
||||||
int? id,
|
int? id,
|
||||||
String? source,
|
String? source,
|
||||||
String? sourceVersion,
|
String? sourceVersion,
|
||||||
|
String? kind,
|
||||||
String? language,
|
String? language,
|
||||||
String? glossLanguage,
|
String? glossLanguage,
|
||||||
String? license,
|
String? license,
|
||||||
|
|
@ -125,6 +134,7 @@ abstract class DictionarySource
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'source': source,
|
'source': source,
|
||||||
'sourceVersion': sourceVersion,
|
'sourceVersion': sourceVersion,
|
||||||
|
'kind': kind,
|
||||||
'language': language,
|
'language': language,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'license': license,
|
'license': license,
|
||||||
|
|
@ -143,6 +153,7 @@ abstract class DictionarySource
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'source': source,
|
'source': source,
|
||||||
'sourceVersion': sourceVersion,
|
'sourceVersion': sourceVersion,
|
||||||
|
'kind': kind,
|
||||||
'language': language,
|
'language': language,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'license': license,
|
'license': license,
|
||||||
|
|
@ -167,6 +178,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
int? id,
|
int? id,
|
||||||
required String source,
|
required String source,
|
||||||
required String sourceVersion,
|
required String sourceVersion,
|
||||||
|
String? kind,
|
||||||
required String language,
|
required String language,
|
||||||
required String glossLanguage,
|
required String glossLanguage,
|
||||||
required String license,
|
required String license,
|
||||||
|
|
@ -179,6 +191,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
id: id,
|
id: id,
|
||||||
source: source,
|
source: source,
|
||||||
sourceVersion: sourceVersion,
|
sourceVersion: sourceVersion,
|
||||||
|
kind: kind,
|
||||||
language: language,
|
language: language,
|
||||||
glossLanguage: glossLanguage,
|
glossLanguage: glossLanguage,
|
||||||
license: license,
|
license: license,
|
||||||
|
|
@ -197,6 +210,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
Object? id = _Undefined,
|
Object? id = _Undefined,
|
||||||
String? source,
|
String? source,
|
||||||
String? sourceVersion,
|
String? sourceVersion,
|
||||||
|
String? kind,
|
||||||
String? language,
|
String? language,
|
||||||
String? glossLanguage,
|
String? glossLanguage,
|
||||||
String? license,
|
String? license,
|
||||||
|
|
@ -210,6 +224,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
id: id is int? ? id : this.id,
|
id: id is int? ? id : this.id,
|
||||||
source: source ?? this.source,
|
source: source ?? this.source,
|
||||||
sourceVersion: sourceVersion ?? this.sourceVersion,
|
sourceVersion: sourceVersion ?? this.sourceVersion,
|
||||||
|
kind: kind ?? this.kind,
|
||||||
language: language ?? this.language,
|
language: language ?? this.language,
|
||||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||||
license: license ?? this.license,
|
license: license ?? this.license,
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,21 @@ class DictionaryRepository(final DictionaryGateway _gateway) {
|
||||||
Future<List<Dictionary>> dictionaries() async =>
|
Future<List<Dictionary>> dictionaries() async =>
|
||||||
_known ??= await _gateway.dictionaries();
|
_known ??= await _gateway.dictionaries();
|
||||||
|
|
||||||
/// The dictionary for a studyset in [languages]: of its front's
|
/// The dictionary of [kind] (words unless given) for a studyset in
|
||||||
/// language, with meanings in its back's, else in English, else in any;
|
/// [languages]: of its front's language, with meanings in its back's,
|
||||||
/// null when there is none, or no front language.
|
/// else in English, else in any; null when there is none, or no front
|
||||||
Future<Dictionary?> dictionaryFor(StudysetLanguages languages) async {
|
/// language.
|
||||||
|
Future<Dictionary?> dictionaryFor(
|
||||||
|
StudysetLanguages languages, {
|
||||||
|
DictionaryKind kind = DictionaryKind.words,
|
||||||
|
}) async {
|
||||||
final front = languages.front;
|
final front = languages.front;
|
||||||
if (front == null) {
|
if (front == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final ofLanguage = [
|
final ofLanguage = [
|
||||||
for (final dictionary in await dictionaries())
|
for (final dictionary in await dictionaries())
|
||||||
if (dictionary.language == front) dictionary,
|
if (dictionary.language == front && dictionary.kind == kind) dictionary,
|
||||||
];
|
];
|
||||||
return ofLanguage
|
return ofLanguage
|
||||||
.where((dictionary) => dictionary.glossLanguage == languages.back)
|
.where((dictionary) => dictionary.glossLanguage == languages.back)
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,30 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
||||||
/// [DictionaryGateway] asking the instance through the Serverpod client.
|
/// [DictionaryGateway] asking the instance through the Serverpod client.
|
||||||
class ServerpodDictionaryGateway(final protocol.Client _client)
|
class ServerpodDictionaryGateway(final protocol.Client _client)
|
||||||
implements DictionaryGateway {
|
implements DictionaryGateway {
|
||||||
|
/// The dictionaries, but those of a kind this app doesn't know (a newer
|
||||||
|
/// instance's).
|
||||||
@override
|
@override
|
||||||
Future<List<Dictionary>> dictionaries() async => [
|
Future<List<Dictionary>> dictionaries() async => [
|
||||||
for (final source in await _client.dictionary.dictionaries())
|
for (final source in await _client.dictionary.dictionaries())
|
||||||
Dictionary(
|
if (DictionaryKind.values.asNameMap()[source.kind] case final kind?)
|
||||||
source: source.source,
|
Dictionary(
|
||||||
sourceVersion: source.sourceVersion,
|
source: source.source,
|
||||||
language: source.language,
|
sourceVersion: source.sourceVersion,
|
||||||
glossLanguage: source.glossLanguage,
|
kind: kind,
|
||||||
license: source.license,
|
language: source.language,
|
||||||
credits: [
|
glossLanguage: source.glossLanguage,
|
||||||
for (final credit in source.credits)
|
license: source.license,
|
||||||
(
|
credits: [
|
||||||
name: credit.name,
|
for (final credit in source.credits)
|
||||||
by: credit.by,
|
(
|
||||||
license: credit.license,
|
name: credit.name,
|
||||||
url: credit.url,
|
by: credit.by,
|
||||||
),
|
license: credit.license,
|
||||||
],
|
url: credit.url,
|
||||||
entryCount: source.entryCount,
|
),
|
||||||
),
|
],
|
||||||
|
entryCount: source.entryCount,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -33,7 +37,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
||||||
String query,
|
String query,
|
||||||
) async => [
|
) async => [
|
||||||
for (final hit in await _client.dictionary.search(
|
for (final hit in await _client.dictionary.search(
|
||||||
dictionary.language,
|
dictionary.source,
|
||||||
dictionary.glossLanguage,
|
dictionary.glossLanguage,
|
||||||
query,
|
query,
|
||||||
))
|
))
|
||||||
|
|
@ -43,7 +47,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
||||||
@override
|
@override
|
||||||
Future<int> countLabelled(Dictionary dictionary, String label) =>
|
Future<int> countLabelled(Dictionary dictionary, String label) =>
|
||||||
_client.dictionary.countLabelled(
|
_client.dictionary.countLabelled(
|
||||||
dictionary.language,
|
dictionary.source,
|
||||||
dictionary.glossLanguage,
|
dictionary.glossLanguage,
|
||||||
label,
|
label,
|
||||||
);
|
);
|
||||||
|
|
@ -56,7 +60,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
||||||
required int limit,
|
required int limit,
|
||||||
}) async => [
|
}) async => [
|
||||||
for (final hit in await _client.dictionary.labelled(
|
for (final hit in await _client.dictionary.labelled(
|
||||||
dictionary.language,
|
dictionary.source,
|
||||||
dictionary.glossLanguage,
|
dictionary.glossLanguage,
|
||||||
label,
|
label,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,22 @@ typedef DictionaryCredit = ({
|
||||||
String url,
|
String url,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// A dictionary the instance holds: words of [language] with meanings in
|
/// What a dictionary holds.
|
||||||
/// [glossLanguage] (BCP 47 tags), from [source] at [sourceVersion].
|
enum DictionaryKind() {
|
||||||
|
/// Words (JMdict).
|
||||||
|
words,
|
||||||
|
|
||||||
|
/// The characters words are written in (KANJIDIC2, with the kana).
|
||||||
|
characters,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dictionary the instance holds: words or characters ([kind]) of
|
||||||
|
/// [language] with meanings in [glossLanguage] (BCP 47 tags), from [source]
|
||||||
|
/// at [sourceVersion].
|
||||||
final class const Dictionary({
|
final class const Dictionary({
|
||||||
required final String source,
|
required final String source,
|
||||||
required final String sourceVersion,
|
required final String sourceVersion,
|
||||||
|
required final DictionaryKind kind,
|
||||||
required final String language,
|
required final String language,
|
||||||
required final String glossLanguage,
|
required final String glossLanguage,
|
||||||
required final String license,
|
required final String license,
|
||||||
|
|
@ -28,6 +39,7 @@ final class const Dictionary({
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
source,
|
source,
|
||||||
sourceVersion,
|
sourceVersion,
|
||||||
|
kind,
|
||||||
language,
|
language,
|
||||||
glossLanguage,
|
glossLanguage,
|
||||||
license,
|
license,
|
||||||
|
|
|
||||||
|
|
@ -102,10 +102,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("finds a studyset's dictionary by its front's language, with "
|
test("finds a studyset's dictionary by its front's language, with "
|
||||||
"meanings in its back's, else in English", () async {
|
"meanings in its back's, else in English; one of words unless a "
|
||||||
|
'kind is asked for', () async {
|
||||||
const german = Dictionary(
|
const german = Dictionary(
|
||||||
source: 'jmdict',
|
source: 'jmdict',
|
||||||
sourceVersion: '1',
|
sourceVersion: '1',
|
||||||
|
kind: DictionaryKind.words,
|
||||||
language: 'ja',
|
language: 'ja',
|
||||||
glossLanguage: 'de',
|
glossLanguage: 'de',
|
||||||
license: 'CC-BY-SA-4.0',
|
license: 'CC-BY-SA-4.0',
|
||||||
|
|
@ -113,7 +115,7 @@ void main() {
|
||||||
entryCount: 1,
|
entryCount: 1,
|
||||||
);
|
);
|
||||||
final repository = DictionaryRepository(
|
final repository = DictionaryRepository(
|
||||||
FakeDictionaryGateway(available: [jmdict, german]),
|
FakeDictionaryGateway(available: [kanjidic, jmdict, german]),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
|
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
|
||||||
|
|
@ -121,6 +123,13 @@ void main() {
|
||||||
expect(await repository.dictionaryFor((front: 'ja', back: null)), jmdict);
|
expect(await repository.dictionaryFor((front: 'ja', back: null)), jmdict);
|
||||||
expect(await repository.dictionaryFor((front: 'ko', back: 'en')), isNull);
|
expect(await repository.dictionaryFor((front: 'ko', back: 'en')), isNull);
|
||||||
expect(await repository.dictionaryFor((front: null, back: 'en')), isNull);
|
expect(await repository.dictionaryFor((front: null, back: 'en')), isNull);
|
||||||
|
expect(
|
||||||
|
await repository.dictionaryFor((
|
||||||
|
front: 'ja',
|
||||||
|
back: 'en',
|
||||||
|
), kind: DictionaryKind.characters),
|
||||||
|
kanjidic,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('brings every word of a level, a page at a time', () async {
|
test('brings every word of a level, a page at a time', () async {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
||||||
const jmdict = Dictionary(
|
const jmdict = Dictionary(
|
||||||
source: 'jmdict',
|
source: 'jmdict',
|
||||||
sourceVersion: '3.6.2 (2026-10-05)',
|
sourceVersion: '3.6.2 (2026-10-05)',
|
||||||
|
kind: DictionaryKind.words,
|
||||||
language: 'ja',
|
language: 'ja',
|
||||||
glossLanguage: 'en',
|
glossLanguage: 'en',
|
||||||
license: 'CC-BY-SA-4.0',
|
license: 'CC-BY-SA-4.0',
|
||||||
|
|
@ -20,6 +21,26 @@ const jmdict = Dictionary(
|
||||||
entryCount: 3,
|
entryCount: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// KANJIDIC2 in English, with the kana, as an instance that loaded it lists
|
||||||
|
/// it.
|
||||||
|
const kanjidic = Dictionary(
|
||||||
|
source: 'kanjidic',
|
||||||
|
sourceVersion: '2026-282',
|
||||||
|
kind: DictionaryKind.characters,
|
||||||
|
language: 'ja',
|
||||||
|
glossLanguage: 'en',
|
||||||
|
license: 'CC-BY-SA-4.0',
|
||||||
|
credits: [
|
||||||
|
(
|
||||||
|
name: 'KANJIDIC2',
|
||||||
|
by: 'Electronic Dictionary Research and Development Group (EDRDG)',
|
||||||
|
license: 'CC-BY-SA-4.0',
|
||||||
|
url: 'https://www.edrdg.org/edrdg/licence.html',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
entryCount: 2,
|
||||||
|
);
|
||||||
|
|
||||||
/// A JMdict word: [headword] read [reading], meaning [glosses], [labels]
|
/// A JMdict word: [headword] read [reading], meaning [glosses], [labels]
|
||||||
/// and the parts of speech [pos] on its one sense.
|
/// and the parts of speech [pos] on its one sense.
|
||||||
DictionaryWord word(
|
DictionaryWord word(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
|
|
||||||
/// The dictionaries the instance holds, to look words up in and to take
|
/// The dictionaries the instance holds, to look words up in and to take
|
||||||
/// words from by a label (all JLPT N5 words). Reference data: any signed-in
|
/// words from by a label (all JLPT N5 words). A dictionary is one source
|
||||||
/// person may read all of it.
|
/// in one meaning language (`jmdict` in `en`). Reference data: any
|
||||||
|
/// signed-in person may read all of it.
|
||||||
class DictionaryEndpoint extends Endpoint {
|
class DictionaryEndpoint extends Endpoint {
|
||||||
@override
|
@override
|
||||||
bool get requireLogin => true;
|
bool get requireLogin => true;
|
||||||
|
|
@ -22,19 +23,28 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
orderBy: (t) => t.language,
|
orderBy: (t) => t.language,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The entries of the dictionary of [language] with meanings in
|
/// The entries of the dictionary of [source] with meanings in
|
||||||
/// [glossLanguage] that [query] finds, best first: those with a key that
|
/// [glossLanguage] that [query] finds, best first: those with a key that
|
||||||
/// is the query, then a loose key that is (`sushi` for すうし), then a key
|
/// is the query, then a loose key that is (`sushi` for すうし), then a key
|
||||||
/// starting with it, then a loose key; each in the dictionary's order. At
|
/// starting with it, then a loose key; each in the dictionary's order. At
|
||||||
/// most [limit] (30 unless given).
|
/// most [limit] (30 unless given). None for a dictionary the instance
|
||||||
|
/// doesn't hold.
|
||||||
Future<List<DictionaryHit>> search(
|
Future<List<DictionaryHit>> search(
|
||||||
Session session,
|
Session session,
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String query, {
|
String query, {
|
||||||
int? limit,
|
int? limit,
|
||||||
}) async {
|
}) async {
|
||||||
final variants = queryVariants(language, query);
|
final dictionary = await DictionarySource.db.findFirstRow(
|
||||||
|
session,
|
||||||
|
where: (t) =>
|
||||||
|
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||||
|
);
|
||||||
|
if (dictionary == null) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
final variants = queryVariants(dictionary.language, query);
|
||||||
if (variants.isEmpty) {
|
if (variants.isEmpty) {
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +58,7 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
].join(' OR ');
|
].join(' OR ');
|
||||||
final rows = await session.db.unsafeQuery(
|
final rows = await session.db.unsafeQuery(
|
||||||
'SELECT "id" FROM "dictionary_entry" '
|
'SELECT "id" FROM "dictionary_entry" '
|
||||||
'WHERE "language" = @language AND "glossLanguage" = @glossLanguage '
|
'WHERE "source" = @source AND "glossLanguage" = @glossLanguage '
|
||||||
'AND (${any('searchKeys', 'starts')} OR ${any('looseKeys', 'starts')}) '
|
'AND (${any('searchKeys', 'starts')} OR ${any('looseKeys', 'starts')}) '
|
||||||
'ORDER BY CASE '
|
'ORDER BY CASE '
|
||||||
'WHEN ${any('searchKeys', 'whole')} THEN 0 '
|
'WHEN ${any('searchKeys', 'whole')} THEN 0 '
|
||||||
|
|
@ -57,7 +67,7 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
'ELSE 3 END, "rank", "id" '
|
'ELSE 3 END, "rank", "id" '
|
||||||
'LIMIT @limit',
|
'LIMIT @limit',
|
||||||
parameters: QueryParameters.named({
|
parameters: QueryParameters.named({
|
||||||
'language': language,
|
'source': source,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'limit': count,
|
'limit': count,
|
||||||
for (final (i, variant) in variants.indexed) ...{
|
for (final (i, variant) in variants.indexed) ...{
|
||||||
|
|
@ -82,17 +92,17 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many entries of the dictionary of [language] with meanings in
|
/// How many entries of the dictionary of [source] with meanings in
|
||||||
/// [glossLanguage] carry [label].
|
/// [glossLanguage] carry [label].
|
||||||
Future<int> countLabelled(
|
Future<int> countLabelled(
|
||||||
Session session,
|
Session session,
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String label,
|
String label,
|
||||||
) => DictionaryEntry.db.count(
|
) => DictionaryEntry.db.count(
|
||||||
session,
|
session,
|
||||||
where: (t) =>
|
where: (t) =>
|
||||||
t.language.equals(language) &
|
t.source.equals(source) &
|
||||||
t.glossLanguage.equals(glossLanguage) &
|
t.glossLanguage.equals(glossLanguage) &
|
||||||
t.labels.like('%|${likeEscaped(label)}|%'),
|
t.labels.like('%|${likeEscaped(label)}|%'),
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +111,7 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
/// them (100 unless given, at most [maxPage]) after the first [offset].
|
/// them (100 unless given, at most [maxPage]) after the first [offset].
|
||||||
Future<List<DictionaryHit>> labelled(
|
Future<List<DictionaryHit>> labelled(
|
||||||
Session session,
|
Session session,
|
||||||
String language,
|
String source,
|
||||||
String glossLanguage,
|
String glossLanguage,
|
||||||
String label, {
|
String label, {
|
||||||
int? offset,
|
int? offset,
|
||||||
|
|
@ -110,7 +120,7 @@ class DictionaryEndpoint extends Endpoint {
|
||||||
for (final entry in await DictionaryEntry.db.find(
|
for (final entry in await DictionaryEntry.db.find(
|
||||||
session,
|
session,
|
||||||
where: (t) =>
|
where: (t) =>
|
||||||
t.language.equals(language) &
|
t.source.equals(source) &
|
||||||
t.glossLanguage.equals(glossLanguage) &
|
t.glossLanguage.equals(glossLanguage) &
|
||||||
t.labels.like('%|${likeEscaped(label)}|%'),
|
t.labels.like('%|${likeEscaped(label)}|%'),
|
||||||
// Many share a rank (every other common word): the id breaks the tie,
|
// Many share a rank (every other common word): the id breaks the tie,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,6 @@ indexes:
|
||||||
dictionary_entry_source_idx:
|
dictionary_entry_source_idx:
|
||||||
fields: source, glossLanguage, sourceId
|
fields: source, glossLanguage, sourceId
|
||||||
unique: true
|
unique: true
|
||||||
# A language's entries in their order, for what is picked by a label.
|
# A dictionary's entries in their order, for what is picked by a label.
|
||||||
dictionary_entry_rank_idx:
|
dictionary_entry_rank_idx:
|
||||||
fields: language, glossLanguage, rank
|
fields: source, glossLanguage, rank
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,8 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
||||||
id: loaded?.id,
|
id: loaded?.id,
|
||||||
source: source,
|
source: source,
|
||||||
sourceVersion: header['sourceVersion']! as String,
|
sourceVersion: header['sourceVersion']! as String,
|
||||||
|
// Files written before kinds were all of words.
|
||||||
|
kind: header['kind'] as String? ?? 'words',
|
||||||
language: language,
|
language: language,
|
||||||
glossLanguage: glossLanguage,
|
glossLanguage: glossLanguage,
|
||||||
license: header['license']! as String,
|
license: header['license']! as String,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ fields:
|
||||||
source: String
|
source: String
|
||||||
### Its version (`3.6.2 (2026-10-05)`).
|
### Its version (`3.6.2 (2026-10-05)`).
|
||||||
sourceVersion: String
|
sourceVersion: String
|
||||||
|
### What it holds: `words` (JMdict) or `characters` (KANJIDIC2). A string
|
||||||
|
### rather than an enum, so an app older than a new kind can leave out
|
||||||
|
### what it doesn't know.
|
||||||
|
kind: String, default='words'
|
||||||
### The words' language (`ja`).
|
### The words' language (`ja`).
|
||||||
language: String
|
language: String
|
||||||
### The meanings' language (`en`).
|
### The meanings' language (`en`).
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ abstract class DictionarySource
|
||||||
this.id,
|
this.id,
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.sourceVersion,
|
required this.sourceVersion,
|
||||||
|
String? kind,
|
||||||
required this.language,
|
required this.language,
|
||||||
required this.glossLanguage,
|
required this.glossLanguage,
|
||||||
required this.license,
|
required this.license,
|
||||||
|
|
@ -31,12 +32,13 @@ abstract class DictionarySource
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.fileHash,
|
required this.fileHash,
|
||||||
required this.loadedAt,
|
required this.loadedAt,
|
||||||
});
|
}) : kind = kind ?? 'words';
|
||||||
|
|
||||||
factory DictionarySource({
|
factory DictionarySource({
|
||||||
int? id,
|
int? id,
|
||||||
required String source,
|
required String source,
|
||||||
required String sourceVersion,
|
required String sourceVersion,
|
||||||
|
String? kind,
|
||||||
required String language,
|
required String language,
|
||||||
required String glossLanguage,
|
required String glossLanguage,
|
||||||
required String license,
|
required String license,
|
||||||
|
|
@ -52,6 +54,7 @@ abstract class DictionarySource
|
||||||
id: jsonSerialization['id'] as int?,
|
id: jsonSerialization['id'] as int?,
|
||||||
source: jsonSerialization['source'] as String,
|
source: jsonSerialization['source'] as String,
|
||||||
sourceVersion: jsonSerialization['sourceVersion'] as String,
|
sourceVersion: jsonSerialization['sourceVersion'] as String,
|
||||||
|
kind: jsonSerialization['kind'] as String?,
|
||||||
language: jsonSerialization['language'] as String,
|
language: jsonSerialization['language'] as String,
|
||||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||||
license: jsonSerialization['license'] as String,
|
license: jsonSerialization['license'] as String,
|
||||||
|
|
@ -81,6 +84,11 @@ abstract class DictionarySource
|
||||||
/// Its version (`3.6.2 (2026-10-05)`).
|
/// Its version (`3.6.2 (2026-10-05)`).
|
||||||
String sourceVersion;
|
String sourceVersion;
|
||||||
|
|
||||||
|
/// What it holds: `words` (JMdict) or `characters` (KANJIDIC2). A string
|
||||||
|
/// rather than an enum, so an app older than a new kind can leave out
|
||||||
|
/// what it doesn't know.
|
||||||
|
String kind;
|
||||||
|
|
||||||
/// The words' language (`ja`).
|
/// The words' language (`ja`).
|
||||||
String language;
|
String language;
|
||||||
|
|
||||||
|
|
@ -114,6 +122,7 @@ abstract class DictionarySource
|
||||||
int? id,
|
int? id,
|
||||||
String? source,
|
String? source,
|
||||||
String? sourceVersion,
|
String? sourceVersion,
|
||||||
|
String? kind,
|
||||||
String? language,
|
String? language,
|
||||||
String? glossLanguage,
|
String? glossLanguage,
|
||||||
String? license,
|
String? license,
|
||||||
|
|
@ -130,6 +139,7 @@ abstract class DictionarySource
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'source': source,
|
'source': source,
|
||||||
'sourceVersion': sourceVersion,
|
'sourceVersion': sourceVersion,
|
||||||
|
'kind': kind,
|
||||||
'language': language,
|
'language': language,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'license': license,
|
'license': license,
|
||||||
|
|
@ -148,6 +158,7 @@ abstract class DictionarySource
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'source': source,
|
'source': source,
|
||||||
'sourceVersion': sourceVersion,
|
'sourceVersion': sourceVersion,
|
||||||
|
'kind': kind,
|
||||||
'language': language,
|
'language': language,
|
||||||
'glossLanguage': glossLanguage,
|
'glossLanguage': glossLanguage,
|
||||||
'license': license,
|
'license': license,
|
||||||
|
|
@ -194,6 +205,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
int? id,
|
int? id,
|
||||||
required String source,
|
required String source,
|
||||||
required String sourceVersion,
|
required String sourceVersion,
|
||||||
|
String? kind,
|
||||||
required String language,
|
required String language,
|
||||||
required String glossLanguage,
|
required String glossLanguage,
|
||||||
required String license,
|
required String license,
|
||||||
|
|
@ -206,6 +218,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
id: id,
|
id: id,
|
||||||
source: source,
|
source: source,
|
||||||
sourceVersion: sourceVersion,
|
sourceVersion: sourceVersion,
|
||||||
|
kind: kind,
|
||||||
language: language,
|
language: language,
|
||||||
glossLanguage: glossLanguage,
|
glossLanguage: glossLanguage,
|
||||||
license: license,
|
license: license,
|
||||||
|
|
@ -224,6 +237,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
Object? id = _Undefined,
|
Object? id = _Undefined,
|
||||||
String? source,
|
String? source,
|
||||||
String? sourceVersion,
|
String? sourceVersion,
|
||||||
|
String? kind,
|
||||||
String? language,
|
String? language,
|
||||||
String? glossLanguage,
|
String? glossLanguage,
|
||||||
String? license,
|
String? license,
|
||||||
|
|
@ -237,6 +251,7 @@ class _DictionarySourceImpl extends DictionarySource {
|
||||||
id: id is int? ? id : this.id,
|
id: id is int? ? id : this.id,
|
||||||
source: source ?? this.source,
|
source: source ?? this.source,
|
||||||
sourceVersion: sourceVersion ?? this.sourceVersion,
|
sourceVersion: sourceVersion ?? this.sourceVersion,
|
||||||
|
kind: kind ?? this.kind,
|
||||||
language: language ?? this.language,
|
language: language ?? this.language,
|
||||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||||
license: license ?? this.license,
|
license: license ?? this.license,
|
||||||
|
|
@ -264,6 +279,11 @@ class DictionarySourceUpdateTable
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_is.ColumnValue<String, String> kind(String value) => _is.ColumnValue(
|
||||||
|
table.kind,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
_is.ColumnValue<String, String> language(String value) => _is.ColumnValue(
|
_is.ColumnValue<String, String> language(String value) => _is.ColumnValue(
|
||||||
table.language,
|
table.language,
|
||||||
value,
|
value,
|
||||||
|
|
@ -323,6 +343,11 @@ class DictionarySourceTable extends _is.Table<int?> {
|
||||||
'sourceVersion',
|
'sourceVersion',
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
kind = _is.ColumnString(
|
||||||
|
'kind',
|
||||||
|
this,
|
||||||
|
hasDefault: true,
|
||||||
|
);
|
||||||
language = _is.ColumnString(
|
language = _is.ColumnString(
|
||||||
'language',
|
'language',
|
||||||
this,
|
this,
|
||||||
|
|
@ -365,6 +390,11 @@ class DictionarySourceTable extends _is.Table<int?> {
|
||||||
/// Its version (`3.6.2 (2026-10-05)`).
|
/// Its version (`3.6.2 (2026-10-05)`).
|
||||||
late final _is.ColumnString sourceVersion;
|
late final _is.ColumnString sourceVersion;
|
||||||
|
|
||||||
|
/// What it holds: `words` (JMdict) or `characters` (KANJIDIC2). A string
|
||||||
|
/// rather than an enum, so an app older than a new kind can leave out
|
||||||
|
/// what it doesn't know.
|
||||||
|
late final _is.ColumnString kind;
|
||||||
|
|
||||||
/// The words' language (`ja`).
|
/// The words' language (`ja`).
|
||||||
late final _is.ColumnString language;
|
late final _is.ColumnString language;
|
||||||
|
|
||||||
|
|
@ -393,6 +423,7 @@ class DictionarySourceTable extends _is.Table<int?> {
|
||||||
id,
|
id,
|
||||||
source,
|
source,
|
||||||
sourceVersion,
|
sourceVersion,
|
||||||
|
kind,
|
||||||
language,
|
language,
|
||||||
glossLanguage,
|
glossLanguage,
|
||||||
license,
|
license,
|
||||||
|
|
|
||||||
|
|
@ -334,8 +334,8 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
'search': _is.MethodConnector(
|
'search': _is.MethodConnector(
|
||||||
name: 'search',
|
name: 'search',
|
||||||
params: {
|
params: {
|
||||||
'language': _is.ParameterDescription(
|
'source': _is.ParameterDescription(
|
||||||
name: 'language',
|
name: 'source',
|
||||||
type: _is.getType<String>(),
|
type: _is.getType<String>(),
|
||||||
nullable: false,
|
nullable: false,
|
||||||
),
|
),
|
||||||
|
|
@ -363,7 +363,7 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
||||||
.search(
|
.search(
|
||||||
session,
|
session,
|
||||||
params['language'],
|
params['source'],
|
||||||
params['glossLanguage'],
|
params['glossLanguage'],
|
||||||
params['query'],
|
params['query'],
|
||||||
limit: params['limit'],
|
limit: params['limit'],
|
||||||
|
|
@ -372,8 +372,8 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
'countLabelled': _is.MethodConnector(
|
'countLabelled': _is.MethodConnector(
|
||||||
name: 'countLabelled',
|
name: 'countLabelled',
|
||||||
params: {
|
params: {
|
||||||
'language': _is.ParameterDescription(
|
'source': _is.ParameterDescription(
|
||||||
name: 'language',
|
name: 'source',
|
||||||
type: _is.getType<String>(),
|
type: _is.getType<String>(),
|
||||||
nullable: false,
|
nullable: false,
|
||||||
),
|
),
|
||||||
|
|
@ -396,7 +396,7 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
||||||
.countLabelled(
|
.countLabelled(
|
||||||
session,
|
session,
|
||||||
params['language'],
|
params['source'],
|
||||||
params['glossLanguage'],
|
params['glossLanguage'],
|
||||||
params['label'],
|
params['label'],
|
||||||
),
|
),
|
||||||
|
|
@ -404,8 +404,8 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
'labelled': _is.MethodConnector(
|
'labelled': _is.MethodConnector(
|
||||||
name: 'labelled',
|
name: 'labelled',
|
||||||
params: {
|
params: {
|
||||||
'language': _is.ParameterDescription(
|
'source': _is.ParameterDescription(
|
||||||
name: 'language',
|
name: 'source',
|
||||||
type: _is.getType<String>(),
|
type: _is.getType<String>(),
|
||||||
nullable: false,
|
nullable: false,
|
||||||
),
|
),
|
||||||
|
|
@ -438,7 +438,7 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
||||||
.labelled(
|
.labelled(
|
||||||
session,
|
session,
|
||||||
params['language'],
|
params['source'],
|
||||||
params['glossLanguage'],
|
params['glossLanguage'],
|
||||||
params['label'],
|
params['label'],
|
||||||
offset: params['offset'],
|
offset: params['offset'],
|
||||||
|
|
|
||||||
|
|
@ -625,7 +625,7 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
elements: [
|
elements: [
|
||||||
_isp.IndexElementDefinition(
|
_isp.IndexElementDefinition(
|
||||||
type: _isp.IndexElementDefinitionType.column,
|
type: _isp.IndexElementDefinitionType.column,
|
||||||
definition: 'language',
|
definition: 'source',
|
||||||
),
|
),
|
||||||
_isp.IndexElementDefinition(
|
_isp.IndexElementDefinition(
|
||||||
type: _isp.IndexElementDefinitionType.column,
|
type: _isp.IndexElementDefinitionType.column,
|
||||||
|
|
@ -668,6 +668,13 @@ class Protocol extends _is.DatabaseSerializationManager {
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
dartType: 'String',
|
dartType: 'String',
|
||||||
),
|
),
|
||||||
|
_isp.ColumnDefinition(
|
||||||
|
name: 'kind',
|
||||||
|
columnType: _isp.ColumnType.text,
|
||||||
|
isNullable: false,
|
||||||
|
dartType: 'String',
|
||||||
|
columnDefault: '\'words\'',
|
||||||
|
),
|
||||||
_isp.ColumnDefinition(
|
_isp.ColumnDefinition(
|
||||||
name: 'language',
|
name: 'language',
|
||||||
columnType: _isp.ColumnType.text,
|
columnType: _isp.ColumnType.text,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.DatabaseMigration",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.DatabaseMigrationAction",
|
||||||
|
"type": "alterTable",
|
||||||
|
"alterTable": {
|
||||||
|
"__className__": "serverpod.TableMigration",
|
||||||
|
"name": "dictionary_entry",
|
||||||
|
"schema": "public",
|
||||||
|
"addColumns": [],
|
||||||
|
"deleteColumns": [],
|
||||||
|
"modifyColumns": [],
|
||||||
|
"addIndexes": [
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.IndexDefinition",
|
||||||
|
"indexName": "dictionary_entry_rank_idx",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.IndexElementDefinition",
|
||||||
|
"type": 0,
|
||||||
|
"definition": "source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.IndexElementDefinition",
|
||||||
|
"type": 0,
|
||||||
|
"definition": "glossLanguage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.IndexElementDefinition",
|
||||||
|
"type": 0,
|
||||||
|
"definition": "rank"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "btree",
|
||||||
|
"isUnique": false,
|
||||||
|
"isPrimary": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deleteIndexes": [
|
||||||
|
"dictionary_entry_rank_idx"
|
||||||
|
],
|
||||||
|
"addForeignKeys": [],
|
||||||
|
"deleteForeignKeys": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.DatabaseMigrationAction",
|
||||||
|
"type": "alterTable",
|
||||||
|
"alterTable": {
|
||||||
|
"__className__": "serverpod.TableMigration",
|
||||||
|
"name": "dictionary_source",
|
||||||
|
"schema": "public",
|
||||||
|
"addColumns": [
|
||||||
|
{
|
||||||
|
"__className__": "serverpod.ColumnDefinition",
|
||||||
|
"name": "kind",
|
||||||
|
"fieldName": "kind",
|
||||||
|
"columnType": 0,
|
||||||
|
"isNullable": false,
|
||||||
|
"columnDefault": "'words'",
|
||||||
|
"dartType": "String"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deleteColumns": [],
|
||||||
|
"modifyColumns": [],
|
||||||
|
"addIndexes": [],
|
||||||
|
"deleteIndexes": [],
|
||||||
|
"addForeignKeys": [],
|
||||||
|
"deleteForeignKeys": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warnings": [],
|
||||||
|
"migrationApiVersion": 1
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- ACTION ALTER TABLE
|
||||||
|
--
|
||||||
|
DROP INDEX "dictionary_entry_rank_idx";
|
||||||
|
CREATE INDEX "dictionary_entry_rank_idx" ON "dictionary_entry" USING btree ("source", "glossLanguage", "rank");
|
||||||
|
--
|
||||||
|
-- ACTION ALTER TABLE
|
||||||
|
--
|
||||||
|
ALTER TABLE "dictionary_source" ADD COLUMN "kind" text NOT NULL DEFAULT 'words'::text;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- MIGRATION VERSION FOR halkyon_learn
|
||||||
|
--
|
||||||
|
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||||
|
VALUES ('halkyon_learn', '20261009123850448-dictionary-kind', now())
|
||||||
|
ON CONFLICT ("module")
|
||||||
|
DO UPDATE SET "version" = '20261009123850448-dictionary-kind', "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;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue