diff --git a/db.mjs b/db.mjs index 27cebe2..93ed4f8 100644 --- a/db.mjs +++ b/db.mjs @@ -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, { diff --git a/db.test.mjs b/db.test.mjs new file mode 100644 index 0000000..93d9f16 --- /dev/null +++ b/db.test.mjs @@ -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/); +}); diff --git a/schema.sql b/schema.sql index b1ca566..a78fbb7 100644 --- a/schema.sql +++ b/schema.sql @@ -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,