feat(server): the review log, synced and append-only

ReviewLog keeps every rating of a card: the card, the rating (FSRS's 1 again to 4 easy), when (UTC) and how long answering took, as decided. It is never changed; each device derives a card's schedule by replaying its reviews, so reviews from different devices never conflict. No device id: the sync engine knows each row's device.

The migration (by CLI) only adds: the table, an index on spaceId for space deletion, one for a card's reviews in time order, and the foreign keys (from the space with ON DELETE CASCADE; from the card with ON DELETE CASCADE, DEFERRABLE INITIALLY DEFERRED).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-09-30 16:59:07 +02:00
commit 5334a0baa5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
18 changed files with 15133 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,491 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_learn",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "card",
"dartName": "Card",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "studysetId",
"fieldName": "studysetId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_studyset_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "studysetId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "position"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "card_revision",
"dartName": "CardRevision",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "parents",
"fieldName": "parents",
"columnType": 8,
"isNullable": false,
"dartType": "List<UuidValue>"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "authorId",
"fieldName": "authorId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reportId",
"fieldName": "reportId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "review_log",
"dartName": "ReviewLog",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "rating",
"fieldName": "rating",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reviewedAt",
"fieldName": "reviewedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "durationMs",
"fieldName": "durationMs",
"columnType": 6,
"isNullable": false,
"dartType": "int"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "reviewedAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "studyset",
"dartName": "Studyset",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "studyset_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "studyset_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,136 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "review_log",
"dartName": "ReviewLog",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "rating",
"fieldName": "rating",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reviewedAt",
"fieldName": "reviewedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "durationMs",
"fieldName": "durationMs",
"columnType": 6,
"isNullable": false,
"dartType": "int"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "reviewedAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -6,6 +6,7 @@ import 'package:serverpod_database/serverpod_database.dart';
part '20260930094729362-studysets/migration.dart';
part '20260930122235111-cards/migration.dart';
part '20260930145136285-reviews/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -14,6 +15,7 @@ class MigrationRegistry {
static final List<MigrationVersionSql> migrations = [
_Migration20260930094729362_studysets(),
_Migration20260930122235111_cards(),
_Migration20260930145136285_reviews(),
];
/// List of all client-side database migration versions.

View file

@ -28,6 +28,7 @@ import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'review/review_log.dart' as _irzjbdqz;
import 'studyset/studyset.dart' as _ikbclmae;
export 'auth/device.dart';
export 'auth/github_sign_in_refusal.dart';
@ -36,6 +37,7 @@ export 'card/card.dart';
export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'review/review_log.dart';
export 'studyset/studyset.dart';
export 'client.dart';
export 'sync_tables.dart';
@ -252,6 +254,107 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'review_log',
dartName: 'ReviewLog',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'cardId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'rating',
columnType: _isd.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
_isd.ColumnDefinition(
name: 'reviewedAt',
columnType: _isd.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
_isd.ColumnDefinition(
name: 'durationMs',
columnType: _isd.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'review_log_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'review_log_fk_1',
columns: ['cardId'],
referenceTable: 'card',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'review_log_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'review_log_card_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'cardId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'reviewedAt',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'studyset',
dartName: 'Studyset',
@ -381,6 +484,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _iskrg2g3.InstanceInfo) {
return _iskrg2g3.InstanceInfo.fromJson(data) as T;
}
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
if (t == _ikbclmae.Studyset) {
return _ikbclmae.Studyset.fromJson(data) as T;
}
@ -412,6 +518,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_iskrg2g3.InstanceInfo?>()) {
return (data != null ? _iskrg2g3.InstanceInfo.fromJson(data) : null) as T;
}
if (t == _isc.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ikbclmae.Studyset?>()) {
return (data != null ? _ikbclmae.Studyset.fromJson(data) : null) as T;
}
@ -446,6 +555,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
_ipbyuef1.CardRevision => 'CardRevision',
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_irzjbdqz.ReviewLog => 'ReviewLog',
_ikbclmae.Studyset => 'Studyset',
_ => null,
};
@ -478,6 +588,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'NotFoundException';
case _iskrg2g3.InstanceInfo():
return 'InstanceInfo';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _ikbclmae.Studyset():
return 'Studyset';
}
@ -529,6 +641,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'InstanceInfo') {
return deserialize<_iskrg2g3.InstanceInfo>(data['data']);
}
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
if (dataClassName == 'Studyset') {
return deserialize<_ikbclmae.Studyset>(data['data']);
}
@ -587,6 +702,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _ikbclmae.Studyset:
return _ikbclmae.Studyset.t;
}

View file

@ -0,0 +1,779 @@
/* 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: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../card/card.dart' as _i4926i81;
/// One rating of a card, never changed once written: a card's schedule isn't
/// stored but derived on each device by replaying its reviews in time order,
/// so reviews from different devices never conflict (decided 2026-09-30).
/// Synced; the review goes with its card.
abstract class ReviewLog
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
ReviewLog._({
this.id,
this.spaceId,
required this.cardId,
this.card,
required this.rating,
required this.reviewedAt,
required this.durationMs,
});
factory ReviewLog({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue cardId,
_i4926i81.Card? card,
required int rating,
required DateTime reviewedAt,
required int durationMs,
}) = _ReviewLogImpl;
factory ReviewLog.fromJson(Map<String, dynamic> jsonSerialization) {
return ReviewLog(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
cardId: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['cardId']),
card: jsonSerialization['card'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i4926i81.Card>(
jsonSerialization['card'],
),
rating: jsonSerialization['rating'] as int,
reviewedAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['reviewedAt'],
),
durationMs: jsonSerialization['durationMs'] as int,
);
}
static final t = ReviewLogTable();
static const db = ReviewLogRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue cardId;
/// The card reviewed.
_i4926i81.Card? card;
/// How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
/// 4 easy.
int rating;
/// When, in UTC.
DateTime reviewedAt;
/// How long answering took, from showing the card to rating it, in
/// milliseconds.
int durationMs;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [ReviewLog]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
ReviewLog copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? cardId,
_i4926i81.Card? card,
int? rating,
DateTime? reviewedAt,
int? durationMs,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'ReviewLog',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJson(),
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'ReviewLog',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJsonForProtocol(),
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
};
}
static ReviewLogInclude include({_i4926i81.CardInclude? card}) {
return ReviewLogInclude._(card: card);
}
static ReviewLogIncludeList includeList({
_isd.WhereExpressionBuilder<ReviewLogTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
ReviewLogInclude? include,
}) {
return ReviewLogIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _ReviewLogImpl extends ReviewLog {
_ReviewLogImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue cardId,
_i4926i81.Card? card,
required int rating,
required DateTime reviewedAt,
required int durationMs,
}) : super._(
id: id,
spaceId: spaceId,
cardId: cardId,
card: card,
rating: rating,
reviewedAt: reviewedAt,
durationMs: durationMs,
);
/// Returns a shallow copy of this [ReviewLog]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
ReviewLog copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? cardId,
Object? card = _Undefined,
int? rating,
DateTime? reviewedAt,
int? durationMs,
}) {
return ReviewLog(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
cardId: cardId ?? this.cardId,
card: card is _i4926i81.Card? ? card : this.card?.copyWith(),
rating: rating ?? this.rating,
reviewedAt: reviewedAt ?? this.reviewedAt,
durationMs: durationMs ?? this.durationMs,
);
}
}
class ReviewLogUpdateTable extends _isd.UpdateTable<ReviewLogTable> {
ReviewLogUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> cardId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.cardId,
value,
);
_isd.ColumnValue<int, int> rating(int value) => _isd.ColumnValue(
table.rating,
value,
);
_isd.ColumnValue<DateTime, DateTime> reviewedAt(DateTime value) =>
_isd.ColumnValue(
table.reviewedAt,
value,
);
_isd.ColumnValue<int, int> durationMs(int value) => _isd.ColumnValue(
table.durationMs,
value,
);
}
class ReviewLogTable extends _isd.Table<_isc.UuidValue?> {
ReviewLogTable({super.tableRelation}) : super(tableName: 'review_log') {
updateTable = ReviewLogUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
cardId = _isd.ColumnUuid(
'cardId',
this,
);
rating = _isd.ColumnInt(
'rating',
this,
);
reviewedAt = _isd.ColumnDateTime(
'reviewedAt',
this,
);
durationMs = _isd.ColumnInt(
'durationMs',
this,
);
}
late final ReviewLogUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid cardId;
/// The card reviewed.
_i4926i81.CardTable? _card;
/// How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
/// 4 easy.
late final _isd.ColumnInt rating;
/// When, in UTC.
late final _isd.ColumnDateTime reviewedAt;
/// How long answering took, from showing the card to rating it, in
/// milliseconds.
late final _isd.ColumnInt durationMs;
_i4926i81.CardTable get card {
if (_card != null) return _card!;
_card = _isd.createRelationTable(
relationFieldName: 'card',
field: ReviewLog.t.cardId,
foreignField: _i4926i81.Card.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_i4926i81.CardTable(tableRelation: foreignTableRelation),
);
return _card!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
cardId,
rating,
reviewedAt,
durationMs,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'card') {
return card;
}
return null;
}
}
class ReviewLogInclude extends _isd.IncludeObject {
ReviewLogInclude._({_i4926i81.CardInclude? card}) {
_card = card;
}
_i4926i81.CardInclude? _card;
@override
Map<String, _isd.Include?> get includes => {'card': _card};
@override
_isd.Table<_isc.UuidValue?> get table => ReviewLog.t;
}
class ReviewLogIncludeList extends _isd.IncludeList {
ReviewLogIncludeList._({
_isd.WhereExpressionBuilder<ReviewLogTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(ReviewLog.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => ReviewLog.t;
}
class ReviewLogRepository {
const ReviewLogRepository._();
final attachRow = const ReviewLogAttachRowRepository._();
/// Returns a list of [ReviewLog]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<ReviewLog>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<ReviewLogTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
_isd.Transaction? transaction,
ReviewLogInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<ReviewLog>(
where: where?.call(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [ReviewLog] 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<ReviewLog?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<ReviewLogTable>? where,
int? offset,
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
_isd.Transaction? transaction,
ReviewLogInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<ReviewLog>(
where: where?.call(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [ReviewLog] by its [id] or null if no such row exists.
Future<ReviewLog?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
ReviewLogInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<ReviewLog>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [ReviewLog]s in the list and returns the inserted rows.
///
/// The returned [ReviewLog]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<ReviewLog>> insert(
_isd.DatabaseSession session,
List<ReviewLog> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<ReviewLog>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [ReviewLog] and returns the inserted row.
///
/// The returned [ReviewLog] will have its `id` field set.
Future<ReviewLog> insertRow(
_isd.DatabaseSession session,
ReviewLog row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<ReviewLog>(
row,
transaction: transaction,
);
}
/// Upserts all [ReviewLog]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 [ReviewLog]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<ReviewLog>> upsert(
_isd.DatabaseSession session,
List<ReviewLog> rows, {
required _isd.ColumnSelections<ReviewLogTable> conflictColumns,
_isd.ColumnSelections<ReviewLogTable>? updateColumns,
_isd.WhereExpressionBuilder<ReviewLogTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<ReviewLog>(
rows,
conflictColumns: conflictColumns(ReviewLog.t),
updateColumns: updateColumns?.call(ReviewLog.t),
updateWhere: updateWhere?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [ReviewLog] 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 [ReviewLog] will have its `id` field set.
Future<ReviewLog?> upsertRow(
_isd.DatabaseSession session,
ReviewLog row, {
required _isd.ColumnSelections<ReviewLogTable> conflictColumns,
_isd.ColumnSelections<ReviewLogTable>? updateColumns,
_isd.WhereExpressionBuilder<ReviewLogTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<ReviewLog>(
row,
conflictColumns: conflictColumns(ReviewLog.t),
updateColumns: updateColumns?.call(ReviewLog.t),
updateWhere: updateWhere?.call(ReviewLog.t),
transaction: transaction,
);
}
/// Updates all [ReviewLog]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<ReviewLog>> update(
_isd.DatabaseSession session,
List<ReviewLog> rows, {
_isd.ColumnSelections<ReviewLogTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<ReviewLog>(
rows,
columns: columns?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [ReviewLog]. 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<ReviewLog> updateRow(
_isd.DatabaseSession session,
ReviewLog row, {
_isd.ColumnSelections<ReviewLogTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<ReviewLog>(
row,
columns: columns?.call(ReviewLog.t),
transaction: transaction,
);
}
/// Updates a single [ReviewLog] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<ReviewLog?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<ReviewLogUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<ReviewLog>(
id,
columnValues: columnValues(ReviewLog.t.updateTable),
transaction: transaction,
);
}
/// Updates all [ReviewLog]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<ReviewLog>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<ReviewLogUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<ReviewLogTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<ReviewLog>(
columnValues: columnValues(ReviewLog.t.updateTable),
where: where(ReviewLog.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [ReviewLog]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<ReviewLog>> delete(
_isd.DatabaseSession session,
List<ReviewLog> rows, {
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<ReviewLog>(
rows,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [ReviewLog].
Future<ReviewLog> deleteRow(
_isd.DatabaseSession session,
ReviewLog row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<ReviewLog>(
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<ReviewLog>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<ReviewLogTable> where,
_isd.OrderByBuilder<ReviewLogTable>? orderBy,
_isd.OrderByListBuilder<ReviewLogTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<ReviewLog>(
where: where(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.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(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<ReviewLogTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<ReviewLog>(
where: where?.call(ReviewLog.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [ReviewLog] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<ReviewLogTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<ReviewLog>(
where: where(ReviewLog.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class ReviewLogAttachRowRepository {
const ReviewLogAttachRowRepository._();
/// Creates a relation between the given [ReviewLog] and [Card]
/// by setting the [ReviewLog]'s foreign key `cardId` to refer to the [Card].
Future<void> card(
_isd.DatabaseSession session,
ReviewLog reviewLog,
_i4926i81.Card card, {
_isd.Transaction? transaction,
}) async {
if (reviewLog.id == null) {
throw ArgumentError.notNull('reviewLog.id');
}
if (card.id == null) {
throw ArgumentError.notNull('card.id');
}
var $reviewLog = reviewLog.copyWith(cardId: card.id);
await session.db.updateRow<ReviewLog>(
$reviewLog,
columns: [ReviewLog.t.cardId],
transaction: transaction,
);
}
}

View file

@ -13,6 +13,7 @@
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'review/review_log.dart' as _irzjbdqz;
import 'studyset/studyset.dart' as _ikbclmae;
/// The tables synchronized between client and server by the
@ -21,5 +22,6 @@ import 'studyset/studyset.dart' as _ikbclmae;
final List<_isd.Table> syncTables = [
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_irzjbdqz.ReviewLog.t,
_ikbclmae.Studyset.t,
];

View file

@ -28,6 +28,7 @@ import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'review/review_log.dart' as _irzjbdqz;
import 'studyset/studyset.dart' as _ikbclmae;
export 'auth/device.dart';
export 'auth/github_sign_in_refusal.dart';
@ -36,6 +37,7 @@ export 'card/card.dart';
export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
export 'instance/instance_info.dart';
export 'review/review_log.dart';
export 'studyset/studyset.dart';
export 'sync_tables.dart';
@ -251,6 +253,107 @@ class Protocol extends _is.DatabaseSerializationManager {
],
managed: true,
),
_isp.TableDefinition(
name: 'review_log',
dartName: 'ReviewLog',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'spaceId',
columnType: _isp.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isp.ColumnDefinition(
name: 'cardId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'rating',
columnType: _isp.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
_isp.ColumnDefinition(
name: 'reviewedAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
_isp.ColumnDefinition(
name: 'durationMs',
columnType: _isp.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'review_log_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
_isp.ForeignKeyDefinition(
constraintName: 'review_log_fk_1',
columns: ['cardId'],
referenceTable: 'card',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'review_log_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'review_log_card_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'cardId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'reviewedAt',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'studyset',
dartName: 'Studyset',
@ -372,6 +475,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _iskrg2g3.InstanceInfo) {
return _iskrg2g3.InstanceInfo.fromJson(data) as T;
}
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
if (t == _ikbclmae.Studyset) {
return _ikbclmae.Studyset.fromJson(data) as T;
}
@ -403,6 +509,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _is.getType<_iskrg2g3.InstanceInfo?>()) {
return (data != null ? _iskrg2g3.InstanceInfo.fromJson(data) : null) as T;
}
if (t == _is.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
if (t == _is.getType<_ikbclmae.Studyset?>()) {
return (data != null ? _ikbclmae.Studyset.fromJson(data) : null) as T;
}
@ -440,6 +549,7 @@ class Protocol extends _is.DatabaseSerializationManager {
_ipbyuef1.CardRevision => 'CardRevision',
_i8oee73b.NotFoundException => 'NotFoundException',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_irzjbdqz.ReviewLog => 'ReviewLog',
_ikbclmae.Studyset => 'Studyset',
_ => null,
};
@ -472,6 +582,8 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'NotFoundException';
case _iskrg2g3.InstanceInfo():
return 'InstanceInfo';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _ikbclmae.Studyset():
return 'Studyset';
}
@ -527,6 +639,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'InstanceInfo') {
return deserialize<_iskrg2g3.InstanceInfo>(data['data']);
}
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
if (dataClassName == 'Studyset') {
return deserialize<_ikbclmae.Studyset>(data['data']);
}
@ -586,6 +701,8 @@ class Protocol extends _is.DatabaseSerializationManager {
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _ikbclmae.Studyset:
return _ikbclmae.Studyset.t;
}

View file

@ -0,0 +1,777 @@
/* 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: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is;
import '../card/card.dart' as _i4926i81;
/// One rating of a card, never changed once written: a card's schedule isn't
/// stored but derived on each device by replaying its reviews in time order,
/// so reviews from different devices never conflict (decided 2026-09-30).
/// Synced; the review goes with its card.
abstract class ReviewLog
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
ReviewLog._({
this.id,
this.spaceId,
required this.cardId,
this.card,
required this.rating,
required this.reviewedAt,
required this.durationMs,
});
factory ReviewLog({
_is.UuidValue? id,
int? spaceId,
required _is.UuidValue cardId,
_i4926i81.Card? card,
required int rating,
required DateTime reviewedAt,
required int durationMs,
}) = _ReviewLogImpl;
factory ReviewLog.fromJson(Map<String, dynamic> jsonSerialization) {
return ReviewLog(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
cardId: _is.UuidValueJsonExtension.fromJson(jsonSerialization['cardId']),
card: jsonSerialization['card'] == null
? null
: _i5lsenq5.Protocol().deserialize<_i4926i81.Card>(
jsonSerialization['card'],
),
rating: jsonSerialization['rating'] as int,
reviewedAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['reviewedAt'],
),
durationMs: jsonSerialization['durationMs'] as int,
);
}
static final t = ReviewLogTable();
static const db = ReviewLogRepository._();
@override
_is.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_is.UuidValue cardId;
/// The card reviewed.
_i4926i81.Card? card;
/// How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
/// 4 easy.
int rating;
/// When, in UTC.
DateTime reviewedAt;
/// How long answering took, from showing the card to rating it, in
/// milliseconds.
int durationMs;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [ReviewLog]
/// with some or all fields replaced by the given arguments.
@_is.useResult
ReviewLog copyWith({
_is.UuidValue? id,
int? spaceId,
_is.UuidValue? cardId,
_i4926i81.Card? card,
int? rating,
DateTime? reviewedAt,
int? durationMs,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'ReviewLog',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJson(),
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'ReviewLog',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'cardId': cardId.toJson(),
if (card != null) 'card': card?.toJsonForProtocol(),
'rating': rating,
'reviewedAt': reviewedAt.toJson(),
'durationMs': durationMs,
};
}
static ReviewLogInclude include({_i4926i81.CardInclude? card}) {
return ReviewLogInclude._(card: card);
}
static ReviewLogIncludeList includeList({
_is.WhereExpressionBuilder<ReviewLogTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
ReviewLogInclude? include,
}) {
return ReviewLogIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _ReviewLogImpl extends ReviewLog {
_ReviewLogImpl({
_is.UuidValue? id,
int? spaceId,
required _is.UuidValue cardId,
_i4926i81.Card? card,
required int rating,
required DateTime reviewedAt,
required int durationMs,
}) : super._(
id: id,
spaceId: spaceId,
cardId: cardId,
card: card,
rating: rating,
reviewedAt: reviewedAt,
durationMs: durationMs,
);
/// Returns a shallow copy of this [ReviewLog]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
ReviewLog copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_is.UuidValue? cardId,
Object? card = _Undefined,
int? rating,
DateTime? reviewedAt,
int? durationMs,
}) {
return ReviewLog(
id: id is _is.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
cardId: cardId ?? this.cardId,
card: card is _i4926i81.Card? ? card : this.card?.copyWith(),
rating: rating ?? this.rating,
reviewedAt: reviewedAt ?? this.reviewedAt,
durationMs: durationMs ?? this.durationMs,
);
}
}
class ReviewLogUpdateTable extends _is.UpdateTable<ReviewLogTable> {
ReviewLogUpdateTable(super.table);
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
table.spaceId,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> cardId(_is.UuidValue value) =>
_is.ColumnValue(
table.cardId,
value,
);
_is.ColumnValue<int, int> rating(int value) => _is.ColumnValue(
table.rating,
value,
);
_is.ColumnValue<DateTime, DateTime> reviewedAt(DateTime value) =>
_is.ColumnValue(
table.reviewedAt,
value,
);
_is.ColumnValue<int, int> durationMs(int value) => _is.ColumnValue(
table.durationMs,
value,
);
}
class ReviewLogTable extends _is.Table<_is.UuidValue?> {
ReviewLogTable({super.tableRelation}) : super(tableName: 'review_log') {
updateTable = ReviewLogUpdateTable(this);
spaceId = _is.ColumnInt(
'spaceId',
this,
);
cardId = _is.ColumnUuid(
'cardId',
this,
);
rating = _is.ColumnInt(
'rating',
this,
);
reviewedAt = _is.ColumnDateTime(
'reviewedAt',
this,
);
durationMs = _is.ColumnInt(
'durationMs',
this,
);
}
late final ReviewLogUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _is.ColumnInt spaceId;
late final _is.ColumnUuid cardId;
/// The card reviewed.
_i4926i81.CardTable? _card;
/// How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
/// 4 easy.
late final _is.ColumnInt rating;
/// When, in UTC.
late final _is.ColumnDateTime reviewedAt;
/// How long answering took, from showing the card to rating it, in
/// milliseconds.
late final _is.ColumnInt durationMs;
_i4926i81.CardTable get card {
if (_card != null) return _card!;
_card = _is.createRelationTable(
relationFieldName: 'card',
field: ReviewLog.t.cardId,
foreignField: _i4926i81.Card.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_i4926i81.CardTable(tableRelation: foreignTableRelation),
);
return _card!;
}
@override
List<_is.Column> get columns => [
id,
spaceId,
cardId,
rating,
reviewedAt,
durationMs,
];
@override
_is.Table? getRelationTable(String relationField) {
if (relationField == 'card') {
return card;
}
return null;
}
}
class ReviewLogInclude extends _is.IncludeObject {
ReviewLogInclude._({_i4926i81.CardInclude? card}) {
_card = card;
}
_i4926i81.CardInclude? _card;
@override
Map<String, _is.Include?> get includes => {'card': _card};
@override
_is.Table<_is.UuidValue?> get table => ReviewLog.t;
}
class ReviewLogIncludeList extends _is.IncludeList {
ReviewLogIncludeList._({
_is.WhereExpressionBuilder<ReviewLogTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(ReviewLog.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => ReviewLog.t;
}
class ReviewLogRepository {
const ReviewLogRepository._();
final attachRow = const ReviewLogAttachRowRepository._();
/// Returns a list of [ReviewLog]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<ReviewLog>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<ReviewLogTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
_is.Transaction? transaction,
ReviewLogInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<ReviewLog>(
where: where?.call(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [ReviewLog] 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<ReviewLog?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<ReviewLogTable>? where,
int? offset,
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
_is.Transaction? transaction,
ReviewLogInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<ReviewLog>(
where: where?.call(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [ReviewLog] by its [id] or null if no such row exists.
Future<ReviewLog?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
ReviewLogInclude? include,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<ReviewLog>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [ReviewLog]s in the list and returns the inserted rows.
///
/// The returned [ReviewLog]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<ReviewLog>> insert(
_is.DatabaseSession session,
List<ReviewLog> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<ReviewLog>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [ReviewLog] and returns the inserted row.
///
/// The returned [ReviewLog] will have its `id` field set.
Future<ReviewLog> insertRow(
_is.DatabaseSession session,
ReviewLog row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<ReviewLog>(
row,
transaction: transaction,
);
}
/// Upserts all [ReviewLog]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 [ReviewLog]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<ReviewLog>> upsert(
_is.DatabaseSession session,
List<ReviewLog> rows, {
required _is.ColumnSelections<ReviewLogTable> conflictColumns,
_is.ColumnSelections<ReviewLogTable>? updateColumns,
_is.WhereExpressionBuilder<ReviewLogTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<ReviewLog>(
rows,
conflictColumns: conflictColumns(ReviewLog.t),
updateColumns: updateColumns?.call(ReviewLog.t),
updateWhere: updateWhere?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [ReviewLog] 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 [ReviewLog] will have its `id` field set.
Future<ReviewLog?> upsertRow(
_is.DatabaseSession session,
ReviewLog row, {
required _is.ColumnSelections<ReviewLogTable> conflictColumns,
_is.ColumnSelections<ReviewLogTable>? updateColumns,
_is.WhereExpressionBuilder<ReviewLogTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<ReviewLog>(
row,
conflictColumns: conflictColumns(ReviewLog.t),
updateColumns: updateColumns?.call(ReviewLog.t),
updateWhere: updateWhere?.call(ReviewLog.t),
transaction: transaction,
);
}
/// Updates all [ReviewLog]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<ReviewLog>> update(
_is.DatabaseSession session,
List<ReviewLog> rows, {
_is.ColumnSelections<ReviewLogTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<ReviewLog>(
rows,
columns: columns?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [ReviewLog]. 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<ReviewLog> updateRow(
_is.DatabaseSession session,
ReviewLog row, {
_is.ColumnSelections<ReviewLogTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<ReviewLog>(
row,
columns: columns?.call(ReviewLog.t),
transaction: transaction,
);
}
/// Updates a single [ReviewLog] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<ReviewLog?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<ReviewLogUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<ReviewLog>(
id,
columnValues: columnValues(ReviewLog.t.updateTable),
transaction: transaction,
);
}
/// Updates all [ReviewLog]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<ReviewLog>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<ReviewLogUpdateTable> columnValues,
required _is.WhereExpressionBuilder<ReviewLogTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<ReviewLog>(
columnValues: columnValues(ReviewLog.t.updateTable),
where: where(ReviewLog.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [ReviewLog]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<ReviewLog>> delete(
_is.DatabaseSession session,
List<ReviewLog> rows, {
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<ReviewLog>(
rows,
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [ReviewLog].
Future<ReviewLog> deleteRow(
_is.DatabaseSession session,
ReviewLog row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<ReviewLog>(
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<ReviewLog>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<ReviewLogTable> where,
_is.OrderByBuilder<ReviewLogTable>? orderBy,
_is.OrderByListBuilder<ReviewLogTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<ReviewLog>(
where: where(ReviewLog.t),
orderBy: orderBy?.call(ReviewLog.t),
orderByList: orderByList?.call(ReviewLog.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<ReviewLogTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<ReviewLog>(
where: where?.call(ReviewLog.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [ReviewLog] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<ReviewLogTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<ReviewLog>(
where: where(ReviewLog.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class ReviewLogAttachRowRepository {
const ReviewLogAttachRowRepository._();
/// Creates a relation between the given [ReviewLog] and [Card]
/// by setting the [ReviewLog]'s foreign key `cardId` to refer to the [Card].
Future<void> card(
_is.DatabaseSession session,
ReviewLog reviewLog,
_i4926i81.Card card, {
_is.Transaction? transaction,
}) async {
if (reviewLog.id == null) {
throw ArgumentError.notNull('reviewLog.id');
}
if (card.id == null) {
throw ArgumentError.notNull('card.id');
}
var $reviewLog = reviewLog.copyWith(cardId: card.id);
await session.db.updateRow<ReviewLog>(
$reviewLog,
columns: [ReviewLog.t.cardId],
transaction: transaction,
);
}
}

View file

@ -13,6 +13,7 @@
import 'package:serverpod/serverpod.dart' as _is;
import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'review/review_log.dart' as _irzjbdqz;
import 'studyset/studyset.dart' as _ikbclmae;
/// The tables synchronized between client and server by the
@ -21,5 +22,6 @@ import 'studyset/studyset.dart' as _ikbclmae;
final List<_is.Table> syncTables = [
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_irzjbdqz.ReviewLog.t,
_ikbclmae.Studyset.t,
];

View file

@ -0,0 +1,25 @@
### One rating of a card, never changed once written: a card's schedule isn't
### stored but derived on each device by replaying its reviews in time order,
### so reviews from different devices never conflict (decided 2026-09-30).
### Synced; the review goes with its card.
class: ReviewLog
table: review_log
database: sync
fields:
### The card reviewed.
card: Card?, relation(onDelete=Cascade, deferred)
### How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
### 4 easy.
rating: int
### When, in UTC.
reviewedAt: DateTime
### How long answering took, from showing the card to rating it, in
### milliseconds.
durationMs: int
indexes:
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
review_log_space_idx:
fields: spaceId
# A card's reviews, in time order.
review_log_card_idx:
fields: cardId, reviewedAt

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,491 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_learn",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "card",
"dartName": "Card",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "studysetId",
"fieldName": "studysetId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "position",
"fieldName": "position",
"columnType": 0,
"isNullable": false,
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_studyset_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "studysetId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "position"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "card_revision",
"dartName": "CardRevision",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "parents",
"fieldName": "parents",
"columnType": 8,
"isNullable": false,
"dartType": "List<UuidValue>"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "authorId",
"fieldName": "authorId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reportId",
"fieldName": "reportId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "card_revision_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "card_revision_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "review_log",
"dartName": "ReviewLog",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "rating",
"fieldName": "rating",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reviewedAt",
"fieldName": "reviewedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "durationMs",
"fieldName": "durationMs",
"columnType": 6,
"isNullable": false,
"dartType": "int"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "reviewedAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "studyset",
"dartName": "Studyset",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "studyset_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "studyset_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,136 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "review_log",
"dartName": "ReviewLog",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "cardId",
"fieldName": "cardId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "rating",
"fieldName": "rating",
"columnType": 6,
"isNullable": false,
"dartType": "int"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "reviewedAt",
"fieldName": "reviewedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "durationMs",
"fieldName": "durationMs",
"columnType": 6,
"isNullable": false,
"dartType": "int"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "review_log_fk_1",
"columns": [
"cardId"
],
"referenceTable": "card",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "review_log_card_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "cardId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "reviewedAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,107 @@
BEGIN;
--
-- Function: gen_random_uuid_v7()
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
-- License: MIT (copyright notice included on the generator source code).
--
create or replace function gen_random_uuid_v7()
returns uuid
as $$
begin
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
return encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
end
$$
language plpgsql
volatile;
--
-- ACTION CREATE TABLE
--
CREATE TABLE "review_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"cardId" uuid NOT NULL,
"rating" bigint NOT NULL,
"reviewedAt" timestamp without time zone NOT NULL,
"durationMs" bigint NOT NULL
);
-- Indexes
CREATE INDEX "review_log_space_idx" ON "review_log" USING btree ("spaceId");
CREATE INDEX "review_log_card_idx" ON "review_log" USING btree ("cardId", "reviewedAt");
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "review_log"
ADD CONSTRAINT "review_log_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "review_log"
ADD CONSTRAINT "review_log_fk_1"
FOREIGN KEY("cardId")
REFERENCES "card"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION
DEFERRABLE INITIALLY DEFERRED;
--
-- MIGRATION VERSION FOR halkyon_learn
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_learn', '20260930145136285-reviews', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260930145136285-reviews', "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;

View file

@ -7,3 +7,4 @@
20260916165410927
20260930094729362-studysets
20260930122235111-cards
20260930145136285-reviews