@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.
Files changed (83) hide show
  1. package/README.md +25 -107
  2. package/dist/bin.cjs +808 -340
  3. package/dist/bin.cjs.map +1 -1
  4. package/dist/bin.js +588 -174
  5. package/dist/bin.js.map +1 -1
  6. package/dist/{chunk-G2PBYMMA.js → chunk-7Y4J2GXP.js} +168 -153
  7. package/dist/chunk-7Y4J2GXP.js.map +1 -0
  8. package/dist/{chunk-Z7GCXMGW.js → chunk-TIKGLNER.js} +44 -9
  9. package/dist/chunk-TIKGLNER.js.map +1 -0
  10. package/dist/{chunk-4IKCHRJ6.js → chunk-TVIRMDER.js} +122 -112
  11. package/dist/chunk-TVIRMDER.js.map +1 -0
  12. package/dist/create.cjs +63 -28
  13. package/dist/create.cjs.map +1 -1
  14. package/dist/create.js +2 -2
  15. package/dist/index.cjs +41 -14
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +21 -1
  18. package/dist/index.d.ts +21 -1
  19. package/dist/index.js +8 -2
  20. package/dist/{studio-server-NIFKZI4F.js → studio-server-XLGZNGG5.js} +11 -2
  21. package/dist/studio-server-XLGZNGG5.js.map +1 -0
  22. package/dist/vite.cjs +351 -0
  23. package/dist/vite.cjs.map +1 -0
  24. package/dist/vite.d.cts +161 -0
  25. package/dist/vite.d.ts +161 -0
  26. package/dist/vite.js +321 -0
  27. package/dist/vite.js.map +1 -0
  28. package/package.json +18 -8
  29. package/templates/react-basic/AGENTS.md +5 -5
  30. package/templates/react-basic/vite.config.ts +4 -1
  31. package/templates/react-sync/AGENTS.md +5 -5
  32. package/templates/react-sync/server.ts +8 -0
  33. package/templates/react-sync/src/vite-env.d.ts +1 -0
  34. package/templates/react-sync/vite.config.ts +4 -1
  35. package/templates/react-tailwind/AGENTS.md +5 -5
  36. package/templates/react-tailwind/vite.config.ts +10 -1
  37. package/templates/react-tailwind-sync/AGENTS.md +9 -7
  38. package/templates/react-tailwind-sync/server.ts +8 -0
  39. package/templates/react-tailwind-sync/src/App.tsx +1 -1
  40. package/templates/react-tailwind-sync/src/kora.ts +32 -0
  41. package/templates/react-tailwind-sync/src/main.tsx +1 -23
  42. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +4 -8
  43. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +2 -2
  44. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  45. package/templates/react-tailwind-sync/src/vite-env.d.ts +1 -0
  46. package/templates/react-tailwind-sync/vite.config.ts +10 -1
  47. package/templates/svelte-basic/AGENTS.md +4 -4
  48. package/templates/svelte-basic/package.json.hbs +6 -0
  49. package/templates/svelte-basic/src/App.svelte +2 -1
  50. package/templates/svelte-basic/vite.config.ts +4 -1
  51. package/templates/svelte-sync/AGENTS.md +4 -4
  52. package/templates/svelte-sync/server.ts +8 -0
  53. package/templates/svelte-sync/src/App.svelte +2 -1
  54. package/templates/svelte-sync/vite.config.ts +4 -1
  55. package/templates/svelte-tailwind/AGENTS.md +4 -4
  56. package/templates/svelte-tailwind/package.json.hbs +7 -1
  57. package/templates/svelte-tailwind/src/App.svelte +2 -1
  58. package/templates/svelte-tailwind/vite.config.ts +10 -1
  59. package/templates/svelte-tailwind-sync/AGENTS.md +4 -4
  60. package/templates/svelte-tailwind-sync/package.json.hbs +1 -1
  61. package/templates/svelte-tailwind-sync/server.ts +8 -0
  62. package/templates/svelte-tailwind-sync/src/App.svelte +2 -1
  63. package/templates/svelte-tailwind-sync/vite.config.ts +10 -1
  64. package/templates/tauri-react/AGENTS.md +5 -5
  65. package/templates/tauri-react/package.json.hbs +1 -1
  66. package/templates/tauri-react/server.ts +8 -0
  67. package/templates/tauri-react/src/vite-env.d.ts +1 -0
  68. package/templates/vue-basic/AGENTS.md +4 -4
  69. package/templates/vue-basic/package.json.hbs +6 -0
  70. package/templates/vue-basic/vite.config.ts +4 -1
  71. package/templates/vue-sync/AGENTS.md +4 -4
  72. package/templates/vue-sync/server.ts +8 -0
  73. package/templates/vue-sync/vite.config.ts +4 -1
  74. package/templates/vue-tailwind/AGENTS.md +4 -4
  75. package/templates/vue-tailwind/package.json.hbs +6 -0
  76. package/templates/vue-tailwind/vite.config.ts +4 -1
  77. package/templates/vue-tailwind-sync/AGENTS.md +4 -4
  78. package/templates/vue-tailwind-sync/server.ts +8 -0
  79. package/templates/vue-tailwind-sync/vite.config.ts +4 -1
  80. package/dist/chunk-4IKCHRJ6.js.map +0 -1
  81. package/dist/chunk-G2PBYMMA.js.map +0 -1
  82. package/dist/chunk-Z7GCXMGW.js.map +0 -1
  83. 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-4IKCHRJ6.js";
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-G2PBYMMA.js";
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
- if (serializeEnum(change.before.enumValues) !== serializeEnum(change.after.enumValues))
1624
- return true;
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 down = [];
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
- down.push(...dropCollectionStatements(change.collection));
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
- down.push(...generateSQL(change.collection, collectionDef));
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
- up.push(...generateRebuildStatements(collection, previousDef, currentDef));
1683
- down.push(...generateRebuildStatements(collection, currentDef, previousDef));
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 generateRebuildStatements(collection, from, to) {
1694
- const table = quoteIdentifier(collection);
1695
- const tempTable = quoteIdentifier(`_kora_mig_${collection}_new`);
1696
- const targetColumns = [
1697
- "id TEXT PRIMARY KEY NOT NULL",
1698
- ...Object.entries(to.fields).map(([field, descriptor]) => columnDefinition(field, descriptor)),
1699
- "_created_at INTEGER NOT NULL",
1700
- "_updated_at INTEGER NOT NULL",
1701
- "_deleted INTEGER NOT NULL DEFAULT 0"
1702
- ];
1703
- const statements = [];
1704
- statements.push(`CREATE TABLE ${tempTable} (
1705
- ${targetColumns.join(",\n ")}
1706
- )`);
1707
- const toFields = Object.keys(to.fields);
1708
- const columns = ["id", ...toFields, "_created_at", "_updated_at", "_deleted"];
1709
- const selectExpressions = columns.map(
1710
- (column) => projectionForColumn(column, from.fields, to.fields[column] ?? null)
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
- return statements;
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 = quoteIdentifier(collection);
1842
- const opsTable = quoteIdentifier(`_kora_ops_${collection}`);
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 columnDefinition(fieldName, descriptor) {
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
- db.exec(statement);
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 (${sqlLiteral2(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()})`
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) : (await loadPostgresModule()).default(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 = ${sqlLiteral2(migrationId)}`
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
- await sql.unsafe(statement);
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 (${sqlLiteral2(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`
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 sqlLiteral2(value) {
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-NIFKZI4F.js");
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(