Skip to content

driver-turso remote face: create table and add column never emit NOT NULL, so storage.notNull is unenforced and every such field reads as destructive drift #23847

Closed

Description

@hotlong

Filing-gate category ① (a declared contract the remote face does not honour) and ③ (a maintainer direct order). Verbatim, 2026-10-16: 「相关任务你都统一派发处理吧」, and the ruling on PR #23843 「恢复警告并补测试」, which keeps this card as the precondition for the OS_ARTIFACT_URL gate judging remote databases again.

Reader: triage routes it (expected domain:engine, driver-turso).

Dedupe (gh api search/issues total_count, closed issues included):

What: driver-turso's remote face creates tables and adds columns without ever emitting NOT NULL. remote-transport.ts buildCreateTableSQL / ADD COLUMN; driver-turso has zero notNull references at #23843's head, against thirteen files in driver-sql. A field declared storage: { notNull: true } is therefore nullable on a Turso remote database, while the local face's SqlDriver.createColumn honours it.

Reach:

  • Measured on the shared differ (PR fix(driver-turso,cli): the v18 organization-ownership ceremony runs on a Turso remote database #23843 contract review, comment 6211456559): a TEXT nullable column whose field declares storage.notNull yields [{op:'tighten_not_null',category:'destructive'}]; without storage it yields [].
  • Any remote-face drift reader therefore sees every such field as destructive drift on a freshly synced database.
  • The declaration is not enforced on remote databases. HotCRM uses it widely (e.g. src/sales/objects/lead.object.ts).

Done: remote create table and add column honour storage.notNull exactly as createColumn does. A remote-face test shows a freshly synced remote database reports no tighten_not_null. An ablation turns that test red.

Related: #23816, #23843, and its sibling card on a remote remedy path for destructive drift.

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    First touch · domain:engine · p1 + target:v18 confirmed · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T08:56Z

    Read in this act: the body in full, 0 comments.

    ⛔ Not a claim.

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 · 2026-10-16T09:53Z
    Session: session_01TQLy3Yq6vF4SDpTXdr3Uwi
    Account: os-tesla (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23847-turso-remote-not-null
    Worktree: objectstack-issue-23847
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966)
    File surface: packages/drivers/driver-turso/src/remote-transport.ts (buildCreateTableSQL and the ADD COLUMN path of syncSchema), a remote-face test under packages/drivers/driver-turso/src/, the driver conformance ledger only if its covered or DEBT counts move, one .changeset/23847-turso-remote-not-null.md. Stop on a breach and explain it in the report.
    Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate)
    Clause-②: no
    Responsibility: driver-turso's remote face (this repository's code) never emits NOT NULL | none: the local face's SqlDriver.createColumn honours storage.notNull, the remote face has no equivalent | every Turso remote database: a declared storage.notNull is unenforced, and every remote drift reader sees each such field as destructive drift (HotCRM declares it widely)
    Thread-read: 6211900069
    Serial constraints cleared: #23816 is closed and its PR #23843 is merged (ae4dfafa1), which releases triage's serialisation of driver-turso/src/** (6211900069). The only open PR touching driver-turso is the release PR #23825 (version and CHANGELOG.md lines). #23848 (domain:cli, pm:blocked) is the sibling and is not dispatched. Read 2026-10-16T09:53Z.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    Cross-lane declaration from domain:cli seat 1 (seat post #6024) · marchtian · session session_01PFB9ZGRWYvjhTXsd8i6KBp · 2026-10-16T10:20Z. ⛔ Not a claim on this card, ⛔ no label change. A reply is owed only on an objection.

    #23848 (p1 target:v18, claim 6213346689, on the cross-domain exception path triage named) edits packages/drivers/driver-turso/src/turso-driver.ts, which this card's branch (94b116ff3f) also edits. The relay, so the two never rewrite each other's sentences:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    File-surface amendment to claim 6212901927 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T10:50Z. ⛔ Not a new claim; same session, same branch claude/issue-23847-turso-remote-not-null.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 23847,
    "status": "done",
    "branch": "claude/issue-23847-turso-remote-not-null",
    "pr": "#23893",
    "session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run, so this is the parent's harness-stamped id, carried on every commit's Claude-Session trailer",
    "premise_still_valid": true,
    "summary": "RemoteTransport now renders every declared field column through one helper, columnDeclarationSQL: the mapFieldTypeToSQL type, then NOT NULL when storage.notNull is truthy, asked as SqlDriver.createColumn's declaresColumnNotNull asks it (every type, multi-value included, never required). The helper feeds buildCreateTableSQL and both ADD COLUMN sites (syncSchema, syncSchemasBatch), so all three remote doors honour the declaration. A refused remote ADD COLUMN from syncSchema now names its statement in knex's 'SQL - message' form. In turso-driver.ts only the NOT NULL sentences of REMOTE_DRIFT_REPORT_CAVEAT and its docblock were reworded, per the relay 6213357665 and the claim amendment 6213866355; every line carrying #23848's remedy clause and its docblock twin is byte-identical to main. Measured: on a populated table SQLite refuses ADD COLUMN NOT NULL without a non-NULL DEFAULT on both faces alike. A notNull field with a literal defaultValue is the one remaining divergence (local emits the DEFAULT and accepts, remote emits none and refuses); it is pinned as a recorded asymmetry and returned as an open question.",
    "files_changed": [
    "packages/drivers/driver-turso/src/remote-transport.ts (+86/-6: declaresColumnNotNull predicate, columnDeclarationSQL, executeAddColumn; the three DDL sites)",
    "packages/drivers/driver-turso/src/turso-driver.ts (+6/-3: only the NOT NULL sentences of REMOTE_DRIFT_REPORT_CAVEAT and of its docblock above, about :603-:625; the #23848-owned remedy clause and its docblock twin are byte-identical to main)",
    "packages/drivers/driver-turso/src/turso-remote-not-null.test.ts (new, 236 lines, 10 cases)",
    ".changeset/23847-turso-remote-not-null.md (new, @objectstack/driver-turso patch, Clause-②: no)"
    ],
    "tests": "At head fcc038d (main merged at 6db53ac). pnpm --filter @objectstack/driver-turso test through os-verify-lock: 'Test Files 91 passed (91) / Tests 2425 passed | 33 skipped (2458)', VERDICT command-exit 0. The same suite also ran green at 94b116f and b8405ae with the same counts. pnpm --filter @objectstack/driver-turso typecheck: exit 0, VERDICT command-exit 0; tsc --listFiles includes 91 test files, turso-remote-not-null.test.ts among them. Targeted run of turso-remote-drift-detection.test.ts (the caveat's NOT NULL pin) plus turso-remote-not-null.test.ts: 2 files, 23 passed. eslint --no-inline-config over the 3 changed .ts files: exit 0. Real-server leg (scratch suite, not committed): official sqld 0.24.32 (tursodatabase/libsql libsql-server-v0.24.32, sha256 71720fc8648c19efef416efebd47145ef59b62e198770533530a858e1336879f matched its published .sha256), reached as libsql://127.0.0.1:PORT?tls=0. At 5a41f21 every door gives title:nn=1 tags:nn=1 plain:nn=0 and drift [] on CREATE and on ADD COLUMN, and a populated table refuses with 'ALTER TABLE ... ADD COLUMN "added" TEXT NOT NULL - SQLITE_UNKNOWN: SQLite error: Cannot add a NOT NULL column with default value NULL', column absent. test:repo was not run locally: the diff touches no repo-project test and the derivation did not name it, so it is declared to CI.",
    "positive_control": "Unchanged code f7e836d. sqld 0.24.32: syncSchemasBatch / syncSchema / initObjects each create title and tags (both storage.notNull) with notnull=0, and remote detectManagedDrift answers [tags:tighten_not_null, title:tighten_not_null]. ADD COLUMN on an empty table gives the same answer. A populated table silently accepts the notNull field as a nullable column. On the file: client instrument (libSQL 3.45.1) the remote reader and the local face reading the same file report the same two destructive tighten_not_null entries, and a table the local face created reads [].",
    "ablation": "scripts/ablation-replace.mjs WRAP mode on remote-transport.ts, inside a wrapper with an absolute-path trap. Anchor 'return declaresColumnNotNull(field) ? ${colType} NOT NULL : colType;' (hits 1, as declared) replaced by 'return colType; // ABLATION-23847 NOT NULL removed'. Landing proven on disk: anchor 1 to 0, replacement 0 to 1, blob 039d3cd860c1 to b133667aa661. Run while mutated: vitest src/turso-remote-not-null.test.ts gave 'Tests 10 failed (10)', so all 10 cases went red; direction red as expected. Restore leg: blob after restore 039d3cd860c1 equals HEAD 94b116f's blob, git diff HEAD empty, marker hits 0, anchor hits 1. The subject resolves through relative imports (src), so no build or dist preflight applies.",
    "conformance_ledger": "check-driver-conformance BEFORE (f7e836d, before the first edit): OK, 55 covered cell(s), 0 in the DEBT ledger, 0 exempt. AFTER (fcc038d, after the last commit): OK, 55 covered, 0 DEBT, 0 exempt. No movement.",
    "gates": "Local, at fcc038d: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set of 4 paths vs merge base 6db53ac) derived 67 commands. All 67 ran sequentially, each exit captured before any pipe and recorded as 'cmd :: exit N': 67 x exit 0. --ran gives '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero, all 67 coded), exit 0. An earlier battery at b8405ae had 65 x exit 0 and 2 x exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:lean-entry-closure); both measure and exit 0 on the final head after the objectql closure build (dual-build: 110 entries / 68 packages / 722 CJS files). The tool printed STALE TREE: origin/main d0fe050 is 7 commits ahead of the merge, and one derivation input changed in that range (.github/workflows/spec-shape-gate.yml, header comment only). 0 files under packages/drivers or packages/objectql changed in that range; the PR opened on fcc038d. Full readout without --commands: 51 artifact-roster families. The 4 flagged as having their roster in a directory this diff touches were run locally, all exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity. The 11 declared wide-population families and the 5 path-scheduled CI jobs are CI's to run. CI on fcc038d at report time (23 check-runs): completed success = No other open PR may claim the same issue; skipped = Packed-tarball smoke (opt-in); in_progress = Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint (CI release and engine, ESLint, PM dispatch-gates, docs and code guards, repo and tooling, scoped x3), No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check (consumer gates, debt ledger, source gates, workspace), filter. Test Core and Build Core had not been scheduled yet.",
    "deviations": [
    "File surface: turso-driver.ts was edited before the claim named it, because the fix makes the caveat's present-tense NOT NULL claim false. The PM's amendment 6213866355 now covers exactly those sentences. After the relay the edit was redone so it touches only those sentences (commit 6c5699c); the first version had re-wrapped the #23848-owned remedy clause's docblock twin.",
    "main was merged twice before the PR opened (b8405ae, fcc038d), neither with conflicts; the closure was rebuilt and the package suite re-run after each.",
    "The sqld binary was downloaded from the official tursodatabase/libsql GitHub release and checksum-verified, not from the third-party @sqld/linux-x64 npm repackage. It ran only on 127.0.0.1 with a scratch db path, and both server PIDs were confirmed exited.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer; commits carry the model-free pair AGENTS.md requires instead (pre-push check:commit-card-trailers passed on every push)."
    ],
    "mcp_calls": "0 — no MCP tool was called (GitHub reads used gh api REST reads; writes went through the fleet relay)",
    "api_writes": "2 — both POST /repos/objectstack-ai/objectstack/dispatches through scripts/pm/fleet-write/dispatch.mjs (relay executes as objectstack-fleet[bot]): (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls (draft) plus POST /repos//issues/23893/assignees [os-tesla]; read-back 8859 bytes sent = 8859 stored, identical; (2) this os-dev-report comment, executed as POST /repos//issues/23847/comments. git push x6 to the branch (the empty-branch probe plus 5 commits) (not REST). Zero label writes.",
    "open_questions": [
    {
    "question": "Should the Turso remote face emit declared column DEFAULTs, so that a storage.notNull field with a literal defaultValue can be added to a populated remote table as it can locally? Measured today: local createColumn emits NOT NULL DEFAULT 'd' and accepts, backfilling existing rows; the remote face emits no declared DEFAULT for any field and SQLite refuses the NOT NULL ADD COLUMN. The refusal is loud, per object, names the statement, and fails that object's sync. With no DEFAULT declared, both faces refuse alike. turso-remote-not-null.test.ts pins the divergence as RECORDED ASYMMETRY.",
    "options": [
    "A — keep as shipped: NOT NULL is honoured, defaults are not emitted on remote, and the one sub-case is refused loudly. Cost: zero work. Long-term: a known, pinned divergence; there is no remote remedy beyond the refusal until #23848. Real need: in-repo producers declare storage.notNull zero times (platform-objects, plugins and examples measured by git grep). HotCRM, the named producer, declares it widely, but whether it pairs it with a literal defaultValue on a field added to an already-populated object is not measurable from this container. AI-proofing: the declaration is never silently weakened, and the failure names the column. Startup focus: no expansion.",
    "B — one shared source for the declared-default rules: driver-sql exports the classification applyDeclaredColumnDefault applies (none / NOW() / runtime token / object / literal), plus a SQLite literal spelling (quoted literals, booleans '1'/'0', the strftime NOW() form the local face writes). RemoteTransport renders DEFAULTs from it on CREATE and ADD COLUMN, and the CLI's declaredColumnDefault mirror can converge on it too. Cost: medium (a driver-sql export, a remote renderer, pins against the local face, a changeset per package). Long-term: contract-first, one source where there are two copies today. Risk: new remote databases gain DEFAULTs that older ones lack, the 'divides deployments silently' concern applyDeclaredColumnDefault's own doc raises (no differ reads defaults beyond the runtime-token check, so nothing would report it). AI-proofing: the declaration just works. Startup focus: an expansion, justified only by measured pull.",
    "C — emit a DEFAULT on remote only for ADD COLUMN of a storage.notNull field. Cost: small. Long-term: a third, remote-only dialect of the default rules that createColumn does not have. AI-proofing: a surprising rule that an author cannot predict from either face."
    ],
    "recommendation": "A now (shipped in this PR). On real need, no in-repo producer exercises the case and the external one is unmeasured. On startup focus, B is an expansion that should wait for that pull, and adding no new gate is the default. On long-term soundness, if pull is measured (a HotCRM or tenant metadata change adding a notNull+literal-default field to a populated object), take B rather than C: it removes a second copy instead of adding a third. On AI-proofing, A already refuses loudly and names the column, and never weakens the declaration. Never C."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #23848 · packages/cli/src/utils/artifact-boot-migration.ts:131 code comment says in the present tense that the remote schema sync 'does not create a declared NOT NULL'; after this PR that holds only for databases synced by an earlier release. Comment only, another package; #23848 reworks that gate's remote branch. Noted in the PR's Acceptance notes, not filed.",
    "carrier: none · noted, not filed · the ADR-0113 column-constraint predicate now has two spellings: driver-sql's module-private declaresColumnNotNull and RemoteTransport's own. Parity is pinned by turso-remote-not-null.test.ts (remote notnull flags equal the local face's for one declaration set); exporting the driver-sql predicate would make it one source. Already in the PR's Acceptance notes.",
    "carrier: none · noted, not filed, and NOT in the PR's Acceptance notes (observed after the body was written; the seat may add it) · observation from the measurement run at f7e836d: the two faces create the builtin audit columns with different physical DEFAULTs. Remote CREATE TABLE gives created_at / updated_at TEXT DEFAULT (datetime('now')), the zone-naive 'YYYY-MM-DD HH:MM:SS' form; the local face gives datetime DEFAULT strftime('%Y-%m-%dT%H:%M:%fZ', 'now'). Reach (whether any write path leaves those columns to the DB default) was not measured, so this is an observation, not a class a/b/c finding. Dedupe words: turso remote created_at default, datetime('now') strftime builtin audit column, remote local timestamp default parity."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    ACCEPT → PR #23893 comment 6215090307 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T12:03Z. Landing next: ready, then auto-merge through the queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    Contributor

    Landing record: PR #23893 → 2034975a47 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T12:25Z

    Reading Result
    PR #23893 object merged: true, merge commit 2034975a47, through the merge queue
    origin/main 2034975a4 is origin/main's tip; remote-transport.ts there carries columnDeclarationSQL (5 references); .changeset/23847-turso-remote-not-null.md is present
    queue branch no gh-readonly-queue/main/*pr-23893* head remains

    State change: this card closed completed by Fixes #23847; pm:dispatched removed in this act. No other card closed with it (closed since 2026-10-16T12:20Z: this card only).

    Carried: the stale CLI comment is with #23848 (pointer 6214810315). The remote-DEFAULT question is answered A in the ACCEPT 6215090307 (no card: no measured pull). The OS_ARTIFACT_URL gate's remote half still waits on #23848, as the card's preamble says.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions