@korajs/cli 1.0.0-beta.11 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -107
- package/dist/bin.cjs +808 -340
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +588 -174
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-G2PBYMMA.js → chunk-7Y4J2GXP.js} +168 -153
- package/dist/chunk-7Y4J2GXP.js.map +1 -0
- package/dist/{chunk-Z7GCXMGW.js → chunk-TIKGLNER.js} +44 -9
- package/dist/chunk-TIKGLNER.js.map +1 -0
- package/dist/{chunk-4IKCHRJ6.js → chunk-TVIRMDER.js} +122 -112
- package/dist/chunk-TVIRMDER.js.map +1 -0
- package/dist/create.cjs +63 -28
- package/dist/create.cjs.map +1 -1
- package/dist/create.js +2 -2
- package/dist/index.cjs +41 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +8 -2
- package/dist/{studio-server-NIFKZI4F.js → studio-server-XLGZNGG5.js} +11 -2
- package/dist/studio-server-XLGZNGG5.js.map +1 -0
- package/dist/vite.cjs +351 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +161 -0
- package/dist/vite.d.ts +161 -0
- package/dist/vite.js +321 -0
- package/dist/vite.js.map +1 -0
- package/package.json +18 -8
- package/templates/react-basic/AGENTS.md +5 -5
- package/templates/react-basic/vite.config.ts +4 -1
- package/templates/react-sync/AGENTS.md +5 -5
- package/templates/react-sync/server.ts +8 -0
- package/templates/react-sync/src/vite-env.d.ts +1 -0
- package/templates/react-sync/vite.config.ts +4 -1
- package/templates/react-tailwind/AGENTS.md +5 -5
- package/templates/react-tailwind/vite.config.ts +10 -1
- package/templates/react-tailwind-sync/AGENTS.md +9 -7
- package/templates/react-tailwind-sync/server.ts +8 -0
- package/templates/react-tailwind-sync/src/App.tsx +1 -1
- package/templates/react-tailwind-sync/src/kora.ts +32 -0
- package/templates/react-tailwind-sync/src/main.tsx +1 -23
- package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +4 -8
- package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +2 -2
- package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
- package/templates/react-tailwind-sync/src/vite-env.d.ts +1 -0
- package/templates/react-tailwind-sync/vite.config.ts +10 -1
- package/templates/svelte-basic/AGENTS.md +4 -4
- package/templates/svelte-basic/package.json.hbs +6 -0
- package/templates/svelte-basic/src/App.svelte +2 -1
- package/templates/svelte-basic/vite.config.ts +4 -1
- package/templates/svelte-sync/AGENTS.md +4 -4
- package/templates/svelte-sync/server.ts +8 -0
- package/templates/svelte-sync/src/App.svelte +2 -1
- package/templates/svelte-sync/vite.config.ts +4 -1
- package/templates/svelte-tailwind/AGENTS.md +4 -4
- package/templates/svelte-tailwind/package.json.hbs +7 -1
- package/templates/svelte-tailwind/src/App.svelte +2 -1
- package/templates/svelte-tailwind/vite.config.ts +10 -1
- package/templates/svelte-tailwind-sync/AGENTS.md +4 -4
- package/templates/svelte-tailwind-sync/package.json.hbs +1 -1
- package/templates/svelte-tailwind-sync/server.ts +8 -0
- package/templates/svelte-tailwind-sync/src/App.svelte +2 -1
- package/templates/svelte-tailwind-sync/vite.config.ts +10 -1
- package/templates/tauri-react/AGENTS.md +5 -5
- package/templates/tauri-react/package.json.hbs +1 -1
- package/templates/tauri-react/server.ts +8 -0
- package/templates/tauri-react/src/vite-env.d.ts +1 -0
- package/templates/vue-basic/AGENTS.md +4 -4
- package/templates/vue-basic/package.json.hbs +6 -0
- package/templates/vue-basic/vite.config.ts +4 -1
- package/templates/vue-sync/AGENTS.md +4 -4
- package/templates/vue-sync/server.ts +8 -0
- package/templates/vue-sync/vite.config.ts +4 -1
- package/templates/vue-tailwind/AGENTS.md +4 -4
- package/templates/vue-tailwind/package.json.hbs +6 -0
- package/templates/vue-tailwind/vite.config.ts +4 -1
- package/templates/vue-tailwind-sync/AGENTS.md +4 -4
- package/templates/vue-tailwind-sync/server.ts +8 -0
- package/templates/vue-tailwind-sync/vite.config.ts +4 -1
- package/dist/chunk-4IKCHRJ6.js.map +0 -1
- package/dist/chunk-G2PBYMMA.js.map +0 -1
- package/dist/chunk-Z7GCXMGW.js.map +0 -1
- package/dist/studio-server-NIFKZI4F.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
createCommand
|
|
4
|
-
} from "./chunk-Z7GCXMGW.js";
|
|
5
2
|
import {
|
|
6
3
|
agentsCommand,
|
|
7
4
|
deployCommand,
|
|
8
5
|
generateTypes
|
|
9
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TVIRMDER.js";
|
|
7
|
+
import {
|
|
8
|
+
createCommand
|
|
9
|
+
} from "./chunk-TIKGLNER.js";
|
|
10
10
|
import {
|
|
11
11
|
DevServerError,
|
|
12
12
|
InvalidProjectError,
|
|
13
13
|
SchemaNotFoundError,
|
|
14
14
|
createLogger,
|
|
15
15
|
promptConfirm
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7Y4J2GXP.js";
|
|
17
17
|
import {
|
|
18
18
|
loadSchemaDefinition
|
|
19
19
|
} from "./chunk-6C4BHSRA.js";
|
|
@@ -1620,8 +1620,8 @@ function isBreakingChange(change) {
|
|
|
1620
1620
|
if (change.type === "field-changed") {
|
|
1621
1621
|
if (change.before.kind !== change.after.kind) return true;
|
|
1622
1622
|
if (change.before.itemKind !== change.after.itemKind) return true;
|
|
1623
|
-
|
|
1624
|
-
|
|
1623
|
+
const kept = new Set(change.after.enumValues ?? []);
|
|
1624
|
+
if ((change.before.enumValues ?? []).some((value) => !kept.has(value))) return true;
|
|
1625
1625
|
if (change.before.required !== change.after.required && change.after.required) return true;
|
|
1626
1626
|
return false;
|
|
1627
1627
|
}
|
|
@@ -1650,22 +1650,484 @@ function compareChanges(left, right) {
|
|
|
1650
1650
|
return 0;
|
|
1651
1651
|
}
|
|
1652
1652
|
|
|
1653
|
+
// src/commands/migrate/table-evolution-directive.ts
|
|
1654
|
+
import {
|
|
1655
|
+
collectionIndexName,
|
|
1656
|
+
isKoraInternalColumn,
|
|
1657
|
+
parseSqliteCheckConstraints,
|
|
1658
|
+
readSqliteTableCatalog,
|
|
1659
|
+
sqlDefaultLiteral,
|
|
1660
|
+
sqliteCheckConstraintDefinition,
|
|
1661
|
+
sqliteCheckReferencesColumn
|
|
1662
|
+
} from "@korajs/core/internal";
|
|
1663
|
+
var EVOLVE_TABLE_DIRECTIVE = "--kora:evolve-table";
|
|
1664
|
+
function evolveFieldSpec(descriptor) {
|
|
1665
|
+
return {
|
|
1666
|
+
kind: descriptor.kind,
|
|
1667
|
+
...descriptor.itemKind ? { itemKind: descriptor.itemKind } : {},
|
|
1668
|
+
...descriptor.enumValues ? { enumValues: [...descriptor.enumValues] } : {},
|
|
1669
|
+
...descriptor.defaultValue !== void 0 ? { default: descriptor.defaultValue } : {},
|
|
1670
|
+
...descriptor.auto ? { auto: true } : {}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function formatEvolveTableDirective(target) {
|
|
1674
|
+
return `${EVOLVE_TABLE_DIRECTIVE} ${JSON.stringify(target)}`;
|
|
1675
|
+
}
|
|
1676
|
+
function parseEvolveTableDirective(statement) {
|
|
1677
|
+
const trimmed = statement.trimStart();
|
|
1678
|
+
if (!trimmed.startsWith(EVOLVE_TABLE_DIRECTIVE)) return null;
|
|
1679
|
+
const payload = trimmed.slice(EVOLVE_TABLE_DIRECTIVE.length).trim();
|
|
1680
|
+
let parsed;
|
|
1681
|
+
try {
|
|
1682
|
+
parsed = JSON.parse(payload);
|
|
1683
|
+
} catch {
|
|
1684
|
+
parsed = null;
|
|
1685
|
+
}
|
|
1686
|
+
const target = parsed;
|
|
1687
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1688
|
+
const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1689
|
+
const isSpec = (value) => isRecord(value) && typeof value.kind === "string";
|
|
1690
|
+
if (!isRecord(target) || typeof target.table !== "string" || !isRecord(target.add) || !Object.values(target.add).every(isSpec) || !isStrings(target.drop) || !isRecord(target.change) || !Object.values(target.change).every(
|
|
1691
|
+
(change) => isRecord(change) && isSpec(change.from) && isSpec(change.to)
|
|
1692
|
+
) || !isStrings(target.addIndexes) || !isStrings(target.removeIndexes)) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Malformed migration directive "${statement}": expected ${EVOLVE_TABLE_DIRECTIVE} {"table", "add", "drop", "change", "addIndexes", "removeIndexes"}. Regenerate the migration with \`kora migrate\`.`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
table: target.table,
|
|
1699
|
+
add: target.add,
|
|
1700
|
+
drop: [...target.drop],
|
|
1701
|
+
change: target.change,
|
|
1702
|
+
addIndexes: [...target.addIndexes],
|
|
1703
|
+
removeIndexes: [...target.removeIndexes]
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async function expandEvolveTableForSqlite(target, query, now) {
|
|
1707
|
+
const catalog = await readSqliteTableCatalog(query, target.table);
|
|
1708
|
+
if (!catalog) return [];
|
|
1709
|
+
const table = quoteIdentifier(target.table);
|
|
1710
|
+
const existing = new Map(
|
|
1711
|
+
catalog.columns.map((column) => [column.name, column.type.toUpperCase()])
|
|
1712
|
+
);
|
|
1713
|
+
const plan = planColumns(target, existing, sqliteType);
|
|
1714
|
+
const statements = [];
|
|
1715
|
+
const presentIndexes = await indexColumnsByName(query, target.table);
|
|
1716
|
+
const removedIndexNames = new Set(
|
|
1717
|
+
target.removeIndexes.flatMap((field) => koraIndexNames(target.table, field)).filter((name) => presentIndexes.has(name))
|
|
1718
|
+
);
|
|
1719
|
+
for (const name of removedIndexNames) {
|
|
1720
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(name)}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (plan.drop.length > 0 || plan.change.length > 0) {
|
|
1723
|
+
statements.push(
|
|
1724
|
+
...await sqliteRebuildStatements(catalog, plan, query, removedIndexNames, now)
|
|
1725
|
+
);
|
|
1726
|
+
} else {
|
|
1727
|
+
for (const [field, spec] of plan.add) {
|
|
1728
|
+
statements.push(
|
|
1729
|
+
`ALTER TABLE ${table} ADD COLUMN ${quoteIdentifier(field)} ${sqliteType(spec)}${defaultClause(spec)}`
|
|
1730
|
+
);
|
|
1731
|
+
if (spec.auto && spec.kind === "timestamp" && spec.default === void 0) {
|
|
1732
|
+
statements.push(`UPDATE ${table} SET ${quoteIdentifier(field)} = ${now}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const indexed = await singleColumnIndexes(query, target.table, removedIndexNames);
|
|
1737
|
+
for (const field of target.addIndexes) {
|
|
1738
|
+
if (indexed.has(field)) continue;
|
|
1739
|
+
statements.push(
|
|
1740
|
+
`CREATE INDEX IF NOT EXISTS ${koraIndexNames(target.table, field)[0]} ON ${table} (${quoteIdentifier(field)})`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
return statements;
|
|
1744
|
+
}
|
|
1745
|
+
async function expandEvolveTableForPostgres(target, query, now) {
|
|
1746
|
+
const columns = await query(
|
|
1747
|
+
`SELECT column_name::text AS name, upper(data_type::text) AS type FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ${sqlText(target.table)}`
|
|
1748
|
+
);
|
|
1749
|
+
if (columns.length === 0) return [];
|
|
1750
|
+
const table = quoteIdentifier(target.table);
|
|
1751
|
+
const existing = new Map(columns.map((row) => [String(row.name), String(row.type)]));
|
|
1752
|
+
const plan = planColumns(target, existing, postgresType);
|
|
1753
|
+
const statements = [];
|
|
1754
|
+
const indexNames = new Set(
|
|
1755
|
+
(await query(
|
|
1756
|
+
`SELECT indexname::text AS name FROM pg_indexes WHERE schemaname = current_schema() AND tablename = ${sqlText(target.table)}`
|
|
1757
|
+
)).map((row) => String(row.name))
|
|
1758
|
+
);
|
|
1759
|
+
for (const field of target.removeIndexes) {
|
|
1760
|
+
const [serverName, clientName] = koraIndexNames(target.table, field);
|
|
1761
|
+
if (indexNames.has(serverName.toLowerCase())) {
|
|
1762
|
+
statements.push(`DROP INDEX IF EXISTS ${serverName}`);
|
|
1763
|
+
}
|
|
1764
|
+
if (indexNames.has(clientName)) {
|
|
1765
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(clientName)}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
for (const field of plan.drop) {
|
|
1769
|
+
statements.push(`ALTER TABLE ${table} DROP COLUMN IF EXISTS ${quoteIdentifier(field)}`);
|
|
1770
|
+
}
|
|
1771
|
+
for (const [field, change] of plan.change) {
|
|
1772
|
+
const column = quoteIdentifier(field);
|
|
1773
|
+
const type = postgresType(change.to);
|
|
1774
|
+
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);
|
|
1775
|
+
statements.push(
|
|
1776
|
+
`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${type} USING CAST((${postgresProjection(column, change.from, change.to, now)}) AS ${type})`
|
|
1777
|
+
);
|
|
1778
|
+
if (change.to.default !== void 0) {
|
|
1779
|
+
statements.push(
|
|
1780
|
+
`ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${sqlDefaultLiteral(change.to.default)}`
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
for (const [field, spec] of plan.add) {
|
|
1785
|
+
statements.push(
|
|
1786
|
+
`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${quoteIdentifier(field)} ${postgresType(spec)}${defaultClause(spec)}`
|
|
1787
|
+
);
|
|
1788
|
+
if (spec.auto && spec.kind === "timestamp" && spec.default === void 0) {
|
|
1789
|
+
statements.push(`UPDATE ${table} SET ${quoteIdentifier(field)} = ${now}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
for (const field of target.addIndexes) {
|
|
1793
|
+
const name = koraIndexNames(target.table, field)[0];
|
|
1794
|
+
if (indexNames.has(name.toLowerCase())) continue;
|
|
1795
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${name} ON ${table} (${quoteIdentifier(field)})`);
|
|
1796
|
+
}
|
|
1797
|
+
return statements;
|
|
1798
|
+
}
|
|
1799
|
+
function planColumns(target, existing, typeOf) {
|
|
1800
|
+
const plan = { add: [], drop: [], change: [] };
|
|
1801
|
+
for (const [field, spec] of Object.entries(target.add)) {
|
|
1802
|
+
if (!existing.has(field)) plan.add.push([field, spec]);
|
|
1803
|
+
}
|
|
1804
|
+
for (const [field, change] of Object.entries(target.change)) {
|
|
1805
|
+
const current = existing.get(field);
|
|
1806
|
+
if (current === void 0) {
|
|
1807
|
+
plan.add.push([field, change.to]);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const from = typeOf(change.from);
|
|
1811
|
+
const to = typeOf(change.to);
|
|
1812
|
+
if (from !== to && current === to) continue;
|
|
1813
|
+
plan.change.push([field, change]);
|
|
1814
|
+
}
|
|
1815
|
+
for (const field of target.drop) {
|
|
1816
|
+
if (existing.has(field) && !(field in target.add) && !(field in target.change)) {
|
|
1817
|
+
if (isKoraInternalColumn(field)) {
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`Migration directive refuses to drop Kora's own column "${field}" of "${target.table}".`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
plan.drop.push(field);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return plan;
|
|
1826
|
+
}
|
|
1827
|
+
async function sqliteRebuildStatements(catalog, plan, query, removedIndexNames, now) {
|
|
1828
|
+
const table = catalog.table;
|
|
1829
|
+
const temp = `_kora_evolve_${table}`;
|
|
1830
|
+
const dropped = new Set(plan.drop);
|
|
1831
|
+
const changed = new Map(plan.change);
|
|
1832
|
+
const ordered = [...catalog.columns].sort((a, b) => a.cid - b.cid).filter((column) => !dropped.has(column.name));
|
|
1833
|
+
const pkColumns = ordered.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk);
|
|
1834
|
+
const fkGroups = /* @__PURE__ */ new Map();
|
|
1835
|
+
for (const fk of catalog.foreignKeys) {
|
|
1836
|
+
const group = fkGroups.get(fk.id) ?? [];
|
|
1837
|
+
group.push(fk);
|
|
1838
|
+
fkGroups.set(fk.id, group);
|
|
1839
|
+
}
|
|
1840
|
+
const columnReferences = /* @__PURE__ */ new Map();
|
|
1841
|
+
const tableConstraints = [];
|
|
1842
|
+
for (const group of fkGroups.values()) {
|
|
1843
|
+
group.sort((a, b) => a.seq - b.seq);
|
|
1844
|
+
const first = group[0];
|
|
1845
|
+
if (!first || group.some((fk) => dropped.has(fk.from))) continue;
|
|
1846
|
+
const actions = foreignKeyActions(first);
|
|
1847
|
+
const targetRef = group.every((fk) => fk.to !== null && fk.to !== "") ? `${quoteIdentifier(first.table)}(${group.map((fk) => quoteIdentifier(fk.to ?? "")).join(", ")})` : quoteIdentifier(first.table);
|
|
1848
|
+
if (group.length === 1) {
|
|
1849
|
+
columnReferences.set(first.from, `REFERENCES ${targetRef}${actions}`);
|
|
1850
|
+
} else {
|
|
1851
|
+
tableConstraints.push(
|
|
1852
|
+
`FOREIGN KEY (${group.map((fk) => quoteIdentifier(fk.from)).join(", ")}) REFERENCES ${targetRef}${actions}`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (pkColumns.length > 1) {
|
|
1857
|
+
tableConstraints.unshift(
|
|
1858
|
+
`PRIMARY KEY (${pkColumns.map((c) => quoteIdentifier(c.name)).join(", ")})`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
for (const unique of catalog.uniqueConstraints) {
|
|
1862
|
+
if (unique.some((column) => dropped.has(column))) continue;
|
|
1863
|
+
tableConstraints.push(`UNIQUE (${unique.map(quoteIdentifier).join(", ")})`);
|
|
1864
|
+
}
|
|
1865
|
+
for (const check of parseSqliteCheckConstraints(catalog.sql)) {
|
|
1866
|
+
if (sqliteCheckReferencesColumn(check, dropped)) continue;
|
|
1867
|
+
tableConstraints.push(sqliteCheckConstraintDefinition(check));
|
|
1868
|
+
}
|
|
1869
|
+
const definitions = [];
|
|
1870
|
+
const targetColumns = [];
|
|
1871
|
+
const selections = [];
|
|
1872
|
+
for (const column of ordered) {
|
|
1873
|
+
const change = changed.get(column.name);
|
|
1874
|
+
const parts = [quoteIdentifier(column.name)];
|
|
1875
|
+
if (change) {
|
|
1876
|
+
parts.push(sqliteType(change.to));
|
|
1877
|
+
} else if (column.type) {
|
|
1878
|
+
parts.push(column.type);
|
|
1879
|
+
}
|
|
1880
|
+
if (pkColumns.length === 1 && column.pk === 1) parts.push("PRIMARY KEY");
|
|
1881
|
+
if (column.notnull === 1) parts.push("NOT NULL");
|
|
1882
|
+
if (change) {
|
|
1883
|
+
if (change.to.default !== void 0) {
|
|
1884
|
+
parts.push(`DEFAULT ${sqlDefaultLiteral(change.to.default)}`);
|
|
1885
|
+
}
|
|
1886
|
+
} else if (column.dflt_value !== null) {
|
|
1887
|
+
parts.push(`DEFAULT (${column.dflt_value})`);
|
|
1888
|
+
}
|
|
1889
|
+
const reference = columnReferences.get(column.name);
|
|
1890
|
+
if (reference) parts.push(reference);
|
|
1891
|
+
definitions.push(parts.join(" "));
|
|
1892
|
+
targetColumns.push(quoteIdentifier(column.name));
|
|
1893
|
+
selections.push(
|
|
1894
|
+
change ? sqliteProjection(quoteIdentifier(column.name), change.from, change.to, now) : quoteIdentifier(column.name)
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
for (const [field, spec] of plan.add) {
|
|
1898
|
+
definitions.push(`${quoteIdentifier(field)} ${sqliteType(spec)}${defaultClause(spec)}`);
|
|
1899
|
+
targetColumns.push(quoteIdentifier(field));
|
|
1900
|
+
selections.push(fallbackLiteral(spec, now));
|
|
1901
|
+
}
|
|
1902
|
+
const indexColumns = await indexColumnsByName(query, table);
|
|
1903
|
+
const dependents = [];
|
|
1904
|
+
for (const row of await query(
|
|
1905
|
+
`SELECT type, name, sql FROM sqlite_master WHERE type IN ('index', 'trigger') AND tbl_name = ${sqlText(table)} AND sql IS NOT NULL ORDER BY type, name`
|
|
1906
|
+
)) {
|
|
1907
|
+
const name = String(row.name);
|
|
1908
|
+
if (row.type === "index") {
|
|
1909
|
+
if (removedIndexNames.has(name)) continue;
|
|
1910
|
+
if ((indexColumns.get(name) ?? []).some((column) => dropped.has(column))) continue;
|
|
1911
|
+
}
|
|
1912
|
+
dependents.push(String(row.sql));
|
|
1913
|
+
}
|
|
1914
|
+
return [
|
|
1915
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(temp)}`,
|
|
1916
|
+
`CREATE TABLE ${quoteIdentifier(temp)} (
|
|
1917
|
+
${[...definitions, ...tableConstraints].join(",\n ")}
|
|
1918
|
+
)`,
|
|
1919
|
+
`INSERT INTO ${quoteIdentifier(temp)} (${targetColumns.join(", ")}) SELECT ${selections.join(", ")} FROM ${quoteIdentifier(table)}`,
|
|
1920
|
+
`DROP TABLE ${quoteIdentifier(table)}`,
|
|
1921
|
+
`ALTER TABLE ${quoteIdentifier(temp)} RENAME TO ${quoteIdentifier(table)}`,
|
|
1922
|
+
...dependents
|
|
1923
|
+
];
|
|
1924
|
+
}
|
|
1925
|
+
async function indexColumnsByName(query, table) {
|
|
1926
|
+
const out = /* @__PURE__ */ new Map();
|
|
1927
|
+
for (const index of await query(`SELECT name FROM pragma_index_list(${sqlText(table)})`)) {
|
|
1928
|
+
const name = String(index.name);
|
|
1929
|
+
const info = await query(`SELECT name FROM pragma_index_info(${sqlText(name)}) ORDER BY seqno`);
|
|
1930
|
+
out.set(
|
|
1931
|
+
name,
|
|
1932
|
+
info.filter((row) => typeof row.name === "string").map((row) => String(row.name))
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
return out;
|
|
1936
|
+
}
|
|
1937
|
+
async function singleColumnIndexes(query, table, ignore) {
|
|
1938
|
+
const out = /* @__PURE__ */ new Set();
|
|
1939
|
+
for (const [name, columns] of await indexColumnsByName(query, table)) {
|
|
1940
|
+
if (ignore.has(name)) continue;
|
|
1941
|
+
const only = columns[0];
|
|
1942
|
+
if (columns.length === 1 && only !== void 0) out.add(only);
|
|
1943
|
+
}
|
|
1944
|
+
return out;
|
|
1945
|
+
}
|
|
1946
|
+
function koraIndexNames(table, field) {
|
|
1947
|
+
quoteIdentifier(table);
|
|
1948
|
+
quoteIdentifier(field);
|
|
1949
|
+
return [`idx_${table}_${field}`, collectionIndexName(table, field)];
|
|
1950
|
+
}
|
|
1951
|
+
function defaultClause(spec) {
|
|
1952
|
+
return spec.default !== void 0 ? ` DEFAULT ${sqlDefaultLiteral(spec.default)}` : "";
|
|
1953
|
+
}
|
|
1954
|
+
function sqliteType(spec) {
|
|
1955
|
+
switch (spec.kind) {
|
|
1956
|
+
case "number":
|
|
1957
|
+
return "REAL";
|
|
1958
|
+
case "boolean":
|
|
1959
|
+
case "timestamp":
|
|
1960
|
+
return "INTEGER";
|
|
1961
|
+
case "richtext":
|
|
1962
|
+
return "BLOB";
|
|
1963
|
+
default:
|
|
1964
|
+
return "TEXT";
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function postgresType(spec) {
|
|
1968
|
+
switch (spec.kind) {
|
|
1969
|
+
case "number":
|
|
1970
|
+
return "DOUBLE PRECISION";
|
|
1971
|
+
case "boolean":
|
|
1972
|
+
return "INTEGER";
|
|
1973
|
+
case "timestamp":
|
|
1974
|
+
return "BIGINT";
|
|
1975
|
+
case "array":
|
|
1976
|
+
case "object":
|
|
1977
|
+
case "json":
|
|
1978
|
+
return "JSONB";
|
|
1979
|
+
case "richtext":
|
|
1980
|
+
return "BYTEA";
|
|
1981
|
+
default:
|
|
1982
|
+
return "TEXT";
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function fallbackLiteral(target, now) {
|
|
1986
|
+
if (target.auto && target.kind === "timestamp") return String(now);
|
|
1987
|
+
if (target.default !== void 0) return sqlDefaultLiteral(target.default);
|
|
1988
|
+
return "NULL";
|
|
1989
|
+
}
|
|
1990
|
+
var NUMERIC_SOURCES = /* @__PURE__ */ new Set(["number", "timestamp", "boolean"]);
|
|
1991
|
+
var TEXT_SOURCES = /* @__PURE__ */ new Set(["string", "enum"]);
|
|
1992
|
+
var TRUE_WORDS = "('1','true','t','yes','y','on')";
|
|
1993
|
+
var FALSE_WORDS = "('0','false','f','no','n','off')";
|
|
1994
|
+
function sqliteProjection(column, source, target, now) {
|
|
1995
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) return column;
|
|
1996
|
+
const fallback = fallbackLiteral(target, now);
|
|
1997
|
+
if (target.kind === "string") return `CAST(${column} AS TEXT)`;
|
|
1998
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
1999
|
+
if (NUMERIC_SOURCES.has(source.kind) || TEXT_SOURCES.has(source.kind)) {
|
|
2000
|
+
const castType = target.kind === "number" ? "REAL" : "INTEGER";
|
|
2001
|
+
return `CASE WHEN ${column} IS NULL THEN NULL ELSE CAST(${column} AS ${castType}) END`;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (target.kind === "boolean") {
|
|
2005
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2006
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN CAST(${column} AS REAL) = 0 THEN 0 ELSE 1 END`;
|
|
2007
|
+
}
|
|
2008
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2009
|
+
const word = `LOWER(TRIM(CAST(${column} AS TEXT)))`;
|
|
2010
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${word} IN ${TRUE_WORDS} THEN 1 WHEN ${word} IN ${FALSE_WORDS} THEN 0 ELSE ${fallback} END`;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
2014
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2015
|
+
const allowed = target.enumValues.map((value) => sqlDefaultLiteral(value)).join(", ");
|
|
2016
|
+
return `CASE WHEN ${column} IN (${allowed}) THEN ${column} ELSE ${fallback} END`;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return fallback;
|
|
2020
|
+
}
|
|
2021
|
+
function postgresProjection(column, source, target, now) {
|
|
2022
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) return column;
|
|
2023
|
+
const fallback = fallbackLiteral(target, now);
|
|
2024
|
+
const text = `(${column})::text`;
|
|
2025
|
+
if (target.kind === "string") return text;
|
|
2026
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
2027
|
+
const toTarget = (value) => target.kind === "number" ? value : `round(${value})::bigint`;
|
|
2028
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2029
|
+
return `CASE WHEN ${column} IS NULL THEN NULL ELSE ${toTarget(`(${column})::double precision`)} END`;
|
|
2030
|
+
}
|
|
2031
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2032
|
+
const trimmed = `btrim(${text})`;
|
|
2033
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${trimmed} ~ '^[-+]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?$' THEN ${toTarget(`(${trimmed})::double precision`)} ELSE ${fallback} END`;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (target.kind === "boolean") {
|
|
2037
|
+
if (NUMERIC_SOURCES.has(source.kind)) {
|
|
2038
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN (${column})::double precision = 0 THEN 0 ELSE 1 END`;
|
|
2039
|
+
}
|
|
2040
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2041
|
+
const word = `lower(btrim(${text}))`;
|
|
2042
|
+
return `CASE WHEN ${column} IS NULL THEN NULL WHEN ${word} IN ${TRUE_WORDS} THEN 1 WHEN ${word} IN ${FALSE_WORDS} THEN 0 ELSE ${fallback} END`;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
2046
|
+
if (TEXT_SOURCES.has(source.kind)) {
|
|
2047
|
+
const allowed = target.enumValues.map((value) => sqlDefaultLiteral(value)).join(", ");
|
|
2048
|
+
return `CASE WHEN ${text} IN (${allowed}) THEN ${text} ELSE ${fallback} END`;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return fallback;
|
|
2052
|
+
}
|
|
2053
|
+
function foreignKeyActions(fk) {
|
|
2054
|
+
let out = "";
|
|
2055
|
+
const onDelete = fk.on_delete?.toUpperCase();
|
|
2056
|
+
const onUpdate = fk.on_update?.toUpperCase();
|
|
2057
|
+
if (onDelete && onDelete !== "NO ACTION") out += ` ON DELETE ${onDelete}`;
|
|
2058
|
+
if (onUpdate && onUpdate !== "NO ACTION") out += ` ON UPDATE ${onUpdate}`;
|
|
2059
|
+
return out;
|
|
2060
|
+
}
|
|
2061
|
+
function sqlText(value) {
|
|
2062
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2063
|
+
}
|
|
2064
|
+
function quoteIdentifier(identifier) {
|
|
2065
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
|
|
2066
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
2067
|
+
}
|
|
2068
|
+
return `"${identifier}"`;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/commands/migrate/value-domain-directive.ts
|
|
2072
|
+
import {
|
|
2073
|
+
planPostgresConstraintRelaxation,
|
|
2074
|
+
planSqliteConstraintRelaxation
|
|
2075
|
+
} from "@korajs/core/internal";
|
|
2076
|
+
var RELAX_VALUE_DOMAIN_DIRECTIVE = "--kora:relax-value-domain";
|
|
2077
|
+
function formatRelaxValueDomainDirective(target) {
|
|
2078
|
+
return `${RELAX_VALUE_DOMAIN_DIRECTIVE} ${JSON.stringify(target)}`;
|
|
2079
|
+
}
|
|
2080
|
+
function parseRelaxValueDomainDirective(statement) {
|
|
2081
|
+
const trimmed = statement.trimStart();
|
|
2082
|
+
if (!trimmed.startsWith(RELAX_VALUE_DOMAIN_DIRECTIVE)) return null;
|
|
2083
|
+
const payload = trimmed.slice(RELAX_VALUE_DOMAIN_DIRECTIVE.length).trim();
|
|
2084
|
+
let parsed;
|
|
2085
|
+
try {
|
|
2086
|
+
parsed = JSON.parse(payload);
|
|
2087
|
+
} catch {
|
|
2088
|
+
parsed = null;
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.table !== "string" || !Array.isArray(parsed.fields) || !parsed.fields.every((field) => typeof field === "string") || parsed.enums !== void 0 && (!Array.isArray(parsed.enums) || !parsed.enums.every((field) => typeof field === "string"))) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Malformed migration directive "${statement}": expected ${RELAX_VALUE_DOMAIN_DIRECTIVE} {"table": "...", "fields": ["..."]}. Regenerate the migration with \`kora migrate\`.`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
const target = parsed;
|
|
2096
|
+
return {
|
|
2097
|
+
table: target.table,
|
|
2098
|
+
fields: [...target.fields],
|
|
2099
|
+
...target.enums !== void 0 ? { enums: [...target.enums] } : {}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async function expandRelaxValueDomainForSqlite(target, query) {
|
|
2103
|
+
return planSqliteConstraintRelaxation(query, [target.table], {
|
|
2104
|
+
[target.table]: target.enums ?? target.fields
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
async function expandRelaxValueDomainForPostgres(target, query) {
|
|
2108
|
+
return planPostgresConstraintRelaxation(
|
|
2109
|
+
query,
|
|
2110
|
+
{ [target.table]: target.fields },
|
|
2111
|
+
target.enums !== void 0 ? { [target.table]: target.enums } : {}
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1653
2115
|
// src/commands/migrate/migration-generator.ts
|
|
1654
2116
|
function generateMigration(previous, current, diff) {
|
|
1655
2117
|
const up = [];
|
|
1656
|
-
const
|
|
2118
|
+
const downGroups = [];
|
|
1657
2119
|
for (const change of diff.changes) {
|
|
1658
2120
|
if (change.type === "collection-added") {
|
|
1659
2121
|
const collectionDef = current.collections[change.collection];
|
|
1660
2122
|
if (!collectionDef) continue;
|
|
1661
2123
|
up.push(...generateSQL(change.collection, collectionDef));
|
|
1662
|
-
|
|
2124
|
+
downGroups.push(dropCollectionStatements(change.collection));
|
|
1663
2125
|
}
|
|
1664
2126
|
if (change.type === "collection-removed") {
|
|
1665
2127
|
const collectionDef = previous.collections[change.collection];
|
|
1666
2128
|
up.push(...dropCollectionStatements(change.collection));
|
|
1667
2129
|
if (collectionDef) {
|
|
1668
|
-
|
|
2130
|
+
downGroups.push(generateSQL(change.collection, collectionDef));
|
|
1669
2131
|
}
|
|
1670
2132
|
}
|
|
1671
2133
|
}
|
|
@@ -1678,48 +2140,59 @@ function generateMigration(previous, current, diff) {
|
|
|
1678
2140
|
const previousDef = previous.collections[collection];
|
|
1679
2141
|
const currentDef = current.collections[collection];
|
|
1680
2142
|
if (!previousDef || !currentDef) continue;
|
|
2143
|
+
const valueDomainOnly = diff.changes.every(
|
|
2144
|
+
(change) => change.collection !== collection || change.type === "field-changed" && isValueDomainChange(change.before, change.after)
|
|
2145
|
+
);
|
|
2146
|
+
if (valueDomainOnly) {
|
|
2147
|
+
const directive = relaxValueDomainDirective(collection, previousDef, currentDef);
|
|
2148
|
+
up.push(directive);
|
|
2149
|
+
downGroups.push([directive]);
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
1681
2152
|
validateRebuildSafety(collection, previousDef, currentDef);
|
|
1682
|
-
|
|
1683
|
-
|
|
2153
|
+
const relax = relaxValueDomainDirective(collection, previousDef, currentDef);
|
|
2154
|
+
up.push(relax, formatEvolveTableDirective(evolveTarget(collection, previousDef, currentDef)));
|
|
2155
|
+
downGroups.push([
|
|
2156
|
+
relax,
|
|
2157
|
+
formatEvolveTableDirective(evolveTarget(collection, currentDef, previousDef))
|
|
2158
|
+
]);
|
|
1684
2159
|
}
|
|
1685
|
-
down.reverse();
|
|
1686
2160
|
return {
|
|
1687
2161
|
up,
|
|
1688
|
-
down,
|
|
2162
|
+
down: downGroups.reverse().flat(),
|
|
1689
2163
|
summary: diff.changes.map(formatChange),
|
|
1690
2164
|
containsBreakingChanges: diff.hasBreakingChanges
|
|
1691
2165
|
};
|
|
1692
2166
|
}
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
statements.push(
|
|
1713
|
-
`INSERT INTO ${tempTable} (${columns.map(quoteIdentifier).join(", ")}) SELECT ${selectExpressions.join(", ")} FROM ${table}`
|
|
1714
|
-
);
|
|
1715
|
-
statements.push(`DROP TABLE ${table}`);
|
|
1716
|
-
statements.push(`ALTER TABLE ${tempTable} RENAME TO ${table}`);
|
|
1717
|
-
for (const indexField of to.indexes) {
|
|
1718
|
-
statements.push(
|
|
1719
|
-
`CREATE INDEX IF NOT EXISTS idx_${collection}_${indexField} ON ${table} (${quoteIdentifier(indexField)})`
|
|
1720
|
-
);
|
|
2167
|
+
function isValueDomainChange(before, after) {
|
|
2168
|
+
return before.kind === after.kind && before.itemKind === after.itemKind && before.auto === after.auto;
|
|
2169
|
+
}
|
|
2170
|
+
function evolveTarget(collection, from, to) {
|
|
2171
|
+
const target = {
|
|
2172
|
+
table: collection,
|
|
2173
|
+
add: {},
|
|
2174
|
+
drop: [],
|
|
2175
|
+
change: {},
|
|
2176
|
+
addIndexes: to.indexes.filter((field) => !from.indexes.includes(field)),
|
|
2177
|
+
removeIndexes: from.indexes.filter((field) => !to.indexes.includes(field))
|
|
2178
|
+
};
|
|
2179
|
+
for (const [field, descriptor] of Object.entries(to.fields)) {
|
|
2180
|
+
const before = from.fields[field];
|
|
2181
|
+
if (!before) {
|
|
2182
|
+
target.add[field] = evolveFieldSpec(descriptor);
|
|
2183
|
+
} else if (before.kind !== descriptor.kind || before.itemKind !== descriptor.itemKind) {
|
|
2184
|
+
target.change[field] = { from: evolveFieldSpec(before), to: evolveFieldSpec(descriptor) };
|
|
2185
|
+
}
|
|
1721
2186
|
}
|
|
1722
|
-
|
|
2187
|
+
target.drop = Object.keys(from.fields).filter((field) => !(field in to.fields));
|
|
2188
|
+
return target;
|
|
2189
|
+
}
|
|
2190
|
+
function relaxValueDomainDirective(collection, from, to) {
|
|
2191
|
+
const fields = [.../* @__PURE__ */ new Set([...Object.keys(from.fields), ...Object.keys(to.fields)])].sort();
|
|
2192
|
+
const enums = fields.filter(
|
|
2193
|
+
(field) => from.fields[field]?.kind === "enum" || to.fields[field]?.kind === "enum"
|
|
2194
|
+
);
|
|
2195
|
+
return formatRelaxValueDomainDirective({ table: collection, fields, enums });
|
|
1723
2196
|
}
|
|
1724
2197
|
function validateRebuildSafety(collection, from, to) {
|
|
1725
2198
|
for (const [fieldName, descriptor] of Object.entries(to.fields)) {
|
|
@@ -1741,69 +2214,6 @@ function validateRebuildSafety(collection, from, to) {
|
|
|
1741
2214
|
}
|
|
1742
2215
|
}
|
|
1743
2216
|
}
|
|
1744
|
-
function projectionForColumn(column, fromFields, targetDescriptor) {
|
|
1745
|
-
if (column === "id" || column === "_created_at" || column === "_updated_at" || column === "_deleted") {
|
|
1746
|
-
return quoteIdentifier(column);
|
|
1747
|
-
}
|
|
1748
|
-
const sourceDescriptor = fromFields[column];
|
|
1749
|
-
if (sourceDescriptor && targetDescriptor) {
|
|
1750
|
-
return projectionForFieldTransform(column, sourceDescriptor, targetDescriptor);
|
|
1751
|
-
}
|
|
1752
|
-
if (sourceDescriptor) {
|
|
1753
|
-
return quoteIdentifier(column);
|
|
1754
|
-
}
|
|
1755
|
-
if (!targetDescriptor) {
|
|
1756
|
-
return "NULL";
|
|
1757
|
-
}
|
|
1758
|
-
if (targetDescriptor.auto && targetDescriptor.kind === "timestamp") {
|
|
1759
|
-
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1760
|
-
}
|
|
1761
|
-
if (targetDescriptor.defaultValue !== void 0) {
|
|
1762
|
-
return sqlLiteral(targetDescriptor.defaultValue);
|
|
1763
|
-
}
|
|
1764
|
-
return "NULL";
|
|
1765
|
-
}
|
|
1766
|
-
function projectionForFieldTransform(column, source, target) {
|
|
1767
|
-
const sourceColumn = quoteIdentifier(column);
|
|
1768
|
-
if (source.kind === target.kind && source.itemKind === target.itemKind) {
|
|
1769
|
-
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
1770
|
-
const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1771
|
-
const fallback = target.defaultValue !== void 0 ? sqlLiteral(target.defaultValue) : sourceColumn;
|
|
1772
|
-
return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${fallback} END`;
|
|
1773
|
-
}
|
|
1774
|
-
return sourceColumn;
|
|
1775
|
-
}
|
|
1776
|
-
if (target.kind === "string") {
|
|
1777
|
-
return `CAST(${sourceColumn} AS TEXT)`;
|
|
1778
|
-
}
|
|
1779
|
-
if (target.kind === "number" || target.kind === "timestamp") {
|
|
1780
|
-
if (source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
|
|
1781
|
-
const castType = target.kind === "number" ? "REAL" : "INTEGER";
|
|
1782
|
-
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL ELSE CAST(${sourceColumn} AS ${castType}) END`;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
if (target.kind === "boolean") {
|
|
1786
|
-
if (source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
|
|
1787
|
-
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN CAST(${sourceColumn} AS REAL) = 0 THEN 0 ELSE 1 END`;
|
|
1788
|
-
}
|
|
1789
|
-
if (source.kind === "string" || source.kind === "enum") {
|
|
1790
|
-
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('1','true','t','yes','y','on') THEN 1 WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('0','false','f','no','n','off') THEN 0 ELSE ${projectionFallback(target)} END`;
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
1794
|
-
if (source.kind === "string" || source.kind === "enum") {
|
|
1795
|
-
const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1796
|
-
return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${projectionFallback(target)} END`;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
if (target.kind === "array" && source.kind === "array" && source.itemKind === target.itemKind) {
|
|
1800
|
-
return sourceColumn;
|
|
1801
|
-
}
|
|
1802
|
-
if (target.auto && target.kind === "timestamp") {
|
|
1803
|
-
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1804
|
-
}
|
|
1805
|
-
return projectionFallback(target);
|
|
1806
|
-
}
|
|
1807
2217
|
function canTransformField(source, target) {
|
|
1808
2218
|
if (source.kind === target.kind && source.itemKind === target.itemKind) {
|
|
1809
2219
|
return true;
|
|
@@ -1828,69 +2238,12 @@ function canTransformField(source, target) {
|
|
|
1828
2238
|
}
|
|
1829
2239
|
return false;
|
|
1830
2240
|
}
|
|
1831
|
-
function projectionFallback(target) {
|
|
1832
|
-
if (target.auto && target.kind === "timestamp") {
|
|
1833
|
-
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1834
|
-
}
|
|
1835
|
-
if (target.defaultValue !== void 0) {
|
|
1836
|
-
return sqlLiteral(target.defaultValue);
|
|
1837
|
-
}
|
|
1838
|
-
return "NULL";
|
|
1839
|
-
}
|
|
1840
2241
|
function dropCollectionStatements(collection) {
|
|
1841
|
-
const table =
|
|
1842
|
-
const opsTable =
|
|
2242
|
+
const table = quoteIdentifier2(collection);
|
|
2243
|
+
const opsTable = quoteIdentifier2(`_kora_ops_${collection}`);
|
|
1843
2244
|
return [`DROP TABLE IF EXISTS ${table}`, `DROP TABLE IF EXISTS ${opsTable}`];
|
|
1844
2245
|
}
|
|
1845
|
-
function
|
|
1846
|
-
const sqlType = mapFieldType(descriptor);
|
|
1847
|
-
const parts = [quoteIdentifier(fieldName), sqlType];
|
|
1848
|
-
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1849
|
-
parts.push("NOT NULL");
|
|
1850
|
-
}
|
|
1851
|
-
if (descriptor.defaultValue !== void 0) {
|
|
1852
|
-
parts.push(`DEFAULT ${sqlLiteral(descriptor.defaultValue)}`);
|
|
1853
|
-
}
|
|
1854
|
-
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1855
|
-
const values = descriptor.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1856
|
-
parts.push(`CHECK (${quoteIdentifier(fieldName)} IN (${values}))`);
|
|
1857
|
-
}
|
|
1858
|
-
return parts.join(" ");
|
|
1859
|
-
}
|
|
1860
|
-
function mapFieldType(descriptor) {
|
|
1861
|
-
switch (descriptor.kind) {
|
|
1862
|
-
case "string":
|
|
1863
|
-
return "TEXT";
|
|
1864
|
-
case "number":
|
|
1865
|
-
return "REAL";
|
|
1866
|
-
case "boolean":
|
|
1867
|
-
return "INTEGER";
|
|
1868
|
-
case "enum":
|
|
1869
|
-
return "TEXT";
|
|
1870
|
-
case "timestamp":
|
|
1871
|
-
return "INTEGER";
|
|
1872
|
-
case "array":
|
|
1873
|
-
return "TEXT";
|
|
1874
|
-
case "object":
|
|
1875
|
-
return "TEXT";
|
|
1876
|
-
case "json":
|
|
1877
|
-
return "TEXT";
|
|
1878
|
-
case "blob":
|
|
1879
|
-
return "TEXT";
|
|
1880
|
-
case "secret":
|
|
1881
|
-
return "TEXT";
|
|
1882
|
-
case "richtext":
|
|
1883
|
-
return "BLOB";
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
function sqlLiteral(value) {
|
|
1887
|
-
if (value === null) return "NULL";
|
|
1888
|
-
if (typeof value === "number") return String(value);
|
|
1889
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
1890
|
-
if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
|
|
1891
|
-
return `'${JSON.stringify(value).replaceAll("'", "''")}'`;
|
|
1892
|
-
}
|
|
1893
|
-
function quoteIdentifier(identifier) {
|
|
2246
|
+
function quoteIdentifier2(identifier) {
|
|
1894
2247
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
|
|
1895
2248
|
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1896
2249
|
}
|
|
@@ -1968,6 +2321,8 @@ async function runSqliteMigration(path, statements, migrationId, fromVersion, to
|
|
|
1968
2321
|
const driver = driverOverride ?? await loadSqliteDriver(projectRoot);
|
|
1969
2322
|
const db = driver.open(path);
|
|
1970
2323
|
let statementsApplied = 0;
|
|
2324
|
+
const foreignKeysOn = Number(db.query?.("PRAGMA foreign_keys")[0]?.foreign_keys ?? 0) === 1;
|
|
2325
|
+
if (foreignKeysOn) db.exec("PRAGMA foreign_keys = OFF");
|
|
1971
2326
|
try {
|
|
1972
2327
|
db.exec("BEGIN");
|
|
1973
2328
|
db.exec(
|
|
@@ -1983,12 +2338,38 @@ async function runSqliteMigration(path, statements, migrationId, fromVersion, to
|
|
|
1983
2338
|
skipped: true
|
|
1984
2339
|
};
|
|
1985
2340
|
}
|
|
2341
|
+
const now = Date.now();
|
|
2342
|
+
const catalogQuery = (table) => {
|
|
2343
|
+
const query = db.query?.bind(db);
|
|
2344
|
+
if (!query) {
|
|
2345
|
+
throw new Error(
|
|
2346
|
+
`Migration ${migrationId}: the SQLite driver cannot read the catalog needed to migrate "${table}".`
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
return async (text) => query(text);
|
|
2350
|
+
};
|
|
1986
2351
|
for (const statement of statements) {
|
|
1987
|
-
|
|
2352
|
+
const relax = parseRelaxValueDomainDirective(statement);
|
|
2353
|
+
const evolve = relax ? null : parseEvolveTableDirective(statement);
|
|
2354
|
+
if (relax) {
|
|
2355
|
+
const expanded = await expandRelaxValueDomainForSqlite(relax, catalogQuery(relax.table));
|
|
2356
|
+
for (const rebuild of expanded) db.exec(rebuild);
|
|
2357
|
+
} else if (evolve) {
|
|
2358
|
+
const expanded = await expandEvolveTableForSqlite(evolve, catalogQuery(evolve.table), now);
|
|
2359
|
+
for (const change of expanded) db.exec(change);
|
|
2360
|
+
} else {
|
|
2361
|
+
db.exec(statement);
|
|
2362
|
+
}
|
|
1988
2363
|
statementsApplied++;
|
|
1989
2364
|
}
|
|
2365
|
+
const violations = db.query?.("PRAGMA foreign_key_check") ?? [];
|
|
2366
|
+
if (violations.length > 0) {
|
|
2367
|
+
throw new Error(
|
|
2368
|
+
`Migration ${migrationId} left ${violations.length} foreign key violation(s) (first: ${JSON.stringify(violations[0])}); nothing was applied.`
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
1990
2371
|
db.exec(
|
|
1991
|
-
`INSERT OR REPLACE INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${
|
|
2372
|
+
`INSERT OR REPLACE INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()})`
|
|
1992
2373
|
);
|
|
1993
2374
|
db.exec("COMMIT");
|
|
1994
2375
|
return {
|
|
@@ -2004,6 +2385,12 @@ async function runSqliteMigration(path, statements, migrationId, fromVersion, to
|
|
|
2004
2385
|
}
|
|
2005
2386
|
throw error;
|
|
2006
2387
|
} finally {
|
|
2388
|
+
if (foreignKeysOn) {
|
|
2389
|
+
try {
|
|
2390
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
2391
|
+
} catch {
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2007
2394
|
if (typeof db.close === "function") {
|
|
2008
2395
|
db.close();
|
|
2009
2396
|
}
|
|
@@ -2025,6 +2412,9 @@ async function loadSqliteDriver(projectRoot) {
|
|
|
2025
2412
|
const row = db.prepare("SELECT COUNT(*) AS count FROM _kora_migrations WHERE id = ?").get(id);
|
|
2026
2413
|
return (row?.count ?? 0) > 0;
|
|
2027
2414
|
},
|
|
2415
|
+
query(sql) {
|
|
2416
|
+
return db.prepare(sql).all();
|
|
2417
|
+
},
|
|
2028
2418
|
close() {
|
|
2029
2419
|
db.close();
|
|
2030
2420
|
}
|
|
@@ -2038,7 +2428,11 @@ async function loadSqliteDriver(projectRoot) {
|
|
|
2038
2428
|
}
|
|
2039
2429
|
}
|
|
2040
2430
|
async function runPostgresMigration(connectionString, statements, migrationId, fromVersion, toVersion, clientFactoryOverride) {
|
|
2041
|
-
const sql = typeof clientFactoryOverride === "function" ? clientFactoryOverride(connectionString) : (
|
|
2431
|
+
const sql = typeof clientFactoryOverride === "function" ? clientFactoryOverride(connectionString) : (
|
|
2432
|
+
// One connection: BEGIN, the statements and COMMIT must share it (postgres.js
|
|
2433
|
+
// refuses a raw BEGIN on a pool, UNSAFE_TRANSACTION).
|
|
2434
|
+
(await loadPostgresModule()).default(connectionString, { max: 1 })
|
|
2435
|
+
);
|
|
2042
2436
|
let statementsApplied = 0;
|
|
2043
2437
|
try {
|
|
2044
2438
|
await sql.unsafe("BEGIN");
|
|
@@ -2046,7 +2440,7 @@ async function runPostgresMigration(connectionString, statements, migrationId, f
|
|
|
2046
2440
|
"CREATE TABLE IF NOT EXISTS _kora_migrations (id TEXT PRIMARY KEY, from_version INTEGER NOT NULL, to_version INTEGER NOT NULL, applied_at BIGINT NOT NULL)"
|
|
2047
2441
|
);
|
|
2048
2442
|
const existing = await sql.unsafe(
|
|
2049
|
-
`SELECT COUNT(*)::int AS count FROM _kora_migrations WHERE id = ${
|
|
2443
|
+
`SELECT COUNT(*)::int AS count FROM _kora_migrations WHERE id = ${sqlLiteral(migrationId)}`
|
|
2050
2444
|
);
|
|
2051
2445
|
if ((existing[0]?.count ?? 0) > 0) {
|
|
2052
2446
|
await sql.unsafe("COMMIT");
|
|
@@ -2057,12 +2451,25 @@ async function runPostgresMigration(connectionString, statements, migrationId, f
|
|
|
2057
2451
|
skipped: true
|
|
2058
2452
|
};
|
|
2059
2453
|
}
|
|
2454
|
+
const now = Date.now();
|
|
2455
|
+
const query = async (text) => await sql.unsafe(text);
|
|
2060
2456
|
for (const statement of statements) {
|
|
2061
|
-
|
|
2457
|
+
const relax = parseRelaxValueDomainDirective(statement);
|
|
2458
|
+
const evolve = relax ? null : parseEvolveTableDirective(statement);
|
|
2459
|
+
if (relax) {
|
|
2460
|
+
const expanded = await expandRelaxValueDomainForPostgres(relax, query);
|
|
2461
|
+
for (const alter of expanded) await sql.unsafe(alter);
|
|
2462
|
+
} else if (evolve) {
|
|
2463
|
+
const expanded = await expandEvolveTableForPostgres(evolve, query, now);
|
|
2464
|
+
for (const alter of expanded) await sql.unsafe(alter);
|
|
2465
|
+
} else {
|
|
2466
|
+
assertNotLegacyRebuild(statement, migrationId);
|
|
2467
|
+
await sql.unsafe(statement);
|
|
2468
|
+
}
|
|
2062
2469
|
statementsApplied++;
|
|
2063
2470
|
}
|
|
2064
2471
|
await sql.unsafe(
|
|
2065
|
-
`INSERT INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${
|
|
2472
|
+
`INSERT INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET from_version = EXCLUDED.from_version, to_version = EXCLUDED.to_version, applied_at = EXCLUDED.applied_at`
|
|
2066
2473
|
);
|
|
2067
2474
|
await sql.unsafe("COMMIT");
|
|
2068
2475
|
return {
|
|
@@ -2083,7 +2490,14 @@ async function runPostgresMigration(connectionString, statements, migrationId, f
|
|
|
2083
2490
|
}
|
|
2084
2491
|
}
|
|
2085
2492
|
}
|
|
2086
|
-
function
|
|
2493
|
+
function assertNotLegacyRebuild(statement, migrationId) {
|
|
2494
|
+
if (/^\s*CREATE\s+TABLE\s+"?_kora_mig_[A-Za-z0-9_]+_new"?/i.test(statement)) {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
`Migration ${migrationId} was generated by an earlier \`kora migrate\` as a SQLite-only table rebuild, which cannot run on Postgres. Regenerate it: restore kora/schema.snapshot.json to the schema version it migrates from, delete the migration's files, and run \`kora migrate\` again.`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
function sqlLiteral(value) {
|
|
2087
2501
|
return `'${value.replaceAll("'", "''")}'`;
|
|
2088
2502
|
}
|
|
2089
2503
|
async function loadPostgresModule() {
|
|
@@ -2633,7 +3047,7 @@ var studioCommand = defineCommand9({
|
|
|
2633
3047
|
if (Number.isNaN(port) || port < 0 || port > 65535) {
|
|
2634
3048
|
throw new Error(`Invalid port "${args.port}"`);
|
|
2635
3049
|
}
|
|
2636
|
-
const { startStudioServer } = await import("./studio-server-
|
|
3050
|
+
const { startStudioServer } = await import("./studio-server-XLGZNGG5.js");
|
|
2637
3051
|
if (typeof args.connect === "string") {
|
|
2638
3052
|
if (typeof args.schema !== "string") {
|
|
2639
3053
|
throw new Error(
|