Repository navigation
Commit ebdb6f2
Fixes #21166
Clause-②: no
## What this does
**Remote face (`driver-turso`).** `TursoDriver.upsert`'s remote branch
now passes `SqlDriver.insertOnlyUpsertColumns(object)` to
`RemoteTransport.upsert`. This is the same list the local faces build
their merge set from, so it names `id`, `created_at` and the
`auto_number` columns. Before, the branch passed only the autonumber
columns, from a lookup of its own (`remoteAutoNumberColumns`, deleted
here). The merge set then carried `"id" = excluded."id"`, and an upsert
keyed on a business column replaced the stored row's primary key. This
is the re-key the `SqlDriver.upsert` docblock forbids: "`id` is
insert-only … the moment `conflictKeys` names a business key the merged
row's identity is silently replaced". There is now one list and no
second copy. A remote object never renames a column, because
`remoteTableFor` refuses a renaming column map before any statement. So
the list's physical names are the names the transport writes.
**The answer (both faces).** With `id` insert-only, the remote read-back
by the payload's `id` stopped finding the merged row and answered the
payload instead. The local face already behaved that way: H3 below. Both
read-backs now look the landed row up by its conflict-key values, which
identify it on both legs. When a conflict key is empty in the payload,
nothing can have matched, so the row was inserted and the read is by
`id`. On the default `['id']` target, both readings are the same query.
## Hypotheses, measured
- **H1: confirmed.** I reproduced it on `origin/main` `f3b16fc2f` with
the card's table on the libsql-sqlite stub
(`turso-local-remote-upsert-identity-parity.test.ts`, run before the
fix: `8 failed | 5 passed`):
- On the remote face, `upsert({ id: 'row-NEW', email: a, title: 'edited'
}, ['email'])` stored `id: 'row-NEW'`.
- `upsert({ email: b, title: 'edited too' }, ['email'])` stored `id:
'u9ZJdBf6xIsx0URs'`, a minted nanoid.
- The `_id` alias stored `'row-ALIAS'`.
- The local face kept `row-a` and `row-b`.
- **H2: the raise rule is NOT met.** `runImport`'s `writeMode: 'upsert'`
never reaches `driver.upsert`:
- It finds the record by `matchFields` through `findExisting` →
`p.findData` (`packages/core/src/utils/import-runner.ts:574`, `:583`).
- Then it updates by id, `p.updateData({ id: target.id })` (`:941`), or
it creates through `p.createData` (`:965`), `createManyData` or
`insertManyData` (`:757`).
- `ImportProtocolLike` (`:132`) declares no upsert member, and
`bulk-write.ts` has zero `upsert` references.
- The connector pull hands `artifact.upsertKey` to that runner as
`matchFields`
(`packages/services/service-automation/src/connector-pull.ts:291`,
`:413`).
- The only in-repo `driver.upsert` caller is still `LifecycleService`
with `['id']`
(`packages/objectql/src/lifecycle/lifecycle-service.ts:1405`). The
sandbox `ql.upsert` falls back to `insert`, because the engine has no
`upsert`.
- **H3: measured on SQLite and on a private live PostgreSQL 16. It is
the same list's consequence, so it is fixed here.** `SqlDriver.upsert`
stored `os21166_seed` and answered `id: 'os21166_new'`: the read by the
payload's `id` missed, and `|| toUpsert` answered the payload. That is
the failing ablation leg A1 below, on both cells. MySQL was NOT
MEASURED, because no server is in this container. The code path is
dialect-independent.
**File-surface increment (declared).** The claim listed `sql-driver.ts`
as read-only unless lifted. H3's ruling arm adds three things:
- the read-back in `packages/drivers/driver-sql/src/sql-driver.ts`
(nothing else in that file);
- one pin in `sql-driver-upsert-conflict-target-dialects.test.ts`;
- `@objectstack/driver-sql: patch` beside `@objectstack/driver-turso:
patch` in the changeset.
## Pins
- `driver-turso/src/turso-local-remote-upsert-identity-parity.test.ts`,
13 cases on both faces:
- the card's table: each stored id is kept, the merge still writes
`title`, and the answer names the stored id;
- the `_id` alias;
- `created_at` is insert-only, the shared list's second member;
- controls: an `id`-keyed upsert, by default and with `['id']` named,
and a business-key upsert that inserts;
- a parity case that compares the two faces with each other and with the
right answer.
- `driver-sql/src/sql-driver-upsert-conflict-target-dialects.test.ts`
adds one case to the existing pins that keep the merged row's identity.
It runs on the SQLite and live-PG cells: the answer names the stored id
for a supplied and for a minted payload id, and the insert-leg answer is
the control.
## Ablations
Each fix was committed first. Each mutation went through
`scripts/ablation-replace.mjs`, which proved it on disk, and was
restored to a HEAD-equal blob.
| leg | mutation | red | green |
|:--|:--|:--|:--|
| A1 | local read-back back to `where('id', toUpsert.id)` (driver-sql
rebuilt; `ablation-dist-preflight` saw the marker in 2 dist files, then
`--absent` after the restore build) | turso pins: the 3 local answer
cases + parity (`4 failed`); driver-sql pin on **sqlite and live
postgres** (`2 failed`, PG answered `'os21166_new'`) | every remote case
|
| A2 | remote set filtered back to autonumber-only | the 3 remote
stored-id cases, remote `created_at`, parity (`5 failed`) | local face,
controls |
| A2b | only `created_at` dropped from the remote set | remote
`created_at` alone (`1 failed`; stored `'2001-01-01…'`) | the rest |
| A3 | remote read-back back to `"id" = ?` | the 3 remote answer cases +
parity (`4 failed`; answered `'row-NEW'`) | remote stored-id cases |
## Tests (head `f80af709ec`, merge of `origin/main` `fbcc05f40`; all
under `os-verify-lock.sh`)
The test runs below ran on the fix commit `7a597948b3`. The merge
brought only `plugin-audit` and docs files, so they were not re-run. The
gate union ran on `f80af709ec`.
- `driver-sql` build exit 0. Closure `pnpm --filter
'@objectstack/driver-turso^...' build` exit 0.
- `driver-turso` full `vitest run`: `83 passed (83)` files, `2231 passed
| 33 skipped`.
- `driver-sql` full suite in two halves of 109 and 108 files: `104
passed | 5 skipped`, `1440 passed | 100 skipped`; `102 passed | 6
skipped`, `1915 passed | 88 skipped`.
- The 8 `driver-sql` files that call `.upsert(` with
`OS_TEST_POSTGRES_URL` on a private PG 16: `8 passed`, `125 passed | 3
skipped`, live postgres RAN, live mysql NOT RUN.
- `driver-sqlite-wasm` (`extends SqlDriver`) upsert file: `3 passed`.
- `driver-turso` and `driver-sql` typecheck exit 0. `--listFiles`
includes both test files.
- Driver conformance ledger, before (`f3b16fc2f`) and after: identical,
`50 covered cell(s), 0 in the DEBT ledger, 0 exempt`.
- Gate union, re-derived with no paths on `f80af709ec`: 63 commands plus
4 roster gates named as ⛔ for these paths. All exit 0.
`check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited
3 (PREREQUISITE NOT MET) and exited 0 after the builds they name.
`dispatch-gates --ran`: `63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN`.
## Acceptance notes
- **Pin sweep.** No other test pinned the payload answer.
`driver-memory` already answers the stored id on a business-key merge
(`memory-unique-constraint.test.ts:174` expects `'1'`), so the SQL faces
now agree with it.
- The answer pin sits in the `sqlite` + `pg` sweep, so the MySQL cell
does not run it. The MySQL read-back takes the same branch.
- A measured cross-tenant behaviour of business-key upserts on the local
face goes to the seat in the report. It was not changed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c1cef3 commit ebdb6f2
6 files changed
Lines changed: 354 additions & 16 deletions
File tree
- .changeset
- packages/drivers
- driver-sql/src
- driver-turso/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
Lines changed: 36 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
466 | 466 | | |
467 | 467 | | |
468 | 468 | | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
469 | 505 | | |
470 | 506 | | |
471 | 507 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8865 | 8865 | | |
8866 | 8866 | | |
8867 | 8867 | | |
| 8868 | + | |
| 8869 | + | |
| 8870 | + | |
8868 | 8871 | | |
8869 | 8872 | | |
8870 | 8873 | | |
8871 | 8874 | | |
| 8875 | + | |
8872 | 8876 | | |
8873 | 8877 | | |
8874 | 8878 | | |
| |||
9052 | 9056 | | |
9053 | 9057 | | |
9054 | 9058 | | |
9055 | | - | |
| 9059 | + | |
| 9060 | + | |
| 9061 | + | |
| 9062 | + | |
| 9063 | + | |
| 9064 | + | |
| 9065 | + | |
| 9066 | + | |
| 9067 | + | |
| 9068 | + | |
| 9069 | + | |
| 9070 | + | |
| 9071 | + | |
| 9072 | + | |
| 9073 | + | |
| 9074 | + | |
| 9075 | + | |
9056 | 9076 | | |
9057 | 9077 | | |
9058 | 9078 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1651 | 1651 | | |
1652 | 1652 | | |
1653 | 1653 | | |
1654 | | - | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
1655 | 1666 | | |
1656 | | - | |
1657 | | - | |
| 1667 | + | |
| 1668 | + | |
1658 | 1669 | | |
1659 | 1670 | | |
1660 | 1671 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2615 | 2615 | | |
2616 | 2616 | | |
2617 | 2617 | | |
2618 | | - | |
2619 | | - | |
2620 | | - | |
2621 | | - | |
2622 | | - | |
2623 | | - | |
2624 | | - | |
2625 | | - | |
2626 | | - | |
2627 | | - | |
2628 | | - | |
2629 | 2618 | | |
2630 | 2619 | | |
2631 | 2620 | | |
| |||
2683 | 2672 | | |
2684 | 2673 | | |
2685 | 2674 | | |
2686 | | - | |
| 2675 | + | |
| 2676 | + | |
| 2677 | + | |
| 2678 | + | |
| 2679 | + | |
| 2680 | + | |
| 2681 | + | |
| 2682 | + | |
| 2683 | + | |
| 2684 | + | |
| 2685 | + | |
| 2686 | + | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
0 commit comments