From 6eef75b09b2260ab4812673b17e46b2c4f9631da Mon Sep 17 00:00:00 2001 From: zichun Date: Thu, 28 May 2026 15:59:38 +0800 Subject: [PATCH] lib: drop duplicated headers, JSDoc restatements, CLI-entry comments; reuse advanceLiteral helper --- lib/apply-ddl.mjs | 27 ++------------------------- lib/merge-gitignore.mjs | 7 +------ lib/render.mjs | 3 --- lib/validate-ddl.mjs | 61 +++++++++++++++++-------------------------------------------- 4 files changed, 20 insertions(+), 78 deletions(-) diff --git a/lib/apply-ddl.mjs b/lib/apply-ddl.mjs index acbd06b..e2dea69 100644 --- a/lib/apply-ddl.mjs +++ b/lib/apply-ddl.mjs @@ -1,14 +1,3 @@ -// lib/apply-ddl.mjs -// -// Replaces the inline `set -a; . .env.local; mysql < V1.sql` bash from db-init. -// -// parseEnv(): dotenv-style line parser. Pure parsing, NO variable expansion and -// NO shell execution — `$VAR`, backticks, `$(...)` and other shell constructs are -// kept verbatim as literal characters, which eliminates the shell-injection vector -// of `source`-ing an untrusted .env file. -// -// applyDDL(): connects with mysql2/promise (multipleStatements) to run a DDL file. - /** * Parse dotenv-style text into a plain object. * @@ -60,14 +49,6 @@ export function parseEnv(text) { /** * Apply a DDL file to a MySQL database using mysql2/promise. * - * Reads connection settings from the parsed env file. Recognised keys (with - * common aliases) — DB_HOST/MYSQL_HOST, DB_PORT/MYSQL_PORT, DB_USER/MYSQL_USER, - * DB_PASS/DB_PASSWORD/MYSQL_PASSWORD, DB_SCHEMA/DB_NAME/MYSQL_DATABASE. - * DB_SCHEMA is the plugin's canonical schema key (see .env.local template); the - * target database MUST resolve to a non-empty value or applyDDL fails closed — - * V1 DDL contains no `USE`/`CREATE DATABASE`, so a missing schema would only - * surface as an opaque `ER_NO_DB_ERROR` on the first CREATE TABLE. - * * @param {{envPath: string, ddlPath: string}} opts * @returns {Promise} */ @@ -104,9 +85,7 @@ export async function applyDDL({ envPath, ddlPath }) { * Resolve mysql2 connection settings from a parsed env object. Pure (no I/O), * so it is unit-testable without mysql2 installed. * - * fail-closed on missing schema: DB_SCHEMA is the plugin's canonical key; V1 DDL - * has no `USE`/`CREATE DATABASE`, so a missing database would otherwise surface - * only as an opaque ER_NO_DB_ERROR on the first CREATE TABLE. + * Throws if no schema resolves — V1 has no USE/CREATE DATABASE. * * @param {Record} env * @param {string} [envPath] only used to make the error message actionable @@ -132,9 +111,7 @@ export class MysqlUnavailableError extends Error { } } -// CLI entry: node lib/apply-ddl.mjs -// Use pathToFileURL so the guard matches even when the path contains spaces or -// other characters that get percent-encoded in import.meta.url. +// CLI entry guard (see render.mjs for pathToFileURL rationale) const { pathToFileURL } = await import('node:url') if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const [envPath, ddlPath] = process.argv.slice(2) diff --git a/lib/merge-gitignore.mjs b/lib/merge-gitignore.mjs index 7f68573..131969e 100644 --- a/lib/merge-gitignore.mjs +++ b/lib/merge-gitignore.mjs @@ -1,7 +1,5 @@ // lib/merge-gitignore.mjs // 合并两份 .gitignore,对**规则行**逐行判重并集合并;注释行透传(相邻去重),空行丢弃(节由注释头承担)。 -// 之所以不对注释去重:两段分组各自的同名注释头(如多次出现的 `# generated`)是分节标题, -// 全局去重会把第二段的标题吞掉,让 add 文件的规则被并入第一段的注释下、破坏分节语义。 export function mergeGitignore(baseText, addText) { const seenRules = new Set() const out = [] @@ -25,10 +23,7 @@ export function mergeGitignore(baseText, addText) { return text } -// CLI entry: node lib/merge-gitignore.mjs -// Use pathToFileURL so the guard matches even when the path contains spaces or -// non-ASCII chars (import.meta.url is percent-encoded; process.argv[1] is raw). -const { pathToFileURL } = await import('node:url') +const { pathToFileURL } = await import('node:url') // CLI entry guard (see render.mjs) if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const [basePath, addPath] = process.argv.slice(2) const { readFileSync, writeFileSync } = await import('node:fs') diff --git a/lib/render.mjs b/lib/render.mjs index 976d9d2..69054ea 100644 --- a/lib/render.mjs +++ b/lib/render.mjs @@ -1,8 +1,5 @@ // lib/render.mjs — literal-safe template render (replaces scope-lock/render.sh) // -// 用法(CLI):node lib/render.mjs -// 程序内:import { render } from './render.mjs' -// // 核心要求:{{key}} 占位替换;值中含 $、{、}、}} 不被二次解释(字面插入); // 先剥离 HTML 注释(模板引导文本);缺少变量则 throw(不静默留空)。 export function render(template, vars) { diff --git a/lib/validate-ddl.mjs b/lib/validate-ddl.mjs index c875d10..196200e 100644 --- a/lib/validate-ddl.mjs +++ b/lib/validate-ddl.mjs @@ -1,20 +1,10 @@ // lib/validate-ddl.mjs — docs/03 表格 ↔ DDL(V1.sql)一致性 5 维校验 // 替换 db-init/scripts/validate.sh(跨平台、纯 Node、零外部依赖)。 -// 语法基线偏向 MySQL 8(int/varchar/json 等 ANSI + MySQL 类型;KEY/UNIQUE KEY 索引语法)。 -// 厂商扩展(Postgres `bytea`、Oracle `nvarchar2` 等)未列入 SQL_TYPE_RE,下游解析可能退化为 -// 跳过整项(fix #2 起 KEY/INDEX 项遇未知类型保留字会跳过而非误判为列)。 // // 用法(CLI):node lib/validate-ddl.mjs // 退出码 0 = 一致;1 = 存在差异(diff 明细打印到 stderr);2 = 用法/路径错误。 // 程序内:import { parseDocsTables, parseDDL, diffSchema } from './validate-ddl.mjs' // -// 5 维 diff: -// 1) 表集合(missingTables / extraTables) -// 2) 列名(columnMismatches,side: 'docs'|'ddl') -// 3) 列类型(typeMismatches) -// 4) 索引(indexMismatches,side: 'docs'|'ddl') -// 5) 外键(foreignKeyMismatches,side: 'docs'|'ddl') -// // 数据结构(解析结果):Map, indexes: Set, foreignKeys: Set }> @@ -102,10 +92,6 @@ function parseIndexBullet(line, indexes) { // 解析外键 bullet: - `name`: from_col → to_table.to_col (on_delete) // 归一化为 parseDDL 同形的 `${fromCols}->${toTable}(${toCols})`(注意 docs 用 unicode → / DDL 用 ->)。 -// 复合外键的两种合法 docs 写法都支持,避免与 DDL 侧的 `(idA, idB)` 形态不对称: -// - `fk`: colA, colB → other.idA, idB ← 平铺,到列也是逗号分隔 -// - `fk`: colA, colB → other.(idA, idB) ← 目标列括起 -// - `fk`: colA, colB → other.`idA`,`idB` ← 各列各带反引号 function parseForeignKeyBullet(line, foreignKeys) { // 1) 先把头部 `- `name`: ... → table` 抠出来,保留"目标表后剩余的尾段"用于解析目标列(可能是 // `.idA`、`.idA, idB`、`.(idA, idB)` 或 `.`idA`,`idB``)。 @@ -209,8 +195,6 @@ function parseTableBody(body) { indexes.add(`${nameMatch[1]}:${kind}:${cols}`) continue } - // 命名是类型关键字 / 无法定位 → 回退到列定义解析; - // 列正则下游会拒绝以保留字开头的列名(fix #2)。 } // CONSTRAINT 但非外键(如 UNIQUE/CHECK 约束)→ 当索引/约束记 if (/^CONSTRAINT\b/i.test(upper)) { @@ -249,7 +233,6 @@ function extractType(rest) { return base + paren + mod } -// ── 5 维 diff ──────────────────────────────────────────────────── export function diffSchema(docsTables, ddlTables) { const diff = { missingTables: [], // docs 有、DDL 无 @@ -264,8 +247,7 @@ export function diffSchema(docsTables, ddlTables) { const docNames = new Set(docsTables.keys()) const ddlNames = new Set(ddlTables.keys()) - for (const t of docNames) if (!ddlNames.has(t)) diff.missingTables.push(t) - for (const t of ddlNames) if (!docNames.has(t)) diff.extraTables.push(t) + symDiff(docNames, ddlNames, t => diff.missingTables.push(t), t => diff.extraTables.push(t)) diff.missingTables.sort() diff.extraTables.sort() @@ -293,14 +275,14 @@ export function diffSchema(docsTables, ddlTables) { // 表内体现 PK、不在 ### 索引 重列 → 从两侧索引集剔除 PRIMARY,避免假阳性;命名二级索引仍比对。 const dIdx = new Set([...(d.indexes || [])].filter(ix => ix !== 'PRIMARY')) const sIdx = new Set([...(s.indexes || [])].filter(ix => ix !== 'PRIMARY')) - for (const ix of dIdx) if (!sIdx.has(ix)) diff.indexMismatches.push({ table: t, index: ix, side: 'docs' }) - for (const ix of sIdx) if (!dIdx.has(ix)) diff.indexMismatches.push({ table: t, index: ix, side: 'ddl' }) + symDiff(dIdx, sIdx, + ix => diff.indexMismatches.push({ table: t, index: ix, side: 'docs' }), + ix => diff.indexMismatches.push({ table: t, index: ix, side: 'ddl' })) // 维度 5:外键 - const dFk = d.foreignKeys || new Set() - const sFk = s.foreignKeys || new Set() - for (const fk of dFk) if (!sFk.has(fk)) diff.foreignKeyMismatches.push({ table: t, foreignKey: fk, side: 'docs' }) - for (const fk of sFk) if (!dFk.has(fk)) diff.foreignKeyMismatches.push({ table: t, foreignKey: fk, side: 'ddl' }) + symDiff(d.foreignKeys || new Set(), s.foreignKeys || new Set(), + fk => diff.foreignKeyMismatches.push({ table: t, foreignKey: fk, side: 'docs' }), + fk => diff.foreignKeyMismatches.push({ table: t, foreignKey: fk, side: 'ddl' })) } diff.hasDiff = diff.missingTables.length > 0 || diff.extraTables.length > 0 || @@ -309,7 +291,6 @@ export function diffSchema(docsTables, ddlTables) { return diff } -// ── 工具函数 ───────────────────────────────────────────────────── // 剥离 SQL 注释:-- 行注释(到行尾)、# 行注释(到行尾)、/* */ 块注释。 // **字符串字面量感知**:单引号 / 双引号 / 反引号字面量内部的注释符按原文保留(DEFAULT 'a--b' / // DEFAULT '#tag' 之类不会被错剥成"列丢失")。转义引号支持 SQL 标准的 '' 与反斜杠 \\'。 @@ -322,19 +303,9 @@ function stripSqlComments(sql) { const next = s[i + 1] // 进入字符串 / 反引号:原样吐出整个字面量 if (ch === "'" || ch === '"' || ch === '`') { - const q = ch - out += ch - i++ - while (i < s.length) { - const c = s[i] - // SQL 标准的双引号转义:'' 或 "" - if (c === q && s[i + 1] === q) { out += c + c; i += 2; continue } - // 反斜杠转义:\' / \" / \\ 等(MySQL 默认开启 NO_BACKSLASH_ESCAPES 才禁,保守按开启处理) - if (c === '\\' && i + 1 < s.length && q !== '`') { out += c + s[i + 1]; i += 2; continue } - out += c - i++ - if (c === q) break - } + const end = advanceLiteral(s, i) + out += s.slice(i, end) + i = end continue } // /* ... */ 块注释(吞到下一个 */) @@ -453,7 +424,12 @@ function typesEqual(a, b) { return norm(a) === norm(b) } -// ── 报告(供 CLI 与外部复用)──────────────────────────────────── +// 集合对称差:对 left\right 调用 onlyLeft,对 right\left 调用 onlyRight。 +function symDiff(left, right, onlyLeft, onlyRight) { + for (const x of left) if (!right.has(x)) onlyLeft(x) + for (const x of right) if (!left.has(x)) onlyRight(x) +} + export function formatDiff(diff) { const out = [] if (diff.missingTables.length) { @@ -491,10 +467,7 @@ export function formatDiff(diff) { return out.join('\n') } -// ── CLI 入口 ───────────────────────────────────────────────────── -// 用 pathToFileURL 做比较:路径含空格/非 ASCII 时 import.meta.url 是百分号编码, -// 而 process.argv[1] 是原始路径,直接 `file://${argv1}` 拼接永远不相等。 -const { pathToFileURL } = await import('node:url') +const { pathToFileURL } = await import('node:url') // CLI entry guard (see render.mjs) const isCliEntry = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href if (isCliEntry) { const { readFileSync, existsSync } = await import('node:fs') -- libgit2 0.22.2