Skip to content

feat(runtime): standalone stack 认 mysql://,并对 OS_DATABASE_DRIVER 做真校验 (#6265) - #6344

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-6265-standalone-mysql-and-driver-validation
Aug 7, 2026
Merged

feat(runtime): standalone stack 认 mysql://,并对 OS_DATABASE_DRIVER 做真校验 (#6265)#6344
qq9340100 merged 1 commit into
mainfrom
claude/issue-6265-standalone-mysql-and-driver-validation

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #6265

两半同批,同属一个缺陷家族:一个本栈无法 dispatch 的 driver 选择。落点全部在 packages/runtime/src/standalone-stack.ts

前提重验(当前 origin/main = db59e9c5f,#6272 已并入,行号按合并后实读)

issue 断言 实读结果 位置
CLI 认 mysql[2]:// if (/^mysql2?:\/\//i.test(u)) return 'mysql';,resolveStorageDefinitionmysql 臂,产出 { driverId: 'mysql', config: { url, ...autoMigrate } } packages/cli/src/utils/storage-driver.ts:128 / :288
共享工厂本来就能造 if (kind === 'mysql') { const { SqlDriver } = await import('@objectstack/driver-sql'); … client: 'mysql2' } packages/services/service-datasource/src/default-datasource-driver-factory.ts:423
本栈无 mysql 臂 detectDriverFromUrl() 依次 memory / postgres / mongodb / libsql / turso-https / wasm / file / 裸路径,随后 throw;无 mysql standalone-stack.ts:164(改前)
env 路是裸断言 ?? (process.env.OS_DATABASE_DRIVER?.trim() as ResolvedDriverKind | undefined) standalone-stack.ts:236(改前)
未知值静默落 sqlite 分派链末尾 } else { // sqlite (better-sqlite3) … standalone-stack.ts:367(改前)

三条全部坐实,前提有效。补充两条实读旁证:

  • content/docs/deployment/environment-variables.mdx:52 已把 mysql 列为 OS_DATABASE_DRIVER 合法值,content/docs/data-modeling/drivers.mdx 的 URL 推断表也已列 mysql://… —— 文档早就在宣传本栈不认的能力(declared ≠ enforced),本 PR 让文档变真,故不需要改文档;
  • mysql2@objectstack/driver-sqloptional peer(与 pg 完全同姿势),而 @objectstack/driver-sql 已是 @objectstack/runtime 的 dependency —— 所以本 PR 零依赖变更,不碰 package.json / pnpm-lock.yaml

半 (a):mysql:// 识别 + 定义

半 (b):OS_DATABASE_DRIVER 真校验 —— 两条口径归一

改前 cfg.databaseDriver 走 zod enum(响亮拒绝),env 那一路是 as 断言(运行期零校验)。未知值不匹配任何分派臂,落到末尾 else = SQLite,一声不吭:

  • OS_DATABASE_DRIVER=mysql 且未设 URL ⇒ 默认 file:…/standalone.db ⇒ 静默建 SQLite 库,操作者以为在连 MySQL;
  • 拼写错(mysq1 / postgress)同样静默落 SQLite;
  • 设了 URL 时反而报 sqlite driver was selected but the URL does not look like a file path —— 对一个从没选过 sqlite 的操作者双重误导。

修法(不留第三份清单):

  1. 新增 StandaloneDatabaseDriverSchema 作为唯一一份 driver 词表:databaseDriver 用它、env 值用它 safeParseResolvedDriverKind 由它 z.infer 推出;
  2. 未知值响亮 throw,合法值全集由 .options.join(', ') 推导输出(⛔ 不手写数组,加一个 kind 不会留下过期清单);
  3. 两类错误刻意分辨:Unsupported OS_DATABASE_DRIVER value: "…" vs 既有 Unsupported database URL scheme: …,互不冒充(有钉子);
  4. env 值先 toLowerCase():CLI 读同一个变量时就是 (explicitDriver ?? '').toLowerCase().trim()。若这里大小写敏感,等于把本 issue 要消灭的分叉换个更窄的形状再开一遍。被接受的词表没有变 —— 就是那个 enum,别无其他;
  5. 分派链末尾的 else 不再等于 sqlite,而是 const unreachable: never = dbDriver; —— 下一个加进 enum 却忘了补臂的 kind 是编译错误,不再是一个连错库的运行时。

反向验证(先预测方向,再跑)

两轮拆除,方向都事先写死再执行。

Pass A —— 摘掉半 (a)(detect 臂 + dispatch 臂 + 消息里的 mysql://)。预测: 走 detect 的钉子红(Unsupported scheme);走 explicit driver 的钉子仍绿(它们根本不经过 detect);supports('mysql') 那条仍绿(它钉的是共享工厂那一端,不是本文件)。实测 9 红 22 绿,逐条吻合:

× mysql:// resolves to mysql, keeps the URL, and probes no sqlite file
  → [StandaloneStack] Unsupported database URL scheme: mysql://user:pw@localhost:3306/objectstack. …
× mysql2:// — the second spelling the CLI regex accepts — resolves the same
× the scheme match is case-insensitive, like every other arm
× OS_DATABASE_URL=mysql://… dispatches with no explicit driver at all
× declares { driver: "mysql", config: { url } } instead of throwing "Unsupported database URL scheme"
× an explicit databaseDriver:"mysql" declares mysql — never the sqlite fallback
  → [StandaloneStack] No dispatch arm for database driver kind: mysql. …
× 正对照表 mysql:// / mysql2:// 两行
× an unknown scheme still throws, and the message now lists mysql://
✓ an explicit databaseDriver: "mysql" is accepted by the config schema     ← 预测就该绿
✓ OS_DATABASE_DRIVER=mysql selects the same kind                           ← 预测就该绿
✓ the declared driver id is one the shared factory supports                ← 预测就该绿
Tests  9 failed | 22 passed (31)

同一状态下 pnpm --filter @objectstack/runtime typecheck 也红 —— 这正是 never 卫兵的价值,值得单列:

src/standalone-stack.ts(464,15): error TS2322: Type '"mysql"' is not assignable to type 'never'.

注意这里是「诊断变多/变早」而非单纯变红:改前同样的遗漏是静默 sqlite,现在是编译期报错。

Pass B —— 把半 (b) 原样还原(裸 as + 末尾 else = sqlite)。预测: 三条「响亮拒绝」钉子红,外加大小写归一那条红;「两类错误可分辨」和「空白值 = 未设」两条按构造仍绿实测 4 红 27 绿,逐条吻合:

× a typo with a URL set throws, naming the value and every legal driver
  → expected [Function] to throw an error                      ← 改前:根本不抛
× a typo with NO URL set throws too — it does not quietly become the sqlite default
  → expected [Function] to throw an error
× the whole boot refuses as well, and produces no sqlite definition (both URL states)
  → expected … /Unsupported OS_DATABASE_DRIVER value/ but got '[StandaloneStack] sqlite driver was s…'
                                                              ← 改前那条误导消息的现场
× accepts the CLI-normalized spellings of a legal value (case + surrounding space)
  → expected 'MySQL' to be 'mysql'
✓ the driver refusal and the URL-scheme refusal stay distinguishable  ← 预测就该绿(它钉的是「不互相冒充」)
✓ an empty / whitespace-only value is "unset", not an unknown driver  ← 预测就该绿
Tests  4 failed | 27 passed (31)

两轮后源码逐字还原,复跑 31/31 绿。

测试与门禁

  • 新增 packages/runtime/src/standalone-stack.mysql.test.ts:31 条,含识别、整栈定义、env 校验(有 URL / 无 URL 两种情形)、正对照全家(含 libsql/turso)、wat://nope 不变(fix(cli): os migrate --json 成功时不再把耗时毫秒当成退出码 (#4873) #6220 e2e 从 CLI 端钉着同一条消息)。
  • pnpm --filter @objectstack/runtime test108 files / 1578 tests passed(feat(runtime): standalone stack 分派 libsql://,经可选 Turso driver 包 (#5820) #6272 刚落的 31 条 libsql 钉子在内,其断言面未被触碰)。
  • pnpm --filter @objectstack/runtime typecheck ⇒ 干净。
  • npx eslint --no-inline-config 改动两文件 ⇒ 无输出。
  • node scripts/check-nul-bytes.mjs ⇒ OK;改动文件另做一次 grep -naP 控制字符自扫,无命中。
  • 消费半径已清扫:resolveStandaloneDatabase 的跨包消费方只有 packages/cli/src/utils/migrate-occupancy-gate.ts(只读 sqliteFile)与 barrel 导出;Unsupported database URL scheme 的仓内断言只有 packages/cli/test/migrate-exit-code.e2e.test.ts:210(wat://nope,行为未变)。

changeset

.changeset/standalone-stack-mysql-and-driver-validation.md = @objectstack/runtime: minor。判据同 #6272 的 minor 判例:新增可用 URL scheme + enum 扩值属功能新增;半 (b) 的响亮拒绝是行为变更,但它替换掉的是「静默连错库」,没有任何人能依赖那个旧行为。

范围声明

不碰 content/docs/releases/package.jsonpnpm-lock.yaml;不改 assignee、不自贴标签。explicit-driver 的别名词表(CLI 收 pg / mysql2 / mongo / libsql 等,本栈 enum 只收规范拼写)与「显式非文件 driver 却没给 URL」这两点仍有分叉,另行开 issue 记录,不在本 PR 内顺手扩契约。


Generated by Claude Code

…DATABASE_DRIVER (#6265)

Two halves of one defect family in `standalone-stack.ts` — a driver selection
this stack could not dispatch.

(a) `mysql://` — the #5820 split with a different scheme. The CLI has classified
`mysql[2]://` as `mysql` since forever (`inferDriverTypeFromUrl`), the shared
factory has always been able to build it (`kind === 'mysql'` -> SqlDriver on
`mysql2`), and the docs' URL-inference table lists it; only `detectDriverFromUrl`
here had no arm, so one `OS_DATABASE_URL=mysql://…` booted under `os start` and
died under `os migrate` with `Unsupported database URL scheme`.

- detection arm uses character-for-character the CLI's regex; dispatch declares
  `{ driver: 'mysql', config: { url } }` for the shared factory, like postgres.
- no dependency change: `mysql2` is already an optional peer of
  `@objectstack/driver-sql`, the same posture `pg` has.
- `sqliteFile` stays null for a MySQL target (occupancy-gate semantics).

(b) `OS_DATABASE_DRIVER` was a bare `as` cast while `cfg.databaseDriver` went
through a zod enum. An unknown value matched no dispatch arm and landed in the
chain's trailing `else`: SQLite, silently (#3276 class).

- one declaration (`StandaloneDatabaseDriverSchema`) now feeds the config key,
  the env value and the `ResolvedDriverKind` union; the refusal enumerates
  `.options` instead of a fourth hand-written list.
- unknown value -> loud throw naming the value and every legal driver; the env
  value is lower-cased first, matching the CLI's reader of the same variable.
- the trailing `else` is a `never` guard now, so the next kind added without a
  dispatch arm is a compile error rather than a wrong database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbxm29qPKnLf44AbSxizqW
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 2:20pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Collaborator Author

范围外发现已归档:#6345(未认领、未贴标签,留给 PM 分诊)。

本 PR 正文「范围声明」里提到的两点分叉,加上第三点,已合并记入 #6345:

  1. 别名词表两侧不同 —— CLI 的 explicit driver 收 pg / postgresql / mysql2 / mongo / libsql / wasm-sqlite / mingo / in-memory / sql,本栈 enum 只收规范拼写。改前这条路是静默 SQLite,本 PR 之后是响亮拒绝并列出合法值,所以是净改善;但同一个 OS_DATABASE_DRIVER 仍有两种答案。收敛方向要裁(仓内已有唯一别名表 DRIVER_ID_ALIASES / resolveDriverId,但其规范 id 是 mongo 而非 mongodb,且不含 turso/libsql,不是机械替换)。
  2. 显式选了「非文件」driver 却没给 URL —— CLI 侧产出 config: { url: undefined }(pg 于是连它自己的 localhost 默认),本栈侧兜底成 file:…/standalone.db(mysql/postgres/mongodb 拿到一个 file: DSN)。turso 在 CLI 侧已经有 typed UnsupportedDriverError 把这句话说清楚了,同一句对 mysql/postgres/mongodb 一字不改都成立。
  3. --database-driver 的 help 文案漏 mysql / sqlite-wasm,而 environment-variables.mdx 列的是全集 —— 三份清单三种答案。

三条都不在本 PR 的裁决面内(#6265 裁的是「mysql 识别」+「env 值真校验」),顺手改等于未裁扩/收公开配置契约,故不在此 PR 内动。


Generated by Claude Code

@qq9340100
qq9340100 marked this pull request as ready for review August 7, 2026 14:37
@qq9340100
qq9340100 added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit cfb549d Aug 7, 2026
24 checks passed
@qq9340100
qq9340100 deleted the claude/issue-6265-standalone-mysql-and-driver-validation branch August 7, 2026 15:02
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

2 participants