Commit graph serverpod-offline-sync/test/serverpod_offline_sync_test_server
Author SHA1 Message Date
845fb557db
feat: Write a row in its own space, and a new one where the host says
A client with a persistent user wrote every change in its personal
space: an update or delete of a row in a shared space it is a member of
found no row and threw, and a new row could reach a shared space only
through transactionForUser, which a host calls at each write itself.

A write without a transaction now acts in the space it touches, through
transactionForUser, which checks membership and role as before: an
update or delete in the space its rows are stored in, a new row in the
one `newRowSpace` names (null, or no hook, for the personal space). One
call touches one space and is refused, with a StateError, where it
would touch two. A write within a transaction acts in its space, as
before; the server, without a persistent user, is unchanged.

Tested on the client: a writing member updates and deletes a shared
row, by row and by condition; a reading member is refused; a call
naming rows of two spaces is refused; new rows go where the hook says,
refused for a space only read or not a member's; a transaction of the
caller's own still decides. The integration suite passes (994 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 12:40:44 +01:00
6fdd926cf0
perf: Send the changes a peer lacks as they are read, on the server
Collecting pending changes ran in one repeatable-read transaction and
handed nothing out until every change was read. A device's first sync of
a large space thus heard nothing from the server until the end: on a home
server, a library of 56,621 rows took about 100 s, so the client's idle
timeout (40 s, its pings unanswered meanwhile) closed the stream first,
and every attempt started over. Reading the rows in batches made that
shorter, not bounded.

An engine may now hand each change out as soon as it is read
(streamCollectedChanges), the collection's transaction staying open until
the last is out or the listener cancels; cancelling ends the collection
and its transaction, and an error, an integrity violation among them,
reaches the listener once the transaction rolled back, recorded as before.
The server's initializeOfflineSync switches it on: PostgreSQL's snapshot
only reads. A device's engine keeps it off, since Serverpod's SQLite
transactions are write transactions and would hold off the device's own
writes for as long as the peer takes to receive. A receiver merges a batch
only once its end arrives, so a stream broken halfway still merges
nothing.

The new tests collect 600 inserts and 300 renames for a peer behind: as
read, the first change arrives before the renamed rows are read; whole, once
all are; both merge into the author's rows. On embedded PostgreSQL, a row
written by another session during the streamed collection stays out of it,
the changes are a whole collection's, in order, and a listener stopping
after the first lets the next collection run. The orphan-insert violation
test runs both ways. Every PostgreSQL test file passes alone; run together,
the files bind the same port, the two already there as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 12:40:44 +01:00
741e334d6a
perf: Read the domain rows a collection sends in batches
Collecting pending changes read each change's domain row on its own: one
statement per insert, and one per update for the column it carries. A
device that had never synced a space of about 100,600 field rows made the
server run some 60,000 of them in the collection's one transaction, 143
to 198 s, and since nothing is sent before everything is collected, the
client's connection closed first and the next attempt started over.

Before walking the inserts, and again before walking the updates, the
collection now reads the rows they name ahead, 500 to a statement per
table and space, with the same column expressions, in the same snapshot:
SELECT those columns WHERE "spaceId" = the space AND "id" IN (the rows).
The raw rows are kept for the collection and each found row's owner is
recorded in the owner cache; an insert decodes its row and restores its
attempted values from them as before, and an update takes its column
from them, so the updates of a row collected as an insert read nothing
more. A row the read-ahead didn't find (missing, or owned by another
space) takes the per-row path unchanged, which tells those cases apart;
both end the collection with an integrity violation, so they are left
unbatched. Deletes still look up their owners row by row, but find in
the owner cache every row the same collection read ahead. Chunking and
streaming are unchanged.

Measured on embedded PostgreSQL, a first collection of 30,040 rows and
90,054 updates (90,067 field rows) took 51.5 to 53.6 s before and takes
16.9 to 17.1 s now, its 120,194 changes identical on the wire, byte for
byte, to what the per-row path produces from the same database. On SQLite,
3,594 changes of a fixture with structured columns, blobs, deletes and
attempted foreign keys on inserts and updates are byte for byte the same
before and after. The new test collects 1,200 rows, 600 of them renamed,
for a fresh peer (1,800 reads of the table before, 3 now), and 600 renames
since a peer's checkpoint (600 reads before, 2 now), and checks every
payload against the author's rows and the peer's rows after the merge.
The core, server, integration and simulation suites pass, but for one
cross-space seed with delta delivery (DST_SEED_BASE=1794152224), whose
local update of an organization finds no row before this change as after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-09 15:19:35 +01:00
949a6645e0
perf: Write a merge's new rows in chunks when the pass is left out
A merge wrote each inserted row one by one: a savepoint for the domain
row, another for its tracker, two statements each, and then read every
tracker back to record attempted values. A device's first push of a
library of 8,000 cards (24,650 changes) took the server 34 to 37 s, nearly
all of it in those round trips.

A batch the projection pass leaves out (nothing in it touches a foreign
key or a unique index, or its inserts are proven to change no decision)
now writes its new rows many to a statement, up to 1,000 at a time, in
one savepoint: each table's domain rows in one insert, parents' tables
first, then their trackers. A row qualifies when the batch inserts it once
and touches it no other way, no tracker exists for it, nothing earlier in
the batch names it, and every row of the batch it names was written
before it. Its values are planned in the batch's order, as the per-row
path plans them. A chunk whose writes fail (a row there already, perhaps
another space's) is rolled back, the presence it claimed for its rows is
forgotten, and it and every row after it are left to the per-row path,
which tells those cases apart. Attempted values read the trackers the
merge holds instead of querying them.

The proof for merged inserts also accepts a parent of the row's own
table inserted earlier in the batch, so a library's nested studysets no
longer send its first push through the pass; a chunk ends before a row
whose parent of its own table it holds. A row naming itself, or a parent
inserted after it, still takes the pass.

Measured on PostgreSQL, the 24,650 changes merge in about 5 s instead of
34 to 37 s, and with the studysets nested instead of 41 s. A device on
SQLite, whose adapter still inserts row by row, receives the library in
44 s instead of 60 s. The new tests cover a batch spanning chunks, a chunk
failing on a row whose tracker was lost and falling back, and a tree of
one table's rows inserted parents first (the shortcut, a chunk per level)
and children first (the pass). The core, server, integration and
simulation suites pass, but for two PostgreSQL tests that can't start
their database while another embedded PostgreSQL runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:10:54 +02:00
012d94d0ca
perf: Skip the projection pass for merged deletes nothing visible outlives
A merge that deleted anything ran the full projection pass, whose closure
follows foreign keys both ways and so loads every row connected to the
deleted ones. A device deleting a card with its revisions, tags and
reviews cost each device receiving it about 4 s, and the server merging
the push about 5.4 s, in a library of 8,000 cards whose tags join it.

A device deleting a row locally tombstones everything visible below it,
so its batch carries those deletes along. What the pass could still
decide is a row below that the deleting device never saw, inserted here
meanwhile, which it hides by cascade. A batch that only deletes (none
restoring) now leaves the pass out when the deleted tables cascade only,
by the same rules as a local delete (_deletesOnlyCascade, shared with
canSkipProjectionAfterLocalDelete now), no attempted value is pending in
the connected tables, and every visible row below a deleted one, found by
walking down the cascades alone, is deleted by the same batch
(canLeaveProjectionUnchangedByMergedDeletes). A batch both inserting and
deleting, or restoring, takes the pass as before.

Measured on PostgreSQL: such a card delete merges in about 35 ms instead
of 5.4 s; received on a device, 99 ms instead of 4 s. The new tests
cover a leaf deleted through the shortcut and a middle row's delete,
which isn't eligible, hiding its children by the pass; in the app's sync
tests a card deleted while another device rated it declines the shortcut,
and dropping the walk's check fails that test. The core, server,
integration and simulation suites pass, but for two PostgreSQL tests
that can't start their database while another embedded PostgreSQL runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:10:53 +02:00
9495f616c4
perf: Skip the projection passes for merged inserts that cannot change them
A merge that touched any foreign key ran the planning pass for its new
rows and then the full projection pass, whose closure follows foreign
keys both ways and so loads every row connected to the merged ones. In a
library whose tags join its parts, one merged rating, edit, tag or new
card loaded all of it: about 9.5 s on a device receiving it and 12 s on
the server merging a push, for 8,000 cards.

A batch that only inserts new rows (each once) and updates columns no
projection reads now leaves both passes out when its inserts are proven,
as local inserts are (canLeaveProjectionUnchangedByMergedInserts, on
localProjectionDependenciesAreStable): each row references its parents by
id, each parent visible or inserted earlier in the same batch, nothing
refers to the row yet (no row, no attempted value), and no unique claim
is contested. A parent inserted after its child in the batch would find
the child's reference held back, which only the pass restores, so it
counts only when earlier. Deletes, inserts onto rows there already and
anything unproven take the passes as before. The batch's parents are
still resolved together for the writes.

The new tests cover a child of a visible parent, a parent and child in
one batch (both through the shortcut), and a child of a merged delete
and a parent arriving after its held-back child (both through the
passes); dropping the parent or the reference check fails the matching
test. The core, server, integration and simulation suites pass, but for
two PostgreSQL tests that cannot start their database while another
embedded PostgreSQL runs, with or without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:11:58 +02:00
b5450353b9
perf: Skip the projection pass after a local delete that cannot change it
insteadOfDelete ran the full foreign key projection after every local
delete. Its closure follows edges both ways, so it loads every row
connected to the deleted one, which in a schema whose tables all hang off
one another is nearly the whole database: one deleted row of a person's
8,000 flashcards loaded 56,000 of 56,350 CRDT rows and took about 8 s on
the calling isolate, most of it deserialising rows. Inserts and updates
have had a proof that the pass is unnecessary
(canLeaveLocalProjectionUnchanged); deletes had none.

canSkipProjectionAfterLocalDelete proves it for a delete: every table
the delete can cascade into is reached by cascade edges on id only, none
of them restricts its parent (a deleted child can release a parent
delete held back by it), none has a unique index (a released claim can
bring a contestant back) or lies in a component with a SET DEFAULT edge,
and no attempted value is pending in the connected tables. The delete's
own cascade (applyDeleteActions) then is the whole projection; otherwise
the full pass runs as before. applyDeleteActions also asks for the
visible children of a whole batch of parents at once for cascades by id,
instead of one query per parent.

On that database a single delete now takes 6-63 ms, a card with its
revisions, tags and reviews 46-145 ms instead of 28-39 s. The new tests
cover a restrict child deleted locally releasing a merged parent delete
(on the person tables, and on the chain tables, where the shortcut is
asked and declines: the test fails with the restrict condition removed)
and a cascade child deleted locally through the shortcut, its parent
then deleted by a merged remote delete. The core, server, integration
and simulation suites pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:38:40 +02:00
Marcelo Mendonça Soares
ee000e348c
fix: Author the column default an upsert writes for a null foreign key (#164)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-08 22:48:47 -03:00
Marcelo Mendonça Soares
4bf2359dda
fix: Capture outbound changes in a consistent snapshot (#166) 2026-10-08 20:32:24 -03:00
Marcelo Mendonça Soares
3a4ada5baf
fix: Prevent web deadlocks in transactional membership reads (#161) 2026-10-08 19:00:44 -03:00
Marcelo Mendonça Soares
e12c9dc9f0
fix: Drop invalid SetNull and SetDefault test models (#165)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Marcelo Mendonça Soares <marcelomendoncasoares@users.noreply.github.com>
2026-10-08 18:21:10 -03:00
Marcelo Mendonça Soares
f59a90bc97
fix: Correct checkpoint progress and add delta DST coverage (#158) 2026-10-07 15:49:15 -03:00
Marcelo Mendonça Soares
e42075cdd9
perf: Batch projection reads and engine writes (#157) 2026-10-03 00:27:56 -03:00
Marcelo Mendonça Soares
65bcfb72d7
chore: Bump version to 0.0.9 and update CHANGELOG (#154) 2026-09-30 16:07:43 -03:00
Marcelo Mendonça Soares
96271a25ab
fix: Refresh node clocks across sync wrappers (#152) 2026-09-21 16:52:03 -03:00
Marcelo Mendonça Soares
51ce999af0
fix: Repair foreign keys on columns that also carry a unique claim (#151) 2026-09-21 13:56:58 -03:00
Marcelo Mendonça Soares
fc6aaa7b4c
fix: Preserve space state across export round trips (#148) 2026-09-18 15:33:24 -03:00
Marcelo Mendonça Soares
811145eda6
fix: Author column values written by set-based updates (#147) 2026-09-17 16:18:18 -03:00
Marcelo Mendonça Soares
ad3e1a49e4
test: Harden DST authored-state checks and workloads (#123) 2026-09-17 10:39:07 -03:00
Marcelo Mendonça Soares
0f4b8116f4
fix: Close generated sync sessions and expose the shared demo (#135) 2026-09-16 11:40:42 -03:00
Marcelo Mendonça Soares
4789f650be
fix: Preserve harness transactions for non-synced row updates (#133) 2026-09-16 11:12:42 -03:00
Marcelo Mendonça Soares
1f5501ff97
fix: Preserve shared model and SQLite value types during sync (#118) 2026-09-16 00:07:51 -03:00
Marcelo Mendonça Soares
27d875ee39
chore: Prepare 0.0.6 for Serverpod 4.0.0 (#129) 2026-09-14 17:06:48 -03:00
Marcelo Mendonça Soares
b89725f6ea
refactor!: Rename sync APIs and ownership spaces (#128) 2026-09-14 13:39:07 -03:00
Marcelo Mendonça Soares
4331daf661
perf: Reduce redundant projection and metadata reads (#124) 2026-09-14 10:53:25 -03:00
Marcelo Mendonça Soares
574f8d067a
test: Make the DST more robust (#116) 2026-09-14 10:26:56 -03:00
Marcelo Mendonça Soares
2a6d66c79e
chore: Update Serverpod and prepare the release (#117) 2026-09-04 22:10:53 -03:00
Marcelo Mendonça Soares
519a8efca0
fix: Rebuild foreign key and unique projection from authored facts (#115) 2026-09-04 15:37:09 -03:00
Kasper Overgård Nielsen
c78af247fe
test: Add a DST and tests reproducing found failures (#110)
Co-authored-by: Marcelo Soares <marcelo.me.soares@gmail.com>
2026-09-04 13:09:20 -03:00
Marcelo Mendonça Soares
fbad5c61fe
chore: Prepare the release of 0.0.4 with serverpod-4.0.0-rc.1 support (#113) 2026-08-31 21:15:35 -03:00
Marcelo Mendonça Soares
852c81c64a
chore: Update to Serverpod 4.0.0-beta.4 (#112) 2026-08-25 11:25:48 -03:00
Marcelo Mendonça Soares
c04bde5e6f
chore: Bump version to 0.0.2 and update CHANGELOG (#111) 2026-08-11 09:23:10 -03:00
Marcelo Mendonça Soares
9f2ce4d9e6
refactor: Move the core implementation to the shared package with no manual queries (#107) 2026-08-04 11:04:04 -03:00
Marcelo Mendonça Soares
40dbd7a73a
chore: Upgrade Serverpod to 4.0.0-beta.1 (#105) 2026-07-27 23:25:23 -03:00
Marcelo Mendonça Soares
8e7e6551e0
fix: General tests improvements and misc small fixes (#104) 2026-07-22 14:28:14 -03:00
Marcelo Mendonça Soares
a24faf39af
chore: Publish version 0.0.1 of the packages (#103) 2026-07-10 13:16:43 -03:00
Marcelo Mendonça Soares
20b23a1a9c
chore: Add the generated code and migrations that were gitignored (#102) 2026-07-09 11:25:28 -03:00
Marcelo Mendonça Soares
ace841de73
chore: Transfer the package license to Serverpod (#99) 2026-07-09 01:16:49 -03:00
Marcelo Mendonça Soares
d3b9a66efd
fix: Fix sync once hanging forever when opened again too quickly after closing (#98) 2026-07-09 01:15:19 -03:00
Marcelo Mendonça Soares
d89118a43a
feat: Implement the scope management interface (#96) 2026-07-07 11:48:19 -03:00
Marcelo Mendonça Soares
467df68a69
feat: Add support for the upsert and upsertRow ORM methods (#94) 2026-07-04 21:17:08 -03:00
Marcelo Mendonça Soares
c6a873d04c
fix: Require unique FK columns to be nullable (#95) 2026-07-04 21:16:37 -03:00
Marcelo Mendonça Soares
65832eda0a
fix: Add proper suppor to the noReturn parameter on the ORM (#92) 2026-07-03 14:03:44 -03:00
Marcelo Mendonça Soares
af139ea800
fix: Allow only one releasable column per unique index (#91) 2026-07-03 10:49:23 -03:00
Marcelo Mendonça Soares
8acfa7f269
feat: Implement shared scopes with role enforcement (#90) 2026-07-03 01:25:15 -03:00
Marcelo Mendonça Soares
517abde4a1
fix: Make the CrdtDatabase class ephemeral (#82) 2026-06-24 01:18:23 -03:00
Marcelo Mendonça Soares
de7e22d91a
feat: Add an example app and misc fixes (#81) 2026-06-23 22:53:47 -03:00
Marcelo Mendonça Soares
5a8f9c91a0
test: Use includeHiddenRows on tests (#80) 2026-06-21 10:12:39 -03:00
Copilot
5642d9b94d
feat: Require scopeId cascade FK relation to crdt_scopes for synced tables (#79)
Co-authored-by: Marcelo Soares <marcelo.me.soares@gmail.com>
2026-06-21 10:02:08 -03:00
Copilot
38fa00ddfc
feat: Expose includeHiddenRows expression to bypass CRDT tombstone filter (#78)
Co-authored-by: Marcelo Soares <marcelo.me.soares@gmail.com>
2026-06-20 23:07:05 -03:00