UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

125 lines (124 loc) 4.63 kB
import { UnimplementedError } from "../../error/UnimplementedError.js"; import { ChoiceSchema } from "../../schema/ChoiceSchema.js"; import { DateSchema } from "../../schema/DateSchema.js"; import { NumberSchema } from "../../schema/NumberSchema.js"; import { SQLMigrator } from "./SQLMigrator.js"; /** SQLite and D1 migrator using sqlite_master as the schema source of truth. */ export class SQLiteMigrator extends SQLMigrator { async getTables() { const rows = await this.provider.exec ` SELECT ${this.provider.sqlIdentifier("name")} AS ${this.provider.sqlIdentifier("name")} FROM ${this.provider.sqlIdentifier("sqlite_master")} WHERE ${this.provider.sqlIdentifier("type")} = ${"table"} AND ${this.provider.sqlIdentifier("name")} NOT LIKE ${"sqlite_%"} ORDER BY ${this.provider.sqlIdentifier("name")} `; return rows.map(({ name }) => name); } async getTable(name) { const rows = await this.provider.exec ` SELECT ${this.provider.sqlIdentifier("sql")} AS ${this.provider.sqlIdentifier("sql")} FROM ${this.provider.sqlIdentifier("sqlite_master")} WHERE ${this.provider.sqlIdentifier("type")} = ${"table"} AND ${this.provider.sqlIdentifier("name")} = ${name} LIMIT 1 `; const sql = rows[0]?.sql; if (!sql) return undefined; return { name, sql, columns: _getSQLiteColumns(sql) }; } getCreateTableSuffix(_collection) { return " STRICT"; } getDataColumnDefinition() { const data = this.quoteIdentifier("data"); return `TEXT NOT NULL CHECK (json_valid(${data}))`; } getGeneratedColumnDefinition(_columnName, path, definition) { return `${definition} GENERATED ALWAYS AS (json_extract(${this.quoteIdentifier("data")}, ${this.quoteString(path)})) STORED`; } getIDColumnDefinition(collection) { const id = collection.id; if (id instanceof NumberSchema) { if (id.step === 1) return "INTEGER PRIMARY KEY"; throw new UnimplementedError("SQLiteMigrator only supports string and integer identifiers", { received: id }); } if (id instanceof ChoiceSchema || id instanceof DateSchema) return "TEXT PRIMARY KEY"; switch (typeof id.value) { case "string": return "TEXT PRIMARY KEY"; case "number": if (Number.isInteger(id.value)) return "INTEGER PRIMARY KEY"; } return "TEXT PRIMARY KEY"; } getAlterColumnQueries(tableName, from, to) { if (from.name === "id" || from.name === "data") { throw new UnimplementedError(`Cannot alter SQLite column "${from.name}" in existing table "${tableName}"`); } return super.getAlterColumnQueries(tableName, from, to); } } function _getSQLiteColumns(sql) { const body = _getCreateTableBody(sql); const columns = _splitSQLColumns(body).map(entry => { const match = entry.match(/^"((?:[^"]|"")+)"\s+([\s\S]+)$/); if (!match) return undefined; const [, name, statement] = match; if (!name || !statement) return undefined; return { name: name.replaceAll(`""`, `"`), statement: statement.trim() }; }); return Object.fromEntries(columns.filter((column) => !!column).map(column => [column.name, column])); } function _getCreateTableBody(sql) { const start = sql.indexOf("("); if (start < 0) return ""; let depth = 0; for (let index = start; index < sql.length; index += 1) { const char = sql[index]; if (char === "(") depth += 1; if (char === ")") { depth -= 1; if (depth === 0) return sql.slice(start + 1, index); } } return ""; } function _splitSQLColumns(value) { const columns = []; let current = ""; let depth = 0; let quote; for (const char of value) { current += char; if (quote) { if (char === quote) quote = undefined; continue; } if (char === "'" || char === '"') { quote = char; continue; } if (char === "(") depth += 1; else if (char === ")") depth -= 1; else if (char === "," && depth === 0) { columns.push(current.slice(0, -1).trim()); current = ""; } } if (current.trim()) columns.push(current.trim()); return columns.filter(column => column && !/^CONSTRAINT\b/i.test(column)); }