Skip to content

Commit 2894ebe

Browse files
committed
fix: tolerate null DuckDB message ids in fingerprints
Legacy or manually repaired DuckDB mirrors can contain message rows where id is NULL even though normal pushes write the mirrored local primary key. The new message-id fingerprint should treat that as a mismatch to repair, not as a scan error that aborts incremental push. Scan message ids as nullable and add regression coverage that corrupts a mirrored id to NULL, then verifies the next incremental push rewrites the session and restores the local id.
1 parent 3e83740 commit 2894ebe

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

internal/duckdb/push_fingerprint.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func duckStoredMessageFingerprint(
209209
var token strings.Builder
210210
for rows.Next() {
211211
var ordinal, contentLength, contextTokens, outputTokens int
212-
var id int64
212+
var id sql.NullInt64
213213
var role, content, thinkingText, model, tokenUsage string
214214
var claudeMsgID, claudeReqID string
215215
var srcType, srcSubtype, srcUUID, srcParentUUID string
@@ -240,7 +240,7 @@ func duckStoredMessageFingerprint(
240240
}
241241
fp.Count++
242242
fp.Sum += int64(contentLength)
243-
fmt.Fprintf(&messageIDs, "%d|%d;", ordinal, id)
243+
fmt.Fprintf(&messageIDs, "%d|%t|%d;", ordinal, id.Valid, id.Int64)
244244
if isSystem {
245245
if systemOrdinals.Len() > 0 {
246246
systemOrdinals.WriteByte(',')

internal/duckdb/sync_fastpath_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,38 @@ func TestSyncIncrementalMessageIDChangeFallsBackToRewrite(t *testing.T) {
186186
), "curation refresh must point pins at mirrored messages")
187187
}
188188

189+
func TestSyncIncrementalNullMirrorMessageIDFallsBackToRewrite(t *testing.T) {
190+
ctx := context.Background()
191+
local := newLocalDB(t)
192+
fixture := seedDuckDBSyncFixture(t, local)
193+
syncer := newInMemoryTestSync(t, local, SyncOptions{})
194+
195+
first, err := syncer.Push(ctx, true, nil)
196+
require.NoError(t, err)
197+
require.Equal(t, 2, first.SessionsPushed)
198+
localMessages, err := local.GetAllMessages(ctx, fixture.betaID)
199+
require.NoError(t, err)
200+
require.Len(t, localMessages, 1)
201+
_, err = syncer.DB().ExecContext(ctx,
202+
`UPDATE messages SET id = NULL WHERE session_id = ? AND ordinal = ?`,
203+
fixture.betaID, 0,
204+
)
205+
require.NoError(t, err)
206+
207+
time.Sleep(time.Millisecond)
208+
renameSessionOnly(t, local, fixture.betaID, "beta null id repair")
209+
second, err := syncer.Push(ctx, false, nil)
210+
require.NoError(t, err)
211+
212+
assert.Equal(t, 1, second.SessionsPushed)
213+
assert.Equal(t, 1, second.MessagesPushed)
214+
assert.Equal(t,
215+
localMessages[0].ID,
216+
duckMessageID(t, syncer.DB(), fixture.betaID, 0),
217+
"NULL mirrored message ids must force a repair rewrite",
218+
)
219+
}
220+
189221
func TestSyncIncrementalToolResultEventChangeUpdatesMirror(t *testing.T) {
190222
ctx := context.Background()
191223
local := newLocalDB(t)

0 commit comments

Comments
 (0)