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:
parent
38843dbff2
commit
5334a0baa5
10 changed files with 15133 additions and 0 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
777
halkyon_learn_server/lib/src/generated/review/review_log.dart
Normal file
777
halkyon_learn_server/lib/src/generated/review/review_log.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
25
halkyon_learn_server/lib/src/review/review_log.spy.yaml
Normal file
25
halkyon_learn_server/lib/src/review/review_log.spy.yaml
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -7,3 +7,4 @@
|
|||
20260916165410927
|
||||
20260930094729362-studysets
|
||||
20260930122235111-cards
|
||||
20260930145136285-reviews
|
||||
|
|
|
|||
Loading…
Reference in a new issue