Skip to content

fix(service-datasource): testing a datasource draft no longer creates its SQLite database file - #27603

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-yhiysu
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-yhiysu

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

Fixes #27512

Clause-②: no
Metadata-protocol: none
Skills-docs: none — Metadata-protocol is none and there is no narrowing arm; nothing an author writes changes.

维护者速读

  • 改了什么:数据源草稿的「测试连接」(POST /api/v1/datasources/test)遇到不存在的 SQLite 文件时,改为回报「文件不存在」,不再在磁盘上创建这个文件和它缺失的目录。
  • 为什么改:测试连接什么都不保存,却会按「启动写库」的方式打开 SQLite,管理员输错路径就会在服务器上留下空数据库文件(sqlite、sqlite-wasm、turso file: 三种驱动实测都会建)。
  • 风险与回滚:只改草稿探测这一条路,判断在探测内部、建驱动之前;导出类型与默认工厂与 main 字节一致,保存数据源、启动恢复、热注册连接池仍照旧建文件。回滚即还原 datasource-admin-plugin.ts。
  • 先看:datasource-admin-plugin.ts 的 probe(调工厂前的 absentDatabaseFileRefusal 一行)及文件尾部的 probedDatabaseFile / absentDatabaseFileRefusal。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/draft-probe-creates-no-database-file.test.ts,预期 15/15 通过。
  • 需要你做的:无,席位落地。

What changes

  • DatasourceAdminServicePlugin.probe (the draft probe behind POST /api/v1/datasources/test) judges a file-backed draft before it asks the driver factory for anything: sqlite / sqlite3 and sqlite-wasm (config.filename, absent ⇒ :memory:), and turso / libsql with a file: url (scheme in any letter case, path as driver-turso's local engine opens it). A draft whose file does not exist is answered on the probe's existing verdict door, HTTP 200 with result.ok: false and an error naming the file (SQLite database file 'PATH' does not exist. Testing a connection never creates it: …); a path that can be neither opened nor created gets driver-sql's own SqliteDatabaseFileError text. Nothing is written to find out.
  • The existence judgement is driver-sql's one write-free check, resolveSqliteAbsentFileTarget (an absent file reads openedEmptyInMemory; an unopenable one throws), so no second existsSync is open-coded.
  • Internal only: the two helpers are module-private in datasource-admin-plugin.ts. contracts/datasource-driver-factory.ts, default-datasource-driver-factory.ts and every export are byte-identical to main. Because the check is the probe's own, it holds whichever driver factory the host registers.
  • Unchanged: Save, the boot restore, hot pools and DatasourceConnectionService build through the factory exactly as before and still create the file; :memory: and server databases are never judged; the Setup page already renders result.error.

Tests

New src/__tests__/draft-probe-creates-no-database-file.test.ts, through the real door (real HonoHttpServer, real admin routes, real plugin probe, default driver factory unless the case says otherwise):

  • 10 cases: each of sqlite, sqlite3, sqlite-wasm, turso, libsql × a missing flat path and a missing nested path → 200, success: true, result.ok: false, error names the file, the file does not exist and the temp root is still empty (no directory made).
  • A host factory (a fake whose create is a spy) is never asked to build any of the five drafts on a missing path; all answer ok: false; nothing created.
  • A path under a regular file (can be neither opened nor created) → ok: false naming the file; only the blocker file remains.
  • Controls: an existing SQLite file still probes ok: true and nothing else appears beside it; a :memory: draft probes ok: true; the default factory, used directly as Save uses it, still creates the file on connect.

Acceptance notes

  • PM assumption measured. Before the fix, a probe on a missing path created the file AND its missing directories for sqlite, sqlite-wasm and turso (file: url); measured through DatasourceAdminServicePlugin.probe with the default factory on the built tree (all three answered ok: true and left nested/dir/X.db behind). memory with file persistence creates nothing on a probe (measured), so it is not judged.
  • The existing sqliteAbsentFile: 'empty-in-memory' option was not used, as the order said: it is factory-wide and answers ok on a missing file.
  • Shape changed in review (seat REWORK on d9b68b90). The first shape carried a fileMustExist intent on DatasourceConnectionSpec, an exported type, which widened the published surface (Clause-② would have been yes). The judgement now lives inside the probe; the contract and the default factory are back to main's bytes, so Clause-②: no holds.
  • "code + status" read as the probe's verdict door. The draft probe answers every failure as 200 { result: { ok: false, error } }; TestConnectionResult has no code field, and adding one is a response-shape addition this card did not ask for, so it was not made. The door pin asserts status 200, ok: false, the named file and the untouched filesystem; the kind is carried by the error's text, as for every other probe failure.
  • Read-only open not added. Triage said "read-only and must-exist". Must-exist is what stops the file being created. For an EXISTING file, SqlDriver.connect() already writes nothing to a file whose pragmas are set (driver-sql: connect() always runs PRAGMA auto_vacuum = INCREMENTAL, so read-only probes (os migrate duplicates) bump the SQLite header — the "no-op" docblock and "writes nothing at all" doc are untrue #21734); a read-only open would instead fail the probe on a legacy auto_vacuum=NONE file, whose connect issues the setter (read from the code, not measured). Left as is.
  • Embedded replica. A turso draft whose local replica is a file: url that does not exist yet is now refused by the probe too (a first connect would have created the replica file). Consistent with the card: the probe creates nothing.
  • The door pin asserts the error names the file and contains does not exist: the card's acceptance is literally that the probe reports the file does not exist, and the door carries no code, so that phrase is the one kind-bearing fact the Setup page shows.
  • Branch is the card thread's harness-pinned claude/project-thread-yhiysu (the claim names it).

Verification

All readings on head d3b198ec unless the line says otherwise.

Red first, then green (the new test file)
  • Before any fix (the base 72b08eec + the first version of the test): Tests 11 failed | 3 passed (14), every door case expected true to be false (the probe answered ok: true and left the file).
  • Final head: Tests 15 passed (15).
Package suite and typecheck
  • pnpm --filter @objectstack/service-datasource test: Test Files 59 passed (59) · Tests 999 passed (999); typecheck exit 0 (tsconfig includes src/**, tests included). Lock VERDICT command-exit 0.
Ablation (scripts/ablation-replace.mjs, restored blob == HEAD)
  • The probe's guard line (if (fileRefusal) return …) deleted on d3b198ec: mutation landed (anchor 1 → 0, blob 9c6083e9 → abcba580) → Tests 11 failed | 4 passed (15): the ten door cases and the host-factory pin red → restored, blob == HEAD 9c6083e9, git diff HEAD empty.
  • The unopenable-path case stays green under the ablation, as expected: without the guard the real driver's connect refuses that path itself and creates nothing; it pins the probe's answer for it, not the guard.
  • Subjects are imported from src/ by relative path: no rebuild in the path.
Derived gates and the --ran reconciliation
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on d3b198ec → 59 commands, each run with its exit captured before any pipe: 59 × exit 0 (full build present, service-datasource rebuilt on this head, so check:dual-build-cjs-loads measured).
  • --ran: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN.
  • The run on the first shape (bf1d2ab4) caught check:dispatcher-error-vocabulary reading a literal ENOENT class field as an unregistered ADR-0112 code; that refusal class no longer exists in this shape.
Lint, conformance ledger, public face, merge-tree
  • eslint packages/services/service-datasource --no-inline-config on d3b198ec: exit 0. Repo-wide pnpm lint exit 0 on the first head bf1d2ab4.
  • check:driver-conformance before (merge base) and after: 55 covered cell(s), 0 in the DEBT ledger, 0 exempt both times. No driver package is touched.
  • git diff of contracts/, default-datasource-driver-factory.ts and index.ts against the merged main (1ed33609): empty.
  • git merge-tree against the one other open PR on service-datasource (fix(service-datasource)!: POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND #27574): clean, no shared file.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

…e instead of creating it

POST /api/v1/datasources/test opened a file-backed SQLite draft the way a
writing boot does, so the file and its missing directories were created
although nothing was saved. The probe now sets fileMustExist on the
connection spec, and the default factory refuses a missing file-backed
target (sqlite, sqlite-wasm, a file: turso url) before any driver is built.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTUuJysc6ThTQ8Xb1r
… OS's own ENOENT answer

The refusal stamped a literal 'ENOENT', which the dispatcher error-vocabulary
gate reads as an unregistered ADR-0112 code. It now takes code and cause from
the operating system's stat answer, as driver-sql's SqliteDatabaseFileError does.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTUuJysc6ThTQ8Xb1r
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9c5131479d56426dd2c61ca78f8da6271de13171 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b034227a3daa25f924c2665ea0ae1de122a13f1f — the merge of head d3b198ecf6f538fc43df9b4dfdf11c870c284913 into base 9c5131479d56426dd2c61ca78f8da6271de13171, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b034227a3daa25f924c2665ea0ae1de122a13f1f && git checkout b034227a3daa25f924c2665ea0ae1de122a13f1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c5131479d56426dd2c61ca78f8da6271de13171 d3b198ecf6f538fc43df9b4dfdf11c870c284913 && git checkout -B drift-repro 9c5131479d56426dd2c61ca78f8da6271de13171 && git merge --no-ff d3b198ecf6f538fc43df9b4dfdf11c870c284913

node scripts/docs-audit/affected-docs.mjs --json 9c5131479d56426dd2c61ca78f8da6271de13171

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…aft probe; the factory contract stays as on main

The first shape added fileMustExist to DatasourceConnectionSpec, an
exported type, which widened the published surface. The probe now judges
the draft's file itself, before the factory builds anything, and answers
the refusal as 200 ok:false; the contract and the default factory are
back to main's bytes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTUuJysc6ThTQ8Xb1r
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:数据源草稿的「测试连接」(POST /api/v1/datasources/test)遇到不存在的 SQLite 文件,改为回报「文件不存在」,不再在服务器上建出这个文件和缺失的目录。
  • 为什么改:测试连接什么都不保存,却按写库方式打开 SQLite;管理员输错路径就会在服务器上留下空数据库文件(sqlite、sqlite-wasm、turso file: 三种驱动实测都会)。
  • 风险与回滚:只改草稿探测这一条路,判断在探测内部、建驱动之前;导出类型和默认工厂与 main 一字不差;保存、启动恢复、热连接池照旧建文件。回滚 = 还原 datasource-admin-plugin.ts。
  • 先看:datasource-admin-plugin.ts 的 probe 里调工厂前那一行,和文件尾部两个私有函数。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/draft-probe-creates-no-database-file.test.ts,15 条全绿。
  • 需要你做的:无。非受管面,席位落地。

席位意见: ACCEPT。返工后不碰公开面,对宿主注册的第三方工厂同样成立。

席位核对

ACCEPT · head d3b198ecf6f538fc43df9b4dfdf11c870c284913 · 2026-10-29T15:36Z

  • 形态: 3 files, +267/−0: datasource-admin-plugin.ts +63, one test file, one changeset. No governed path, no packages/spec; Tier S.
  • REWORK closed: the first head d9b68b90 added fileMustExist to DatasourceConnectionSpec, an exported type (public-surface widening); on this head contracts/, default-datasource-driver-factory.ts and index.ts are byte-identical to main, so Clause-②: no holds and no contract review is owed (no narrowing of an accept set: the probe's verdict door keeps its 200 { result: { ok, error } } shape).
  • 前提实测: before the fix the probe created the file and its missing directories for sqlite, sqlite-wasm and turso file: (measured); memory creates nothing.
  • 抽查读数: the seat read the hunk: the judgement uses driver-sql's write-free resolveSqliteAbsentFileTarget (a declared dependency of the package, already dynamically imported by sqlite-driver-fallback.ts), returns early only for file-backed ids, leaves :memory: alone; a host factory's create is pinned never called on a missing path.
  • 门禁 check-runs: 52 on this head: 36 success, 16 skipped, 0 other (read 2026-10-29T15:36Z); mergeable_state: clean. Derived gates 59/59, 0 NOT-MEASURED; driver conformance 55 covered / 0 DEBT before and after.
  • 接受的偏差: "code + status" read as the probe's verdict door (no code field exists on TestConnectionResult; adding one would be a response-shape addition); no read-only open (Acceptance notes give the legacy auto_vacuum reason).
  • 范围外去向: none. Skills-docs: none.
  • 散文面: the changeset checked against the diff (Save / boot / hot pools unchanged; probe refuses an absent file).
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

datasources: the draft probe POST /datasources/test creates the SQLite database file on disk although nothing is saved

1 participant