Skip to content

Commit 1cbb0bd

Browse files
committed
fix(sync): deliver snapshot tombstones across scopes
Snapshot publication follows the owning session's current project, while the deletion journal retains the immutable source project. Filtering deletions by that source label can strand identity rows in a destination that legitimately published the moved session. Deliver snapshot tombstones to every revision scope and let PostgreSQL publication ownership or the scoped DuckDB mirror decide whether the row is present.
1 parent fbeee7b commit 1cbb0bd

4 files changed

Lines changed: 33 additions & 6 deletions

File tree

internal/db/project_identity.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type ProjectIdentityObservationKey struct {
5757
}
5858

5959
// SessionProjectIdentitySnapshotKey identifies one immutable snapshot that a
60-
// downstream publication must remove. Project is retained to apply filters.
60+
// downstream publication must remove.
6161
type SessionProjectIdentitySnapshotKey struct {
6262
SessionID string
6363
Project string
@@ -73,8 +73,11 @@ type ProjectIdentityPublicationDelta struct {
7373
}
7474

7575
// LoadProjectIdentityPublicationDelta returns the compact identity changes in
76-
// (afterRevision, throughRevision]. Project filters are applied to both current
77-
// rows and tombstones so filtered targets can maintain independent cursors.
76+
// (afterRevision, throughRevision]. Project filters apply to current rows and
77+
// aggregate-observation tombstones. Snapshot tombstones remain unfiltered
78+
// because current rows are scoped by the owning session project while their
79+
// deletion journal retains the immutable snapshot project. Destinations decide
80+
// whether a tombstone applies from their resident rows or publication owners.
7881
func (db *DB) LoadProjectIdentityPublicationDelta(
7982
ctx context.Context,
8083
afterRevision, throughRevision int64,
@@ -223,7 +226,7 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
223226

224227
tombstoneWhere, tombstoneArgs := projectIdentityPublicationChangeWhere(
225228
"c", "c.project", afterRevision, throughRevision,
226-
projects, excludeProjects,
229+
nil, nil,
227230
)
228231
rows, err = db.getReader().QueryContext(ctx, `
229232
SELECT c.session_id, c.project

internal/db/project_identity_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ func TestLoadProjectIdentityPublicationDeltaReturnsRowsAndTombstones(
225225
assert.Empty(t, betaDelta.Observations)
226226
assert.Equal(t, delta.ObservationDeletes, betaDelta.ObservationDeletes)
227227
assert.Empty(t, betaDelta.Snapshots)
228-
assert.Empty(t, betaDelta.SnapshotDeletes)
228+
assert.Equal(t, delta.SnapshotDeletes, betaDelta.SnapshotDeletes,
229+
"snapshot tombstones must reach scopes selected by the live session project")
229230
}
230231

231232
func TestCopyArchiveIdentityFromPreservesLogicalArchiveAndNewGeneration(

internal/duckdb/sync_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1660,7 +1660,6 @@ func TestFilteredIncrementalPushPublishesMovedSessionSourceSnapshot(t *testing.T
16601660
require.NoError(t, err)
16611661
mirror, err := OpenReadOnly(path)
16621662
require.NoError(t, err)
1663-
t.Cleanup(func() { require.NoError(t, mirror.Close()) })
16641663

16651664
var gotProject string
16661665
require.NoError(t, mirror.QueryRowContext(ctx,
@@ -1675,6 +1674,19 @@ func TestFilteredIncrementalPushPublishesMovedSessionSourceSnapshot(t *testing.T
16751674
).Scan(&snapshotProject))
16761675
assert.Equal(t, sourceProject, snapshotProject,
16771676
"the immutable snapshot keeps its source label while scope follows the session")
1677+
require.NoError(t, mirror.Close())
1678+
1679+
require.NoError(t, local.DeleteSession(sessionID))
1680+
deleted, err := Push(ctx, path, local, duckPushMachine, opts, false, nil)
1681+
require.NoError(t, err)
1682+
assert.False(t, deleted.Diagnostics.Full)
1683+
mirror, err = OpenReadOnly(path)
1684+
require.NoError(t, err)
1685+
t.Cleanup(func() { require.NoError(t, mirror.Close()) })
1686+
assertDuckDBCountWhere(t, mirror,
1687+
"source_session_project_identity_snapshots",
1688+
"source_session_id = ?", sessionID, 0,
1689+
)
16781690
}
16791691

16801692
func TestSyncPreservesAmbiguousIdentityAlongsideResolvedRemote(t *testing.T) {

internal/postgres/push_pgtest_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,17 @@ func TestPushProjectMoveReconcilesFilteredScope(t *testing.T) {
516516
SELECT COUNT(*) FROM source_session_project_identity_snapshots
517517
WHERE project = $1`, targetProject).Scan(&count))
518518
assert.Zero(t, count, "immutable snapshot must remain source-labelled")
519+
if tc.wantSnapshot {
520+
require.NoError(t, local.DeleteSession(sessionID))
521+
_, err = syncer.Push(ctx, false, nil)
522+
require.NoError(t, err)
523+
require.NoError(t, syncer.pg.QueryRowContext(ctx, `
524+
SELECT COUNT(*)
525+
FROM source_session_project_identity_snapshots
526+
WHERE source_session_id = $1`, sessionID).Scan(&count))
527+
assert.Zero(t, count,
528+
"hard deletion must remove the filtered source-labelled snapshot")
529+
}
519530
})
520531
}
521532
}

0 commit comments

Comments
 (0)