feat: step 7's migration, with an index on spaceId

The first migration since the initial one, created with the CLI (serverpod create-migration --tag studysets), since no MCP bridge was set up yet. On the server it is one transaction that only adds: gen_random_uuid_v7(), the studyset table, the sync engine's tables (spaces, their members and nodes, the CRDT bookkeeping, the log of integrity violations) with their foreign keys, and, from Serverpod v4.0.3, the auth module's profile image foreign key re-created as DEFERRABLE INITIALLY DEFERRED, so that dumps restore. The server tests apply it to their Postgres.

studyset gets an index on spaceId: deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables. AGENTS.md makes it the rule for every synced table.

The client migration creates each device's SQLite schema. The generator builds it from the whole definition, so it also contains the server's own tables; only the synced tables and the bookkeeping ever hold data on a device.

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 11:48:55 +02:00
commit 8f45b39c0e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
15 changed files with 18154 additions and 6 deletions

View file

@ -108,6 +108,8 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **Granting a scope** goes through a function like `grantIdentified`, which updates the person *and* their session rows. - **Granting a scope** goes through a function like `grantIdentified`, which updates the person *and* their session rows.
- **Content envelope**: synced content tables carry `keyId` and `nonce` (nullable; `null` = plaintext, the only value, ADR 0002 rule 6) and `payload` (`ByteData`): UTF-8 JSON with a `schemaVersion`, encoded and decoded in one place per feature (e.g. `studyset_payload.dart`). Content fields live inside `payload`: a new field raises the schema version instead of migrating a synced table, which would lock older app versions out of syncing (ADR 0002, rule 3). A payload in a newer schema is skipped with a log, never shown half-read. - **Content envelope**: synced content tables carry `keyId` and `nonce` (nullable; `null` = plaintext, the only value, ADR 0002 rule 6) and `payload` (`ByteData`): UTF-8 JSON with a `schemaVersion`, encoded and decoded in one place per feature (e.g. `studyset_payload.dart`). Content fields live inside `payload`: a new field raises the schema version instead of migrating a synced table, which would lock older app versions out of syncing (ADR 0002, rule 3). A payload in a newer schema is skipped with a log, never shown half-read.
- **Synced data** (`database: sync`, `serverpod_offline_sync`, ADR 0001): every person has their own local SQLite database (`<app support>/people/<authUserId>/sync.sqlite`). Its gateway writes through the `OfflineSyncDatabaseSession`, **names the columns of every update**, and calls `SyncService.requestSync()` after every write; the repository re-reads after its own writes and when a sync merged changes (`remoteChanges`). `SyncService` syncs on triggers (at start, 2 s after the last write, on resume, every 5 min), one sync at a time. **Deleting blanks the payload first** (ADR 0002, rule 2): the engine keeps deleted rows until the space is deleted. Order by the UUID v7 id (ADR 0001, rule 7). Access to synced rows is the engine's: a person syncs their personal space and the spaces granted to them; the endpoint rules above guard everything that isn't synced. - **Synced data** (`database: sync`, `serverpod_offline_sync`, ADR 0001): every person has their own local SQLite database (`<app support>/people/<authUserId>/sync.sqlite`). Its gateway writes through the `OfflineSyncDatabaseSession`, **names the columns of every update**, and calls `SyncService.requestSync()` after every write; the repository re-reads after its own writes and when a sync merged changes (`remoteChanges`). `SyncService` syncs on triggers (at start, 2 s after the last write, on resume, every 5 min), one sync at a time. **Deleting blanks the payload first** (ADR 0002, rule 2): the engine keeps deleted rows until the space is deleted. Order by the UUID v7 id (ADR 0001, rule 7). Access to synced rows is the engine's: a person syncs their personal space and the spaces granted to them; the endpoint rules above guard everything that isn't synced.
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
- **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides, after reading the sync package's changelog. - **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides, after reading the sync package's changelog.
- **Session logs are not persisted** (`persistentEnabled: false` in every run mode) for privacy. Don't re-enable them. - **Session logs are not persisted** (`persistentEnabled: false` in every run mode) for privacy. Don't re-enable them.
- **Secrets** live in `halkyon_learn_server/config/passwords.yaml` (git-ignored). Never change `serverSideSessionKeyHashPepper`: it would sign everyone out, and anonymous accounts can't sign back in. - **Secrets** live in `halkyon_learn_server/config/passwords.yaml` (git-ignored). Never change `serverSideSessionKeyHashPepper`: it would sign everyone out, and anonymous accounts can't sign back in.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_learn",
"tables": [
{
"__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

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,18 @@
/// AUTOGENERATED BY SERVERPOD generate - do not remove. /// AUTOGENERATED BY SERVERPOD create-migration - DO NOT EDIT BY HAND
/// Merging: resolve conflicts like server migration_registry.txt. /// Merging: resolve registry conflicts the same as migration_registry.txt.
/// Updated when you run `serverpod create-migration` for the client.
library; library;
import 'package:serverpod_database/serverpod_database.dart'; import 'package:serverpod_database/serverpod_database.dart';
part '20260930094729362-studysets/migration.dart';
/// Migration registry for the client-side database. /// Migration registry for the client-side database.
class MigrationRegistry { class MigrationRegistry {
/// Ordered list of all client-side database migrations. /// Ordered list of all client-side database migrations.
/// New migrations are always appended at the end. /// New migrations are always appended at the end.
static final List<MigrationVersionSql> migrations = []; static final List<MigrationVersionSql> migrations = [
_Migration20260930094729362_studysets(),
];
/// List of all client-side database migration versions. /// List of all client-side database migration versions.
static List<String> get versions => migrations.map((m) => m.version).toList(); static List<String> get versions => migrations.map((m) => m.version).toList();

View file

@ -88,7 +88,21 @@ class Protocol extends _isd.DatabaseSerializationManager {
matchType: null, matchType: null,
), ),
], ],
indexes: [], indexes: [
_isd.IndexDefinition(
indexName: 'studyset_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true, managed: true,
), ),
..._iaic.Protocol() is _isd.DatabaseSerializationManager ..._iaic.Protocol() is _isd.DatabaseSerializationManager

View file

@ -87,7 +87,21 @@ class Protocol extends _is.DatabaseSerializationManager {
matchType: null, matchType: null,
), ),
], ],
indexes: [], indexes: [
_isp.IndexDefinition(
indexName: 'studyset_space_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true, managed: true,
), ),
..._iais.Protocol.targetTableDefinitions, ..._iais.Protocol.targetTableDefinitions,

View file

@ -15,3 +15,9 @@ fields:
payload: ByteData payload: ByteData
### The encryption nonce; always null, like keyId. ### The encryption nonce; always null, like keyId.
nonce: ByteData? nonce: ByteData?
indexes:
# Deleting a space (account deletion, pruning) finds its rows through this index
# instead of reading the whole table: Postgres doesn't index foreign keys by itself.
# Every synced table gets one.
studyset_space_idx:
fields: spaceId

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,113 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_learn",
"tables": [
{
"__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,388 @@
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 "studyset" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"keyId" text,
"payload" bytea NOT NULL,
"nonce" bytea
);
-- Indexes
CREATE INDEX "studyset_space_idx" ON "studyset" USING btree ("spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_attempted_value" (
"id" bigserial PRIMARY KEY,
"fieldId" bigint NOT NULL,
"value" jsonb NOT NULL,
"projectionReason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_attempted_value__fieldId__unique_idx" ON "crdt_data_attempted_value" USING btree ("fieldId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_fields" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"columnId" bigint NOT NULL,
"nodeId" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_fields_row_column_idx" ON "crdt_data_fields" USING btree ("rowId", "columnId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_rows" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"spaceId" bigint NOT NULL,
"tblId" bigint NOT NULL,
"uuidRowId" uuid NOT NULL,
"nodeId" bigint NOT NULL,
"visibility" bigint NOT NULL DEFAULT 0
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_rows_space_tbl_row_idx" ON "crdt_data_rows" USING btree ("spaceId", "tblId", "uuidRowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_tombstone" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"clFlag" bigint NOT NULL,
"reason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_tombstone_row_idx" ON "crdt_data_tombstone" USING btree ("rowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_nodes" (
"id" bigserial PRIMARY KEY,
"uuidNodeId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"lastHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "crdt_nodes__uuidNodeId__unique_idx" ON "crdt_nodes" USING btree ("uuidNodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_columns" (
"id" bigserial PRIMARY KEY,
"tblId" bigint NOT NULL,
"name" text NOT NULL,
"columnType" text NOT NULL,
"dartType" text NOT NULL,
"isNullable" boolean NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_columns_table_column_idx" ON "crdt_schema_columns" USING btree ("tblId", "name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_tables" (
"id" bigserial PRIMARY KEY,
"name" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_tables__name__unique_idx" ON "crdt_schema_tables" USING btree ("name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_integrity_violations" (
"id" bigserial PRIMARY KEY,
"type" text NOT NULL,
"domainTableName" text NOT NULL,
"uuidRowId" uuid NOT NULL,
"ownerSpaceUuid" uuid,
"incomingSpaceUuid" uuid NOT NULL,
"operation" text NOT NULL,
"uuidNodeId" uuid,
"crdtDataRowId" bigint,
"hlcDatetime" timestamp without time zone,
"hlcCounter" bigint,
"firstSeenAt" timestamp without time zone NOT NULL,
"lastSeenAt" timestamp without time zone NOT NULL,
"occurrences" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_integrity_violations_key_idx" ON "offline_sync_integrity_violations" USING btree ("type", "operation", "domainTableName", "uuidRowId", "ownerSpaceUuid", "incomingSpaceUuid");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_members" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"userUuid" uuid NOT NULL,
"role" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_member_unique_idx" ON "offline_sync_space_members" USING btree ("userUuid", "spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_nodes" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"lastReceivedHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_node_unique_idx" ON "offline_sync_space_nodes" USING btree ("spaceId", "nodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_spaces" (
"id" bigserial PRIMARY KEY,
"uuidSpaceId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"currentNodeId" bigint
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_spaces__uuidSpaceId__unique_idx" ON "offline_sync_spaces" USING btree ("uuidSpaceId");
--
-- ACTION ALTER TABLE
--
ALTER TABLE "serverpod_auth_core_profile" DROP CONSTRAINT IF EXISTS "serverpod_auth_core_profile_fk_1";
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "studyset"
ADD CONSTRAINT "studyset_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_attempted_value"
ADD CONSTRAINT "crdt_data_attempted_value_fk_0"
FOREIGN KEY("fieldId")
REFERENCES "crdt_data_fields"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_1"
FOREIGN KEY("columnId")
REFERENCES "crdt_schema_columns"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_1"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_schema_columns"
ADD CONSTRAINT "crdt_schema_columns_fk_0"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_members"
ADD CONSTRAINT "offline_sync_space_members_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_spaces"
ADD CONSTRAINT "offline_sync_spaces_fk_0"
FOREIGN KEY("currentNodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "serverpod_auth_core_profile"
ADD CONSTRAINT "serverpod_auth_core_profile_fk_1"
FOREIGN KEY("imageId")
REFERENCES "serverpod_auth_core_profile_image"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION
DEFERRABLE INITIALLY DEFERRED;
--
-- MIGRATION VERSION FOR halkyon_learn
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_learn', '20260930094729362-studysets', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260930094729362-studysets', "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

@ -5,3 +5,4 @@
### the conflict by removing and recreating the conflicting migration. ### the conflict by removing and recreating the conflicting migration.
20260916165410927 20260916165410927
20260930094729362-studysets