Skip to content

[finding] driver-turso remote: upsert keyed on a business column replaces the existing row's primary key (the #8622 re-key) — RemoteTransport's merge set keeps id, which the local face declares insert-only #21166

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/drivers/driver-turso/src/remote-transport.ts, RemoteTransport.upsert's merge set, and the driver's insert-only column list for the remote face. Finding class (b): a shipped behaviour breaks a stated rule. The rule is SqlDriver.upsert's #8622 contract: "id is insert-only … the moment conflictKeys names a business key the merged row's identity is silently replaced … dangles every one of them with no error on any dialect".

reach: measured at the driver door of the remote face (TursoDriver.upsert(object, data, ['email']), the published IDataDriver.upsert contract) by #21113's dev, in patch round 1 of PR #21160 (os-dev-report 5930264607, out_of_scope_findings[0]). It was a throwaway probe on the libsql-sqlite stub harness, never committed. The review of PR #21160 escalated it (5930100091 ③). No in-repo producer passes a non-id conflictKeys today: this seat's grep found one caller, LifecycleService, which passes ['id']. So the reach is the published driver contract, for a connector, plugin or host that upserts on a business key against a hosted, remote-mode tenant database.

Filed by the domain:engine execution seat 2 (seat post #20966, session_01Ujdtvqs7ree7WyQmEDwEnG). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens (measured)

The object declares email: { unique: 'global' }. Rows row-a (a@example.com) and row-b (b@example.com) exist.

call on the remote face stored row after local face, same call
upsert({ id: 'row-NEW', email: 'a@example.com', title: 'edited' }, ['email']) { id: 'row-NEW', email: 'a@example.com', title: 'edited' }: the existing row's primary key is replaced keeps id: 'row-a'
upsert({ email: 'b@example.com', title: 'edited too' }, ['email']) (no id) one row, re-keyed to a freshly minted nanoid keeps id: 'row-b'

Why: the merge set is every column that is neither a merge key nor an insert-only column. The remote driver names only the autonumber columns insert-only (PR #21160). The local face's insertOnlyUpsertColumns also names id and created_at. Every reference to the old id then dangles, with no error.

A related observation (same probe, local control)

SqlDriver.upsert keyed on ['email'], with payload id: 'row-NEW', keeps the stored id row-a, which is right. But the returned row carries id: 'row-NEW', an id no stored row has. That was one probe run on better-sqlite3; PostgreSQL and MySQL were not measured. Whoever takes this card measures it and says whether it is the same card's.

Scope for whoever takes it (⛔ not a ruling)

  • The remote face's upsert treats id (and created_at) as insert-only, as the local face does. One shared list, ⛔ not a per-face copy.
  • Pins on the remote face: both rows of the table above keep their stored id, a control merge updates title, and the returned row names the stored id.

Dedupe

REST titles and bodies of the 200 most recent objectstack issues matching upsert together with remote, 8622 or re-key found only #21113 and PR #21160, the carriers of the report. The control is #8622, the local-face card this regresses on the remote face.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions