You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Managed SQL datasource: storing an object over an existing table that has _id and no id drops and recreates the table, and its rows are lost #30562
Filing gate: ① product defect with a named landing and a measured reproduction (data loss). Filed by the domain:ui#5 seat from the out-of-scope finding (class a) of #30327's card thread, os-dev-report 6662619947. Carrier: the server lane.
Business reason. An administrator who connects a company database as a datasource and then stores an object on it can lose that table's rows without any warning. The New datasource dialog used to default to managed mode (PR #30561 changes the default to external), so this path was the default path.
Reader. The server lane dev in packages/plugins/driver-sql (initObjects, in sql-driver-rotation.ts).
Measured (base 8e5fabe5, examples/app-showcase under objectstack dev)
A runtime managed SQLite datasource customer_managed over an existing file with two tables:
parts (3 columns, 2 rows);
legacy_items (_id, title, 2 rows; no id column).
PUT /api/v1/meta/object/parts bound to customer_managed: the table gained 9 platform columns and kept its 2 rows.
PUT /api/v1/meta/object/legacy_items bound to customer_managed: the table was dropped and recreated empty, 2 rows before, 0 after.
Control: the same two objects on an external datasource over a copy of the file left both tables unchanged.
Expected
Storing an object on a managed datasource never drops a table that already holds rows. If the existing shape cannot be adopted (here _id without id), the save is refused with a message that names the table and the column, and nothing is changed.
A test pins the _id-without-id case with rows present.
Not measured
Other SQL drivers (Postgres, MySQL) on the same path. The rebuild is in shared driver-sql code, so they may behave the same.
Triage: audit · p1 · domain:server · data loss · 2026-11-05T17:32Z
⛔ Not a claim.
Measured: storing an object over an existing table that has _id and no id in a managed SQL datasource drops and recreates the table, and its rows are lost with no warning. Reader: driver-sqlinitObjects (sql-driver-rotation.ts).
Claim: PM loop round 16 — p1, data loss
Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6CYE7swB6ksdveFrzwH
Account: os-bill
Branch: claude/project-thread-cs7yq4 (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30562
Domain: domain:server
Seat: domain:server#2
File surface: packages/drivers/driver-sql/src/sql-driver-rotation.ts (the legacy-_id rebuild in initObjects), any other driver-sql drop site the rule forces (named before editing), tests under packages/drivers/driver-sql/src/**, a hand-written doc sentence this makes false, one changeset .changeset/30562-sql-never-drop-table-with-rows.md. ⛔ packages/spec/**, ui/**, apps/**, skills/**, docs/adr/**, content/docs/releases/**, .claude/** (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: xhigh
Clause-②: yes expected (narrowing) — a save over an existing table holding rows whose shape cannot be adopted is refused, naming the table and the column, instead of the table being dropped and recreated empty
Metadata-protocol: none
Responsibility: schema sync drops an existing table holding rows when it has _id and no id | adopting tables that can be adopted; rebuilding empty tables (unchanged) | administrators storing objects on managed SQL datasources over existing databases
Thread-read: 6664775220
Premise: read, to be measured first — at bdbaf80c: initObjects calls dropTable whenever an existing table has _id and no id, without counting rows
Serial constraints cleared: no sibling seat claims driver-sql; seats 3–6 take no new cards.
Order: project file server-seat2/dispatch-30562.md, delivered as the card thread's opening message.
{
"issue": 30562,
"status": "done",
"branch": "claude/project-thread-cs7yq4",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30632",
"session": "session_01Utp6CYE7swB6ksdveFrzwH",
"premise_still_valid": true,
"summary": "Reproduced live at base bdbaf80c: PUT /api/v1/meta/object/legacy_items bound to a runtime managed SQLite datasource over an existing file answered 200 and dropped legacy_items (_id, title; 2 rows), recreating it empty with 11 columns. initObjects now counts the rows of a _id-keyed table without id before its rebuild: one row or more throws ExistingTableAdoptionRefusal (table, column '_id', rows; message names both and gives three ways forward) before any DDL; an empty table is rebuilt as before; an id-keyed table is still adopted by adding columns. After the fix the same PUT answers 200, the table keeps its 2 columns and 2 rows, and the server log carries the #22985 error line followed by the refusal. The _id branch came over unchanged in cdd1bd15c7 (2026-03-22) from the ObjectQL driver; nothing in the repo creates a _id-keyed SQL table, so no shipped upgrade path relied on the drop (title without '!'). The other driver-sql drop sites (sqlite copy-swap rebuild, rotation merge-then-drop, aged shards by declared retention, _objectstack_sequences copy-swap, explicit dropTable) never drop a table holding rows the platform did not create; left alone.",
"tests": "pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 with local PostgreSQL 16 (OS_TEST_POSTGRES_URL), on 8157270e: Test Files 247 passed | 3 skipped (250); Tests 4885 passed | 110 skipped (4995); coverage reporter sqlite RAN, live postgres RAN, live mysql NOT RUN; lock VERDICT command-exit 0. src/sql-driver-schema.test.ts alone with PG: 26 passed, 1 skipped (MySQL cell). New pins (describe 'schema sync never drops a table holding rows — CELL', via syncSchema): refusal with 2 rows (instance + {name, table, column:'_id', rows:2} + message names table and column + column set and rows equal the pre-sync snapshot); CONTROL empty _id table rebuilt (id present, _id gone, declared columns, 0 rows); CONTROL id-keyed table with rows adopted (columns [id, qty, title], rows kept). driver-sql typecheck (tsc --noEmit) exit 0, --listFiles includes the changed files. driver-sqlite-wasm 40 files / 750 tests passed; driver-turso 97 files / 2514 passed, 34 skipped; service-datasource 72 files / 1195 passed. eslint --no-inline-config on the 3 changed source/test files exit 0. Ablations (scripts/ablation-replace.mjs; suite imports src, no build between legs): A1 delete the row guard -> refusal pin red in sqlite + pg cells ('expected null to be an instance of ExistingTableAdoptionRefusal', 2 failed | 24 passed); A2 '> 0' -> '>= 0' -> empty-rebuild control red in sqlite + pg (2 failed | 24 passed); each mutation anchor 1 -> 0 with blob changed, each restore blob == HEAD and git diff HEAD empty.",
"gates": "Local, on head 9a71d244 (merge of origin/main 1c5cee67): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack <4 paths> -> 59 families; all 59 run, exit code written to a file before any pipe: 59 x exit 0. --ran: '59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero). pnpm --filter @objectstack/spec check:generated: 14 artifacts up to date. Repo-wide pnpm lint (eslint . --no-inline-config) NOT run locally: os-dev.md makes the repo-wide scan CI-owned; CI 'Lint · ESLint' carries it (see deviations). CI on 9a71d244 at report time: Governed Surface Queue Guard success, filter success; Lint · ESLint, Lint · repo & tooling gates, Lint · PM dispatch-gates, Type Check · workspace/source/consumer/debt ledger, Check Changeset, Check PR Size, PR-body guards, Build Core, Dogfood Regression Gate in_progress; Temporal Conformance (live PG + MySQL) skipped on PR (push/schedule only). Not waited on; the seat reads the final conclusions.",
"dogfood_battery": "9a71d244 · os-verify-lock: VERDICT command-exit 0 · held the lock 2305s (38m25s) · Test Files 420 passed | 3 skipped (423) · Tests 3496 passed | 36 skipped (3532) · no failure (after full turbo run build of the merged tree, 118 tasks successful)",
"mcp_calls": "7 — issue_read x4, pull_request_read x3 (get, get_check_runs, and one earlier read); no write tool",
"api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 42092094535 -> PR #30632 draft, body read back 12652/12652 bytes identical, assignees none); this comment via post-stamped. git push to claude/project-thread-cs7yq4 is not a REST write.",
"line_budget": "4 files, +211 -1 against merge base (PR #30632 additions 211, deletions 1)",
"files_changed": [
"packages/drivers/driver-sql/src/sql-driver-rotation.ts",
"packages/drivers/driver-sql/src/existing-table-refusal.ts",
"packages/drivers/driver-sql/src/sql-driver-schema.test.ts",
".changeset/30562-sql-never-drop-table-with-rows.md"
],
"deviations": [
"New file packages/drivers/driver-sql/src/existing-table-refusal.ts (refusal class + message), outside the named file list; agreed by the seat. Not exported from index.ts.",
"No ADR-0112 code on the refusal: no door serves a code for a schema-sync refusal and a driver-sql code needs a ledger row in packages/spec (out of surface); pinned by class name + named fields; agreed by the seat.",
"Repo-wide pnpm lint not run locally: the dispatch lists it, os-dev.md makes the repo-wide scan CI-owned; eslint ran on the changed files, CI 'Lint · ESLint' carries the rest.",
"MySQL cell not run locally (no MySQL server here); stated in the PR body. CI's Temporal Conformance job runs it on main push/schedule, not on the PR.",
"Session-context step 'set assignee/reviewers via MCP' skipped: the dispatch forbids MCP writes and assignee writes."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: b · reach: public door + wrong answer — PUT /api/v1/meta/object/NAME answers 200 success when its schema sync is refused (now including this refusal); the refusal reaches only the server log (ensureObjectStorage, #22985 design) · evidence: live measurement in PR #30632 body · dedupe words: publish 200 schema sync refused, ensureObjectStorage error log, save reports sync refusal · carrier: seat (separate card already announced), noted, not filed",
"class: b · reach: named producer — os migrate plan (sql-driver-schema-sync.ts add_columns branch) previews add_columns for a _id-keyed table with no id, while apply refuses it when it holds rows and rebuilds it when empty · evidence: reading, not measured through the CLI · dedupe words: migrate plan _id table, plan apply mismatch, add_columns legacy _id · carrier: seat, noted, not filed"
]
}
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 ·
domain:server· data loss · 2026-11-05T17:32Z⛔ Not a claim.
_idand noidin a managed SQL datasource drops and recreates the table, and its rows are lost with no warning. Reader:driver-sqlinitObjects(sql-driver-rotation.ts).Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Claim: PM loop round 16 — p1, data loss
Session:
session_01Utp6Cbvcmv8YWFhFJwzjKL(seat) · dev threadsession_01Utp6CYE7swB6ksdveFrzwHAccount:
os-billBranch:
claude/project-thread-cs7yq4(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30562Domain:
domain:serverSeat:
domain:server#2File surface:
packages/drivers/driver-sql/src/sql-driver-rotation.ts(the legacy-_idrebuild ininitObjects), any other driver-sql drop site the rule forces (named before editing), tests underpackages/drivers/driver-sql/src/**, a hand-written doc sentence this makes false, one changeset.changeset/30562-sql-never-drop-table-with-rows.md. ⛔packages/spec/**,ui/**,apps/**,skills/**,docs/adr/**,content/docs/releases/**,.claude/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: xhighClause-②: yes expected (narrowing) — a save over an existing table holding rows whose shape cannot be adopted is refused, naming the table and the column, instead of the table being dropped and recreated empty
Metadata-protocol: none
Responsibility: schema sync drops an existing table holding rows when it has
_idand noid| adopting tables that can be adopted; rebuilding empty tables (unchanged) | administrators storing objects on managed SQL datasources over existing databasesThread-read: 6664775220
Premise: read, to be measured first — at
bdbaf80c:initObjectscallsdropTablewhenever an existing table has_idand noid, without counting rowsSerial constraints cleared: no sibling seat claims driver-sql; seats 3–6 take no new cards.
Order: project file
server-seat2/dispatch-30562.md, delivered as the card thread's opening message.objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6CYE7swB6ksdveFrzwH
os-dev-report
{ "issue": 30562, "status": "done", "branch": "claude/project-thread-cs7yq4", "pr": "https://github.com/objectstack-ai/objectstack/pull/30632", "session": "session_01Utp6CYE7swB6ksdveFrzwH", "premise_still_valid": true, "summary": "Reproduced live at base bdbaf80c: PUT /api/v1/meta/object/legacy_items bound to a runtime managed SQLite datasource over an existing file answered 200 and dropped legacy_items (_id, title; 2 rows), recreating it empty with 11 columns. initObjects now counts the rows of a _id-keyed table without id before its rebuild: one row or more throws ExistingTableAdoptionRefusal (table, column '_id', rows; message names both and gives three ways forward) before any DDL; an empty table is rebuilt as before; an id-keyed table is still adopted by adding columns. After the fix the same PUT answers 200, the table keeps its 2 columns and 2 rows, and the server log carries the #22985 error line followed by the refusal. The _id branch came over unchanged in cdd1bd15c7 (2026-03-22) from the ObjectQL driver; nothing in the repo creates a _id-keyed SQL table, so no shipped upgrade path relied on the drop (title without '!'). The other driver-sql drop sites (sqlite copy-swap rebuild, rotation merge-then-drop, aged shards by declared retention, _objectstack_sequences copy-swap, explicit dropTable) never drop a table holding rows the platform did not create; left alone.", "tests": "pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 with local PostgreSQL 16 (OS_TEST_POSTGRES_URL), on 8157270e: Test Files 247 passed | 3 skipped (250); Tests 4885 passed | 110 skipped (4995); coverage reporter sqlite RAN, live postgres RAN, live mysql NOT RUN; lock VERDICT command-exit 0. src/sql-driver-schema.test.ts alone with PG: 26 passed, 1 skipped (MySQL cell). New pins (describe 'schema sync never drops a table holding rows — CELL', via syncSchema): refusal with 2 rows (instance + {name, table, column:'_id', rows:2} + message names table and column + column set and rows equal the pre-sync snapshot); CONTROL empty _id table rebuilt (id present, _id gone, declared columns, 0 rows); CONTROL id-keyed table with rows adopted (columns [id, qty, title], rows kept). driver-sql typecheck (tsc --noEmit) exit 0, --listFiles includes the changed files. driver-sqlite-wasm 40 files / 750 tests passed; driver-turso 97 files / 2514 passed, 34 skipped; service-datasource 72 files / 1195 passed. eslint --no-inline-config on the 3 changed source/test files exit 0. Ablations (scripts/ablation-replace.mjs; suite imports src, no build between legs): A1 delete the row guard -> refusal pin red in sqlite + pg cells ('expected null to be an instance of ExistingTableAdoptionRefusal', 2 failed | 24 passed); A2 '> 0' -> '>= 0' -> empty-rebuild control red in sqlite + pg (2 failed | 24 passed); each mutation anchor 1 -> 0 with blob changed, each restore blob == HEAD and git diff HEAD empty.", "gates": "Local, on head 9a71d244 (merge of origin/main 1c5cee67): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack <4 paths> -> 59 families; all 59 run, exit code written to a file before any pipe: 59 x exit 0. --ran: '59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero). pnpm --filter @objectstack/spec check:generated: 14 artifacts up to date. Repo-wide pnpm lint (eslint . --no-inline-config) NOT run locally: os-dev.md makes the repo-wide scan CI-owned; CI 'Lint · ESLint' carries it (see deviations). CI on 9a71d244 at report time: Governed Surface Queue Guard success, filter success; Lint · ESLint, Lint · repo & tooling gates, Lint · PM dispatch-gates, Type Check · workspace/source/consumer/debt ledger, Check Changeset, Check PR Size, PR-body guards, Build Core, Dogfood Regression Gate in_progress; Temporal Conformance (live PG + MySQL) skipped on PR (push/schedule only). Not waited on; the seat reads the final conclusions.", "dogfood_battery": "9a71d244 · os-verify-lock: VERDICT command-exit 0 · held the lock 2305s (38m25s) · Test Files 420 passed | 3 skipped (423) · Tests 3496 passed | 36 skipped (3532) · no failure (after full turbo run build of the merged tree, 118 tasks successful)", "mcp_calls": "7 — issue_read x4, pull_request_read x3 (get, get_check_runs, and one earlier read); no write tool", "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 42092094535 -> PR #30632 draft, body read back 12652/12652 bytes identical, assignees none); this comment via post-stamped. git push to claude/project-thread-cs7yq4 is not a REST write.", "line_budget": "4 files, +211 -1 against merge base (PR #30632 additions 211, deletions 1)", "files_changed": [ "packages/drivers/driver-sql/src/sql-driver-rotation.ts", "packages/drivers/driver-sql/src/existing-table-refusal.ts", "packages/drivers/driver-sql/src/sql-driver-schema.test.ts", ".changeset/30562-sql-never-drop-table-with-rows.md" ], "deviations": [ "New file packages/drivers/driver-sql/src/existing-table-refusal.ts (refusal class + message), outside the named file list; agreed by the seat. Not exported from index.ts.", "No ADR-0112 code on the refusal: no door serves a code for a schema-sync refusal and a driver-sql code needs a ledger row in packages/spec (out of surface); pinned by class name + named fields; agreed by the seat.", "Repo-wide pnpm lint not run locally: the dispatch lists it, os-dev.md makes the repo-wide scan CI-owned; eslint ran on the changed files, CI 'Lint · ESLint' carries the rest.", "MySQL cell not run locally (no MySQL server here); stated in the PR body. CI's Temporal Conformance job runs it on main push/schedule, not on the PR.", "Session-context step 'set assignee/reviewers via MCP' skipped: the dispatch forbids MCP writes and assignee writes." ], "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: b · reach: public door + wrong answer — PUT /api/v1/meta/object/NAME answers 200 success when its schema sync is refused (now including this refusal); the refusal reaches only the server log (ensureObjectStorage, #22985 design) · evidence: live measurement in PR #30632 body · dedupe words: publish 200 schema sync refused, ensureObjectStorage error log, save reports sync refusal · carrier: seat (separate card already announced), noted, not filed", "class: b · reach: named producer — os migrate plan (sql-driver-schema-sync.ts add_columns branch) previews add_columns for a _id-keyed table with no id, while apply refuses it when it holds rows and rebuilds it when empty · evidence: reading, not measured through the CLI · dedupe words: migrate plan _id table, plan apply mismatch, add_columns legacy _id · carrier: seat, noted, not filed" ] }Generated by Claude Code