@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
162 lines (159 loc) • 6.23 kB
JavaScript
import { quoteIdentifier, sqlAffinityForKind, frameworkColumnDdl, columnRef, physicalIndexName } from '@lunora/d1/dialect';
const validatorKindToSqlType = (kind) => sqlAffinityForKind(kind);
const renderColumnDefinition = (name, column) => {
const parts = [quoteIdentifier(name), column.sqlType];
if (!column.nullable) {
parts.push("NOT NULL");
}
return parts.join(" ");
};
const renderCreateTable = (table) => {
const columns = Object.entries(table.columns).map(([columnName, column]) => ` ${renderColumnDefinition(columnName, column)}`);
const lines = [...frameworkColumnDdl().map((column) => ` ${column}`), ...columns].join(",\n");
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(table.name)} (
${lines}
);`;
};
const renderDropTable = (tableName) => `DROP TABLE IF EXISTS ${quoteIdentifier(tableName)};`;
const renderAddColumn = (tableName, columnName, column) => `ALTER TABLE ${quoteIdentifier(tableName)} ADD COLUMN ${renderColumnDefinition(columnName, column)};`;
const renderCreateIndex = (tableName, index) => {
const fields = index.fields.map((field) => columnRef(field)).join(", ");
const uniqueClause = index.unique ? "UNIQUE " : "";
return `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${physicalIndexName(tableName, index.name)} ON ${quoteIdentifier(tableName)} (${fields});`;
};
const renderDropIndex = (tableName, indexName) => `DROP INDEX IF EXISTS ${physicalIndexName(tableName, indexName)};`;
const diffExistingColumn = (tableName, columnName, old, column, unsupported) => {
if (old.sqlType !== column.sqlType) {
unsupported.push({
kind: "columnTypeChange",
summary: `column type change on ${tableName}.${columnName}: ${old.sqlType} → ${column.sqlType} (write SQL manually)`
});
}
if (old.nullable !== column.nullable) {
unsupported.push({
kind: "columnTypeChange",
summary: `nullability change on ${tableName}.${columnName}: ${old.nullable ? "NULL" : "NOT NULL"} → ${column.nullable ? "NULL" : "NOT NULL"} (write SQL manually)`
});
}
};
const diffColumns = (tableName, previous, next, entries, unsupported) => {
for (const [columnName, column] of Object.entries(next)) {
const old = previous[columnName];
if (old === void 0) {
entries.push({
kind: "addColumn",
sql: renderAddColumn(tableName, columnName, column),
summary: `ADD COLUMN ${tableName}.${columnName}`
});
continue;
}
diffExistingColumn(tableName, columnName, old, column, unsupported);
}
for (const columnName of Object.keys(previous)) {
if (next[columnName] === void 0) {
unsupported.push({
kind: "dropColumn",
summary: `DROP COLUMN ${tableName}.${columnName} (SQLite drop-column requires careful migration — write SQL manually)`
});
}
}
};
const diffIndexes = (tableName, previous, next, entries, unsupported) => {
for (const [indexName, index] of Object.entries(next)) {
const old = previous[indexName];
if (old === void 0) {
entries.push({
kind: "createIndex",
sql: renderCreateIndex(tableName, index),
summary: `CREATE INDEX ${indexName} ON ${tableName}`
});
continue;
}
const fieldsEqual = old.fields.length === index.fields.length && old.fields.every((field, i) => field === index.fields[i]);
if (!fieldsEqual || old.unique !== index.unique) {
unsupported.push({
kind: "indexRename",
summary: `index ${indexName} changed on ${tableName} — drop+create manually if intentional`
});
}
}
for (const indexName of Object.keys(previous)) {
if (next[indexName] === void 0) {
entries.push({
kind: "dropIndex",
sql: renderDropIndex(tableName, indexName),
summary: `DROP INDEX ${indexName}`
});
}
}
};
const diffNewTable = (tableName, table, entries) => {
entries.push({
kind: "createTable",
sql: renderCreateTable(table),
summary: `CREATE TABLE ${tableName}`
});
for (const index of Object.values(table.indexes)) {
entries.push({
kind: "createIndex",
sql: renderCreateIndex(tableName, index),
summary: `CREATE INDEX ${index.name} ON ${tableName}`
});
}
};
const diffSnapshots = (previous, next) => {
const previousTables = previous?.tables ?? {};
const entries = [];
const unsupported = [];
for (const [tableName, table] of Object.entries(next.tables)) {
const old = previousTables[tableName];
if (old === void 0) {
diffNewTable(tableName, table, entries);
continue;
}
diffColumns(tableName, old.columns, table.columns, entries, unsupported);
diffIndexes(tableName, old.indexes, table.indexes, entries, unsupported);
}
for (const [tableName] of Object.entries(previousTables)) {
if (next.tables[tableName] === void 0) {
entries.push({
kind: "dropTable",
sql: renderDropTable(tableName),
summary: `DROP TABLE ${tableName}`
});
}
}
return {
empty: entries.length === 0 && unsupported.length === 0,
entries,
unsupported
};
};
const renderMigrationFile = (name, diff, generatedAt) => {
const lines = [
`-- Lunora migration: ${name}`,
`-- Generated at ${generatedAt}`,
"-- This file was produced by `lunora migrate generate`. Review carefully before applying.",
""
];
for (const entry of diff.entries) {
lines.push(`-- ${entry.summary}`, entry.sql, "");
}
if (diff.unsupported.length > 0) {
lines.push(
"-- ---------------------------------------------------------------",
"-- The following deltas are NOT auto-generated in v0.1.",
"-- Write the appropriate SQL below by hand:",
"--"
);
for (const entry of diff.unsupported) {
lines.push(`-- * ${entry.summary}`);
}
lines.push("-- ---------------------------------------------------------------", "");
}
if (diff.empty) {
lines.push("-- No changes detected. Re-running `lunora migrate generate` will overwrite this file.", "");
}
return lines.join("\n");
};
export { diffSnapshots, renderAddColumn, renderCreateIndex, renderCreateTable, renderDropIndex, renderDropTable, renderMigrationFile, validatorKindToSqlType };