fix: make schema bootstrap mysql safe

This commit is contained in:
john
2026-06-27 21:40:58 +08:00
parent fc5b50c936
commit 0420c42fe2
3 changed files with 104 additions and 7 deletions
+80 -5
View File
@@ -46,6 +46,84 @@ export function createDbPool() {
});
}
export function splitSqlStatements(sql) {
const statements = [];
let current = '';
let quote = null;
let lineComment = false;
let blockComment = false;
for (let i = 0; i < sql.length; i += 1) {
const char = sql[i];
const next = sql[i + 1];
if (lineComment) {
if (char === '\n') {
lineComment = false;
current += '\n';
}
continue;
}
if (blockComment) {
if (char === '*' && next === '/') {
blockComment = false;
i += 1;
}
continue;
}
if (quote) {
current += char;
if (char === '\\' && (quote === '\'' || quote === '"') && next) {
current += next;
i += 1;
continue;
}
if (char === quote) {
if (next === quote && quote !== '`') {
current += next;
i += 1;
} else {
quote = null;
}
}
continue;
}
if (char === '-' && next === '-') {
lineComment = true;
i += 1;
continue;
}
if (char === '/' && next === '*') {
blockComment = true;
i += 1;
continue;
}
if (char === '\'' || char === '"' || char === '`') {
quote = char;
current += char;
continue;
}
if (char === ';') {
const trimmed = current.trim();
if (trimmed) statements.push(trimmed);
current = '';
continue;
}
current += char;
}
const trimmed = current.trim();
if (trimmed) statements.push(trimmed);
return statements;
}
async function columnExists(pool, table, column) {
const [rows] = await pool.query(
`SELECT 1 FROM information_schema.COLUMNS
@@ -589,11 +667,8 @@ export async function migrateSchema(pool) {
export async function initSchema(pool) {
const schemaPath = path.join(__dirname, 'schema.sql');
const sql = fs.readFileSync(schemaPath, 'utf8');
for (const statement of sql.split(';')) {
const trimmed = statement.trim();
if (trimmed) {
await pool.query(trimmed);
}
for (const statement of splitSqlStatements(sql)) {
await pool.query(statement);
}
await migrateSchema(pool);
await ensureDefaultSpaces(pool, {
+22
View File
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { splitSqlStatements } from './db.mjs';
test('splitSqlStatements ignores semicolons in comments and strings', () => {
const statements = splitSqlStatements(`
-- comment with a semicolon; should not split
CREATE TABLE one (
label VARCHAR(64) DEFAULT 'semi;colon'
);
/* block; comment */
CREATE TABLE two (
id BIGINT PRIMARY KEY,
name VARCHAR(64) DEFAULT "two;name"
);
`);
assert.equal(statements.length, 2);
assert.match(statements[0], /^CREATE TABLE one/);
assert.match(statements[1], /^CREATE TABLE two/);
});
+2 -2
View File
@@ -233,7 +233,7 @@ CREATE TABLE IF NOT EXISTS h5_publish_records (
updated_at BIGINT NOT NULL,
KEY idx_h5_publish_route (url_slug, status, published_at),
KEY idx_h5_publish_page (user_id, page_id, published_at),
KEY idx_h5_publish_auto_private (expires_at, user_confirmed_at) WHERE access_mode = 'public' AND user_confirmed_at IS NULL,
KEY idx_h5_publish_auto_private (expires_at, user_confirmed_at),
UNIQUE KEY uq_h5_publish_token_hash (token_hash),
CONSTRAINT fk_h5_publish_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
CONSTRAINT fk_h5_publish_page FOREIGN KEY (page_id) REFERENCES h5_page_records(id) ON DELETE CASCADE,
@@ -1013,7 +1013,7 @@ CREATE TABLE IF NOT EXISTS h5_blocked_words (
-- Shared experience store (etat C): all goosed Worker instances read/write the
-- same learned experience so capability is not siloed per instance. Built on
-- MySQL first with a keyword + recency retrieval; `embedding` is reserved for a
-- MySQL first with a keyword + recency retrieval. `embedding` is reserved for a
-- later move to PostgreSQL + pgvector without changing the calling contract.
CREATE TABLE IF NOT EXISTS h5_experience (
id CHAR(36) PRIMARY KEY,