@prisma-next/target-postgres 0.3.0-dev.53 → 0.3.0-dev.55

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/dist/control.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as postgresTargetDescriptorMeta } from "./descriptor-meta-DxB8oZzB.mjs";
2
2
  import { SQL_CHAR_CODEC_ID, SQL_FLOAT_CODEC_ID, SQL_INT_CODEC_ID, SQL_VARCHAR_CODEC_ID } from "@prisma-next/sql-relational-core/ast";
3
- import { arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema } from "@prisma-next/family-sql/schema-verify";
3
+ import { arraysEqual, verifySqlSchema } from "@prisma-next/family-sql/schema-verify";
4
4
  import { ifDefined } from "@prisma-next/utils/defined";
5
5
  import { bigintJsonReplacer, isTaggedBigInt } from "@prisma-next/contract/types";
6
6
  import { createMigrationPlan, extractCodecControlHooks, plannerFailure, plannerSuccess, runnerFailure, runnerSuccess } from "@prisma-next/family-sql/control";
@@ -8,31 +8,6 @@ import { readMarker } from "@prisma-next/family-sql/verify";
8
8
  import { SqlQueryError } from "@prisma-next/sql-errors";
9
9
  import { ok, okVoid } from "@prisma-next/utils/result";
10
10
 
11
- //#region ../../6-adapters/postgres/dist/codec-ids-Bsm9c7ns.mjs
12
- const PG_TEXT_CODEC_ID = "pg/text@1";
13
- const PG_ENUM_CODEC_ID = "pg/enum@1";
14
- const PG_CHAR_CODEC_ID = "pg/char@1";
15
- const PG_VARCHAR_CODEC_ID = "pg/varchar@1";
16
- const PG_INT_CODEC_ID = "pg/int@1";
17
- const PG_INT2_CODEC_ID = "pg/int2@1";
18
- const PG_INT4_CODEC_ID = "pg/int4@1";
19
- const PG_INT8_CODEC_ID = "pg/int8@1";
20
- const PG_FLOAT_CODEC_ID = "pg/float@1";
21
- const PG_FLOAT4_CODEC_ID = "pg/float4@1";
22
- const PG_FLOAT8_CODEC_ID = "pg/float8@1";
23
- const PG_NUMERIC_CODEC_ID = "pg/numeric@1";
24
- const PG_BOOL_CODEC_ID = "pg/bool@1";
25
- const PG_BIT_CODEC_ID = "pg/bit@1";
26
- const PG_VARBIT_CODEC_ID = "pg/varbit@1";
27
- const PG_TIMESTAMP_CODEC_ID = "pg/timestamp@1";
28
- const PG_TIMESTAMPTZ_CODEC_ID = "pg/timestamptz@1";
29
- const PG_TIME_CODEC_ID = "pg/time@1";
30
- const PG_TIMETZ_CODEC_ID = "pg/timetz@1";
31
- const PG_INTERVAL_CODEC_ID = "pg/interval@1";
32
- const PG_JSON_CODEC_ID = "pg/json@1";
33
- const PG_JSONB_CODEC_ID = "pg/jsonb@1";
34
-
35
- //#endregion
36
11
  //#region ../../6-adapters/postgres/dist/sql-utils-CSfAGEwF.mjs
37
12
  /**
38
13
  * Shared SQL utility functions for the Postgres adapter.
@@ -108,6 +83,31 @@ function validateEnumValueLength(value, enumTypeName) {
108
83
  if (value.length > MAX_IDENTIFIER_LENGTH$1) throw new SqlEscapeError(`Enum value "${value.slice(0, 20)}..." for type "${enumTypeName}" exceeds PostgreSQL's ${MAX_IDENTIFIER_LENGTH$1}-character label limit`, value, "literal");
109
84
  }
110
85
 
86
+ //#endregion
87
+ //#region ../../6-adapters/postgres/dist/codec-ids-Bsm9c7ns.mjs
88
+ const PG_TEXT_CODEC_ID = "pg/text@1";
89
+ const PG_ENUM_CODEC_ID = "pg/enum@1";
90
+ const PG_CHAR_CODEC_ID = "pg/char@1";
91
+ const PG_VARCHAR_CODEC_ID = "pg/varchar@1";
92
+ const PG_INT_CODEC_ID = "pg/int@1";
93
+ const PG_INT2_CODEC_ID = "pg/int2@1";
94
+ const PG_INT4_CODEC_ID = "pg/int4@1";
95
+ const PG_INT8_CODEC_ID = "pg/int8@1";
96
+ const PG_FLOAT_CODEC_ID = "pg/float@1";
97
+ const PG_FLOAT4_CODEC_ID = "pg/float4@1";
98
+ const PG_FLOAT8_CODEC_ID = "pg/float8@1";
99
+ const PG_NUMERIC_CODEC_ID = "pg/numeric@1";
100
+ const PG_BOOL_CODEC_ID = "pg/bool@1";
101
+ const PG_BIT_CODEC_ID = "pg/bit@1";
102
+ const PG_VARBIT_CODEC_ID = "pg/varbit@1";
103
+ const PG_TIMESTAMP_CODEC_ID = "pg/timestamp@1";
104
+ const PG_TIMESTAMPTZ_CODEC_ID = "pg/timestamptz@1";
105
+ const PG_TIME_CODEC_ID = "pg/time@1";
106
+ const PG_TIMETZ_CODEC_ID = "pg/timetz@1";
107
+ const PG_INTERVAL_CODEC_ID = "pg/interval@1";
108
+ const PG_JSON_CODEC_ID = "pg/json@1";
109
+ const PG_JSONB_CODEC_ID = "pg/jsonb@1";
110
+
111
111
  //#endregion
112
112
  //#region ../../6-adapters/postgres/dist/descriptor-meta-ilnFI7bx.mjs
113
113
  const ENUM_INTROSPECT_QUERY = `
@@ -1463,6 +1463,395 @@ var control_default$1 = {
1463
1463
  }
1464
1464
  };
1465
1465
 
1466
+ //#endregion
1467
+ //#region src/core/migrations/planner-reconciliation.ts
1468
+ function buildReconciliationPlan(options) {
1469
+ const operations = [];
1470
+ const conflicts = [];
1471
+ const { mode } = options;
1472
+ const seenOperationIds = /* @__PURE__ */ new Set();
1473
+ for (const issue of sortSchemaIssues(options.issues)) {
1474
+ if (isAdditiveIssue(issue)) continue;
1475
+ const operation = buildReconciliationOperationFromIssue({
1476
+ issue,
1477
+ contract: options.contract,
1478
+ schemaName: options.schemaName,
1479
+ mode
1480
+ });
1481
+ if (operation) {
1482
+ if (!seenOperationIds.has(operation.id)) {
1483
+ seenOperationIds.add(operation.id);
1484
+ if (options.policy.allowedOperationClasses.includes(operation.operationClass)) operations.push(operation);
1485
+ else {
1486
+ const conflict = convertIssueToConflict(issue);
1487
+ if (conflict) conflicts.push(conflict);
1488
+ }
1489
+ }
1490
+ } else {
1491
+ const conflict = convertIssueToConflict(issue);
1492
+ if (conflict) conflicts.push(conflict);
1493
+ }
1494
+ }
1495
+ return {
1496
+ operations,
1497
+ conflicts: conflicts.sort(conflictComparator)
1498
+ };
1499
+ }
1500
+ function isAdditiveIssue(issue) {
1501
+ switch (issue.kind) {
1502
+ case "type_missing":
1503
+ case "type_values_mismatch":
1504
+ case "missing_table":
1505
+ case "missing_column":
1506
+ case "extension_missing": return true;
1507
+ case "primary_key_mismatch": return issue.actual === void 0;
1508
+ case "unique_constraint_mismatch":
1509
+ case "index_mismatch":
1510
+ case "foreign_key_mismatch": return issue.indexOrConstraint === void 0;
1511
+ default: return false;
1512
+ }
1513
+ }
1514
+ function buildReconciliationOperationFromIssue(options) {
1515
+ const { issue, contract, schemaName, mode } = options;
1516
+ switch (issue.kind) {
1517
+ case "extra_table":
1518
+ if (!mode.allowDestructive || !issue.table) return null;
1519
+ return buildDropTableOperation(schemaName, issue.table);
1520
+ case "extra_column":
1521
+ if (!mode.allowDestructive || !issue.table || !issue.column) return null;
1522
+ return buildDropColumnOperation(schemaName, issue.table, issue.column);
1523
+ case "extra_index":
1524
+ if (!mode.allowDestructive || !issue.table || !issue.indexOrConstraint) return null;
1525
+ return buildDropIndexOperation(schemaName, issue.table, issue.indexOrConstraint);
1526
+ case "extra_foreign_key":
1527
+ case "extra_unique_constraint": {
1528
+ if (!mode.allowDestructive || !issue.table || !issue.indexOrConstraint) return null;
1529
+ const constraintKind = issue.kind === "extra_foreign_key" ? "foreignKey" : "unique";
1530
+ return buildDropConstraintOperation(schemaName, issue.table, issue.indexOrConstraint, constraintKind);
1531
+ }
1532
+ case "extra_primary_key": {
1533
+ if (!mode.allowDestructive || !issue.table) return null;
1534
+ const constraintName = issue.indexOrConstraint ?? `${issue.table}_pkey`;
1535
+ return buildDropConstraintOperation(schemaName, issue.table, constraintName, "primaryKey");
1536
+ }
1537
+ case "nullability_mismatch":
1538
+ if (!issue.table || !issue.column) return null;
1539
+ if (issue.expected === "true") return mode.allowWidening ? buildDropNotNullOperation(schemaName, issue.table, issue.column) : null;
1540
+ return mode.allowDestructive ? buildSetNotNullOperation(schemaName, issue.table, issue.column) : null;
1541
+ case "type_mismatch": {
1542
+ if (!mode.allowDestructive || !issue.table || !issue.column) return null;
1543
+ const contractColumn = getContractColumn(contract, issue.table, issue.column);
1544
+ if (!contractColumn) return null;
1545
+ return buildAlterColumnTypeOperation(schemaName, issue.table, issue.column, contractColumn);
1546
+ }
1547
+ default: return null;
1548
+ }
1549
+ }
1550
+ function getContractColumn(contract, tableName, columnName) {
1551
+ const table = contract.storage.tables[tableName];
1552
+ if (!table) return null;
1553
+ return table.columns[columnName] ?? null;
1554
+ }
1555
+ function buildDropTableOperation(schemaName, tableName) {
1556
+ return {
1557
+ id: `dropTable.${tableName}`,
1558
+ label: `Drop table ${tableName}`,
1559
+ summary: `Drops extra table ${tableName}`,
1560
+ operationClass: "destructive",
1561
+ target: {
1562
+ id: "postgres",
1563
+ details: buildTargetDetails("table", tableName, schemaName)
1564
+ },
1565
+ precheck: [{
1566
+ description: `ensure table "${tableName}" exists`,
1567
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NOT NULL`
1568
+ }],
1569
+ execute: [{
1570
+ description: `drop table "${tableName}"`,
1571
+ sql: `DROP TABLE ${qualifyTableName(schemaName, tableName)}`
1572
+ }],
1573
+ postcheck: [{
1574
+ description: `verify table "${tableName}" is removed`,
1575
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NULL`
1576
+ }]
1577
+ };
1578
+ }
1579
+ function buildDropColumnOperation(schemaName, tableName, columnName) {
1580
+ return {
1581
+ id: `dropColumn.${tableName}.${columnName}`,
1582
+ label: `Drop column ${columnName} from ${tableName}`,
1583
+ summary: `Drops extra column ${columnName} from table ${tableName}`,
1584
+ operationClass: "destructive",
1585
+ target: {
1586
+ id: "postgres",
1587
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
1588
+ },
1589
+ precheck: [{
1590
+ description: `ensure column "${columnName}" exists`,
1591
+ sql: columnExistsCheck({
1592
+ schema: schemaName,
1593
+ table: tableName,
1594
+ column: columnName
1595
+ })
1596
+ }],
1597
+ execute: [{
1598
+ description: `drop column "${columnName}"`,
1599
+ sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)} DROP COLUMN ${quoteIdentifier(columnName)}`
1600
+ }],
1601
+ postcheck: [{
1602
+ description: `verify column "${columnName}" is removed`,
1603
+ sql: columnExistsCheck({
1604
+ schema: schemaName,
1605
+ table: tableName,
1606
+ column: columnName,
1607
+ exists: false
1608
+ })
1609
+ }]
1610
+ };
1611
+ }
1612
+ function buildDropIndexOperation(schemaName, tableName, indexName) {
1613
+ return {
1614
+ id: `dropIndex.${tableName}.${indexName}`,
1615
+ label: `Drop index ${indexName} on ${tableName}`,
1616
+ summary: `Drops extra index ${indexName} on table ${tableName}`,
1617
+ operationClass: "destructive",
1618
+ target: {
1619
+ id: "postgres",
1620
+ details: buildTargetDetails("index", indexName, schemaName, tableName)
1621
+ },
1622
+ precheck: [{
1623
+ description: `ensure index "${indexName}" exists`,
1624
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`
1625
+ }],
1626
+ execute: [{
1627
+ description: `drop index "${indexName}"`,
1628
+ sql: `DROP INDEX ${qualifyTableName(schemaName, indexName)}`
1629
+ }],
1630
+ postcheck: [{
1631
+ description: `verify index "${indexName}" is removed`,
1632
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`
1633
+ }]
1634
+ };
1635
+ }
1636
+ function buildDropConstraintOperation(schemaName, tableName, constraintName, constraintKind) {
1637
+ return {
1638
+ id: `dropConstraint.${tableName}.${constraintName}`,
1639
+ label: `Drop constraint ${constraintName} on ${tableName}`,
1640
+ summary: `Drops extra constraint ${constraintName} on table ${tableName}`,
1641
+ operationClass: "destructive",
1642
+ target: {
1643
+ id: "postgres",
1644
+ details: buildTargetDetails(constraintKind, constraintName, schemaName, tableName)
1645
+ },
1646
+ precheck: [{
1647
+ description: `ensure constraint "${constraintName}" exists`,
1648
+ sql: constraintExistsCheck({
1649
+ constraintName,
1650
+ schema: schemaName
1651
+ })
1652
+ }],
1653
+ execute: [{
1654
+ description: `drop constraint "${constraintName}"`,
1655
+ sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
1656
+ DROP CONSTRAINT ${quoteIdentifier(constraintName)}`
1657
+ }],
1658
+ postcheck: [{
1659
+ description: `verify constraint "${constraintName}" is removed`,
1660
+ sql: constraintExistsCheck({
1661
+ constraintName,
1662
+ schema: schemaName,
1663
+ exists: false
1664
+ })
1665
+ }]
1666
+ };
1667
+ }
1668
+ function buildDropNotNullOperation(schemaName, tableName, columnName) {
1669
+ return {
1670
+ id: `alterNullability.${tableName}.${columnName}`,
1671
+ label: `Relax nullability for ${columnName} on ${tableName}`,
1672
+ summary: `Drops NOT NULL constraint for ${columnName} on table ${tableName}`,
1673
+ operationClass: "widening",
1674
+ target: {
1675
+ id: "postgres",
1676
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
1677
+ },
1678
+ precheck: [{
1679
+ description: `ensure column "${columnName}" exists`,
1680
+ sql: columnExistsCheck({
1681
+ schema: schemaName,
1682
+ table: tableName,
1683
+ column: columnName
1684
+ })
1685
+ }],
1686
+ execute: [{
1687
+ description: `drop NOT NULL from "${columnName}"`,
1688
+ sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
1689
+ ALTER COLUMN ${quoteIdentifier(columnName)} DROP NOT NULL`
1690
+ }],
1691
+ postcheck: [{
1692
+ description: `verify "${columnName}" is nullable`,
1693
+ sql: columnNullabilityCheck({
1694
+ schema: schemaName,
1695
+ table: tableName,
1696
+ column: columnName,
1697
+ nullable: true
1698
+ })
1699
+ }]
1700
+ };
1701
+ }
1702
+ function buildSetNotNullOperation(schemaName, tableName, columnName) {
1703
+ const qualified = qualifyTableName(schemaName, tableName);
1704
+ return {
1705
+ id: `alterNullability.${tableName}.${columnName}`,
1706
+ label: `Enforce NOT NULL for ${columnName} on ${tableName}`,
1707
+ summary: `Sets NOT NULL on ${columnName} for table ${tableName}`,
1708
+ operationClass: "destructive",
1709
+ target: {
1710
+ id: "postgres",
1711
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
1712
+ },
1713
+ precheck: [{
1714
+ description: `ensure column "${columnName}" exists`,
1715
+ sql: columnExistsCheck({
1716
+ schema: schemaName,
1717
+ table: tableName,
1718
+ column: columnName
1719
+ })
1720
+ }, {
1721
+ description: `ensure "${columnName}" has no NULL values`,
1722
+ sql: `SELECT NOT EXISTS (
1723
+ SELECT 1 FROM ${qualified}
1724
+ WHERE ${quoteIdentifier(columnName)} IS NULL
1725
+ LIMIT 1
1726
+ )`
1727
+ }],
1728
+ execute: [{
1729
+ description: `set NOT NULL on "${columnName}"`,
1730
+ sql: `ALTER TABLE ${qualified}
1731
+ ALTER COLUMN ${quoteIdentifier(columnName)} SET NOT NULL`
1732
+ }],
1733
+ postcheck: [{
1734
+ description: `verify "${columnName}" is NOT NULL`,
1735
+ sql: columnNullabilityCheck({
1736
+ schema: schemaName,
1737
+ table: tableName,
1738
+ column: columnName,
1739
+ nullable: false
1740
+ })
1741
+ }]
1742
+ };
1743
+ }
1744
+ function buildAlterColumnTypeOperation(schemaName, tableName, columnName, column) {
1745
+ const qualified = qualifyTableName(schemaName, tableName);
1746
+ const expectedType = buildColumnTypeSql(column);
1747
+ return {
1748
+ id: `alterType.${tableName}.${columnName}`,
1749
+ label: `Alter type for ${columnName} on ${tableName}`,
1750
+ summary: `Changes type of ${columnName} to ${expectedType}`,
1751
+ operationClass: "destructive",
1752
+ target: {
1753
+ id: "postgres",
1754
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
1755
+ },
1756
+ meta: {
1757
+ warning: "TABLE_REWRITE",
1758
+ detail: "ALTER COLUMN TYPE requires a full table rewrite and acquires an ACCESS EXCLUSIVE lock. On large tables, this can cause significant downtime."
1759
+ },
1760
+ precheck: [{
1761
+ description: `ensure column "${columnName}" exists`,
1762
+ sql: columnExistsCheck({
1763
+ schema: schemaName,
1764
+ table: tableName,
1765
+ column: columnName
1766
+ })
1767
+ }],
1768
+ execute: [{
1769
+ description: `alter type of "${columnName}"`,
1770
+ sql: `ALTER TABLE ${qualified}
1771
+ ALTER COLUMN ${quoteIdentifier(columnName)}
1772
+ TYPE ${expectedType}
1773
+ USING ${quoteIdentifier(columnName)}::${expectedType}`
1774
+ }],
1775
+ postcheck: [{
1776
+ description: `verify column "${columnName}" exists after type change`,
1777
+ sql: columnExistsCheck({
1778
+ schema: schemaName,
1779
+ table: tableName,
1780
+ column: columnName
1781
+ })
1782
+ }]
1783
+ };
1784
+ }
1785
+ function convertIssueToConflict(issue) {
1786
+ switch (issue.kind) {
1787
+ case "type_mismatch": return buildConflict("typeMismatch", issue);
1788
+ case "nullability_mismatch": return buildConflict("nullabilityConflict", issue);
1789
+ case "default_missing":
1790
+ case "default_mismatch":
1791
+ case "extra_table":
1792
+ case "extra_column":
1793
+ case "extra_primary_key":
1794
+ case "extra_foreign_key":
1795
+ case "extra_unique_constraint":
1796
+ case "extra_index": return buildConflict("missingButNonAdditive", issue);
1797
+ case "primary_key_mismatch":
1798
+ case "unique_constraint_mismatch":
1799
+ case "index_mismatch": return buildConflict("indexIncompatible", issue);
1800
+ case "foreign_key_mismatch": return buildConflict("foreignKeyConflict", issue);
1801
+ default: return null;
1802
+ }
1803
+ }
1804
+ function buildConflict(kind, issue) {
1805
+ const location = buildConflictLocation(issue);
1806
+ const meta = issue.expected || issue.actual ? Object.freeze({
1807
+ ...ifDefined("expected", issue.expected),
1808
+ ...ifDefined("actual", issue.actual)
1809
+ }) : void 0;
1810
+ return {
1811
+ kind,
1812
+ summary: issue.message,
1813
+ ...ifDefined("location", location),
1814
+ ...ifDefined("meta", meta)
1815
+ };
1816
+ }
1817
+ function sortSchemaIssues(issues) {
1818
+ return [...issues].sort((a, b) => {
1819
+ const kindCompare = a.kind.localeCompare(b.kind);
1820
+ if (kindCompare !== 0) return kindCompare;
1821
+ const tableCompare = compareStrings(a.table, b.table);
1822
+ if (tableCompare !== 0) return tableCompare;
1823
+ const columnCompare = compareStrings(a.column, b.column);
1824
+ if (columnCompare !== 0) return columnCompare;
1825
+ return compareStrings(a.indexOrConstraint, b.indexOrConstraint);
1826
+ });
1827
+ }
1828
+ function buildConflictLocation(issue) {
1829
+ const location = {
1830
+ ...ifDefined("table", issue.table),
1831
+ ...ifDefined("column", issue.column),
1832
+ ...ifDefined("constraint", issue.indexOrConstraint)
1833
+ };
1834
+ return Object.keys(location).length > 0 ? location : void 0;
1835
+ }
1836
+ function conflictComparator(a, b) {
1837
+ if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
1838
+ const aLocation = a.location ?? {};
1839
+ const bLocation = b.location ?? {};
1840
+ const tableCompare = compareStrings(aLocation.table, bLocation.table);
1841
+ if (tableCompare !== 0) return tableCompare;
1842
+ const columnCompare = compareStrings(aLocation.column, bLocation.column);
1843
+ if (columnCompare !== 0) return columnCompare;
1844
+ const constraintCompare = compareStrings(aLocation.constraint, bLocation.constraint);
1845
+ if (constraintCompare !== 0) return constraintCompare;
1846
+ return compareStrings(a.summary, b.summary);
1847
+ }
1848
+ function compareStrings(a, b) {
1849
+ if (a === b) return 0;
1850
+ if (a === void 0) return -1;
1851
+ if (b === void 0) return 1;
1852
+ return a < b ? -1 : 1;
1853
+ }
1854
+
1466
1855
  //#endregion
1467
1856
  //#region src/core/migrations/planner.ts
1468
1857
  const DEFAULT_PLANNER_CONFIG = { defaultSchema: "public" };
@@ -1480,13 +1869,23 @@ var PostgresMigrationPlanner = class {
1480
1869
  const schemaName = options.schemaName ?? this.config.defaultSchema;
1481
1870
  const policyResult = this.ensureAdditivePolicy(options.policy);
1482
1871
  if (policyResult) return policyResult;
1483
- const classification = this.classifySchema(options);
1484
- if (classification.kind === "conflict") return plannerFailure(classification.conflicts);
1872
+ const planningMode = this.resolvePlanningMode(options.policy);
1873
+ const schemaIssues = this.collectSchemaIssues(options, planningMode.includeExtraObjects);
1485
1874
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
1486
1875
  const operations = [];
1876
+ const reconciliationPlan = buildReconciliationPlan({
1877
+ contract: options.contract,
1878
+ issues: schemaIssues,
1879
+ schemaName,
1880
+ mode: planningMode,
1881
+ policy: options.policy
1882
+ });
1883
+ if (reconciliationPlan.conflicts.length > 0) return plannerFailure(reconciliationPlan.conflicts);
1487
1884
  const storageTypePlan = this.buildStorageTypeOperations(options, schemaName, codecHooks);
1488
1885
  if (storageTypePlan.conflicts.length > 0) return plannerFailure(storageTypePlan.conflicts);
1489
- operations.push(...this.buildDatabaseDependencyOperations(options), ...storageTypePlan.operations, ...this.buildTableOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildColumnOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildPrimaryKeyOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildUniqueOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildIndexOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildFkBackingIndexOperations(options.contract.storage.tables, options.schema, schemaName), ...this.buildForeignKeyOperations(options.contract.storage.tables, options.schema, schemaName));
1886
+ const sortedTables = sortedEntries(options.contract.storage.tables);
1887
+ const schemaLookups = buildSchemaLookupMap(options.schema);
1888
+ operations.push(...this.buildDatabaseDependencyOperations(options), ...storageTypePlan.operations, ...reconciliationPlan.operations, ...this.buildTableOperations(sortedTables, options.schema, schemaName), ...this.buildColumnOperations(sortedTables, options.schema, schemaName), ...this.buildPrimaryKeyOperations(sortedTables, options.schema, schemaName), ...this.buildUniqueOperations(sortedTables, schemaLookups, schemaName), ...this.buildIndexOperations(sortedTables, schemaLookups, schemaName), ...this.buildFkBackingIndexOperations(sortedTables, schemaLookups, schemaName), ...this.buildForeignKeyOperations(sortedTables, schemaLookups, schemaName));
1490
1889
  return plannerSuccess(createMigrationPlan({
1491
1890
  targetId: "postgres",
1492
1891
  origin: null,
@@ -1500,8 +1899,8 @@ var PostgresMigrationPlanner = class {
1500
1899
  ensureAdditivePolicy(policy) {
1501
1900
  if (!policy.allowedOperationClasses.includes("additive")) return plannerFailure([{
1502
1901
  kind: "unsupportedOperation",
1503
- summary: "Init planner requires additive operations be allowed",
1504
- why: "The init planner only emits additive operations. Update the policy to include \"additive\"."
1902
+ summary: "Migration planner requires additive operations be allowed",
1903
+ why: "The planner requires the \"additive\" operation class to be allowed in the policy."
1505
1904
  }]);
1506
1905
  return null;
1507
1906
  }
@@ -1576,7 +1975,7 @@ var PostgresMigrationPlanner = class {
1576
1975
  }
1577
1976
  buildTableOperations(tables, schema, schemaName) {
1578
1977
  const operations = [];
1579
- for (const [tableName, table] of sortedEntries(tables)) {
1978
+ for (const [tableName, table] of tables) {
1580
1979
  if (schema.tables[tableName]) continue;
1581
1980
  const qualified = qualifyTableName(schemaName, tableName);
1582
1981
  operations.push({
@@ -1606,7 +2005,7 @@ var PostgresMigrationPlanner = class {
1606
2005
  }
1607
2006
  buildColumnOperations(tables, schema, schemaName) {
1608
2007
  const operations = [];
1609
- for (const [tableName, table] of sortedEntries(tables)) {
2008
+ for (const [tableName, table] of tables) {
1610
2009
  const schemaTable = schema.tables[tableName];
1611
2010
  if (!schemaTable) continue;
1612
2011
  for (const [columnName, column] of sortedEntries(table.columns)) {
@@ -1646,10 +2045,11 @@ var PostgresMigrationPlanner = class {
1646
2045
  })
1647
2046
  }, ...notNull ? [{
1648
2047
  description: `verify column "${columnName}" is NOT NULL`,
1649
- sql: columnIsNotNullCheck({
2048
+ sql: columnNullabilityCheck({
1650
2049
  schema,
1651
2050
  table: tableName,
1652
- column: columnName
2051
+ column: columnName,
2052
+ nullable: false
1653
2053
  })
1654
2054
  }] : []];
1655
2055
  return {
@@ -1668,7 +2068,7 @@ var PostgresMigrationPlanner = class {
1668
2068
  }
1669
2069
  buildPrimaryKeyOperations(tables, schema, schemaName) {
1670
2070
  const operations = [];
1671
- for (const [tableName, table] of sortedEntries(tables)) {
2071
+ for (const [tableName, table] of tables) {
1672
2072
  if (!table.primaryKey) continue;
1673
2073
  const schemaTable = schema.tables[tableName];
1674
2074
  if (!schemaTable || schemaTable.primaryKey) continue;
@@ -1700,12 +2100,12 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(", ")})`
1700
2100
  }
1701
2101
  return operations;
1702
2102
  }
1703
- buildUniqueOperations(tables, schema, schemaName) {
2103
+ buildUniqueOperations(tables, schemaLookups, schemaName) {
1704
2104
  const operations = [];
1705
- for (const [tableName, table] of sortedEntries(tables)) {
1706
- const schemaTable = schema.tables[tableName];
2105
+ for (const [tableName, table] of tables) {
2106
+ const lookup = schemaLookups.get(tableName);
1707
2107
  for (const unique of table.uniques) {
1708
- if (schemaTable && hasUniqueConstraint(schemaTable, unique.columns)) continue;
2108
+ if (lookup && hasUniqueConstraint(lookup, unique.columns)) continue;
1709
2109
  const constraintName = unique.name ?? `${tableName}_${unique.columns.join("_")}_key`;
1710
2110
  operations.push({
1711
2111
  id: `unique.${tableName}.${constraintName}`,
@@ -1742,12 +2142,12 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
1742
2142
  }
1743
2143
  return operations;
1744
2144
  }
1745
- buildIndexOperations(tables, schema, schemaName) {
2145
+ buildIndexOperations(tables, schemaLookups, schemaName) {
1746
2146
  const operations = [];
1747
- for (const [tableName, table] of sortedEntries(tables)) {
1748
- const schemaTable = schema.tables[tableName];
2147
+ for (const [tableName, table] of tables) {
2148
+ const lookup = schemaLookups.get(tableName);
1749
2149
  for (const index of table.indexes) {
1750
- if (schemaTable && hasIndex(schemaTable, index.columns)) continue;
2150
+ if (lookup && hasIndex(lookup, index.columns)) continue;
1751
2151
  const indexName = index.name ?? `${tableName}_${index.columns.join("_")}_idx`;
1752
2152
  operations.push({
1753
2153
  id: `index.${tableName}.${indexName}`,
@@ -1779,15 +2179,15 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
1779
2179
  * Generates FK-backing index operations for FKs with `index: true`,
1780
2180
  * but only when no matching user-declared index exists in `contractTable.indexes`.
1781
2181
  */
1782
- buildFkBackingIndexOperations(tables, schema, schemaName) {
2182
+ buildFkBackingIndexOperations(tables, schemaLookups, schemaName) {
1783
2183
  const operations = [];
1784
- for (const [tableName, table] of sortedEntries(tables)) {
1785
- const schemaTable = schema.tables[tableName];
2184
+ for (const [tableName, table] of tables) {
2185
+ const lookup = schemaLookups.get(tableName);
1786
2186
  const declaredIndexColumns = new Set(table.indexes.map((idx) => idx.columns.join(",")));
1787
2187
  for (const fk of table.foreignKeys) {
1788
2188
  if (fk.index === false) continue;
1789
2189
  if (declaredIndexColumns.has(fk.columns.join(","))) continue;
1790
- if (schemaTable && hasIndex(schemaTable, fk.columns)) continue;
2190
+ if (lookup && hasIndex(lookup, fk.columns)) continue;
1791
2191
  const indexName = `${tableName}_${fk.columns.join("_")}_idx`;
1792
2192
  operations.push({
1793
2193
  id: `index.${tableName}.${indexName}`,
@@ -1815,13 +2215,13 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
1815
2215
  }
1816
2216
  return operations;
1817
2217
  }
1818
- buildForeignKeyOperations(tables, schema, schemaName) {
2218
+ buildForeignKeyOperations(tables, schemaLookups, schemaName) {
1819
2219
  const operations = [];
1820
- for (const [tableName, table] of sortedEntries(tables)) {
1821
- const schemaTable = schema.tables[tableName];
2220
+ for (const [tableName, table] of tables) {
2221
+ const lookup = schemaLookups.get(tableName);
1822
2222
  for (const foreignKey of table.foreignKeys) {
1823
2223
  if (foreignKey.constraint === false) continue;
1824
- if (schemaTable && hasForeignKey(schemaTable, foreignKey)) continue;
2224
+ if (lookup && hasForeignKey(lookup, foreignKey)) continue;
1825
2225
  const fkName = foreignKey.name ?? `${tableName}_${foreignKey.columns.join("_")}_fkey`;
1826
2226
  operations.push({
1827
2227
  id: `foreignKey.${tableName}.${fkName}`,
@@ -1857,62 +2257,27 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
1857
2257
  return operations;
1858
2258
  }
1859
2259
  buildTargetDetails(objectType, name, schema, table) {
2260
+ return buildTargetDetails(objectType, name, schema, table);
2261
+ }
2262
+ resolvePlanningMode(policy) {
2263
+ const allowWidening = policy.allowedOperationClasses.includes("widening");
2264
+ const allowDestructive = policy.allowedOperationClasses.includes("destructive");
1860
2265
  return {
1861
- schema,
1862
- objectType,
1863
- name,
1864
- ...ifDefined("table", table)
2266
+ includeExtraObjects: allowWidening || allowDestructive,
2267
+ allowWidening,
2268
+ allowDestructive
1865
2269
  };
1866
2270
  }
1867
- classifySchema(options) {
1868
- const verifyResult = verifySqlSchema({
2271
+ collectSchemaIssues(options, strict) {
2272
+ return verifySqlSchema({
1869
2273
  contract: options.contract,
1870
2274
  schema: options.schema,
1871
- strict: false,
2275
+ strict,
1872
2276
  typeMetadataRegistry: /* @__PURE__ */ new Map(),
1873
2277
  frameworkComponents: options.frameworkComponents,
1874
2278
  normalizeDefault: parsePostgresDefault,
1875
2279
  normalizeNativeType: normalizeSchemaNativeType
1876
- });
1877
- const conflicts = this.extractConflicts(verifyResult.schema.issues);
1878
- if (conflicts.length > 0) return {
1879
- kind: "conflict",
1880
- conflicts
1881
- };
1882
- return { kind: "ok" };
1883
- }
1884
- extractConflicts(issues) {
1885
- const conflicts = [];
1886
- for (const issue of issues) {
1887
- if (isAdditiveIssue(issue)) continue;
1888
- const conflict = this.convertIssueToConflict(issue);
1889
- if (conflict) conflicts.push(conflict);
1890
- }
1891
- return conflicts.sort(conflictComparator);
1892
- }
1893
- convertIssueToConflict(issue) {
1894
- switch (issue.kind) {
1895
- case "type_mismatch": return this.buildConflict("typeMismatch", issue);
1896
- case "nullability_mismatch": return this.buildConflict("nullabilityConflict", issue);
1897
- case "primary_key_mismatch": return this.buildConflict("indexIncompatible", issue);
1898
- case "unique_constraint_mismatch": return this.buildConflict("indexIncompatible", issue);
1899
- case "index_mismatch": return this.buildConflict("indexIncompatible", issue);
1900
- case "foreign_key_mismatch": return this.buildConflict("foreignKeyConflict", issue);
1901
- default: return null;
1902
- }
1903
- }
1904
- buildConflict(kind, issue) {
1905
- const location = buildConflictLocation(issue);
1906
- const meta = issue.expected || issue.actual ? Object.freeze({
1907
- ...ifDefined("expected", issue.expected),
1908
- ...ifDefined("actual", issue.actual)
1909
- }) : void 0;
1910
- return {
1911
- kind,
1912
- summary: issue.message,
1913
- ...ifDefined("location", location),
1914
- ...ifDefined("meta", meta)
1915
- };
2280
+ }).schema.issues;
1916
2281
  }
1917
2282
  };
1918
2283
  function isSqlDependencyProvider(component) {
@@ -1941,6 +2306,23 @@ function buildCreateTableSql(qualifiedTableName, table) {
1941
2306
  return `CREATE TABLE ${qualifiedTableName} (\n ${[...columnDefinitions, ...constraintDefinitions].join(",\n ")}\n)`;
1942
2307
  }
1943
2308
  /**
2309
+ * Pattern for safe PostgreSQL type names.
2310
+ * Allows letters, digits, underscores, spaces (for "double precision", "character varying"),
2311
+ * and trailing [] for array types.
2312
+ */
2313
+ const SAFE_NATIVE_TYPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9_ ]*(\[\])?$/;
2314
+ function assertSafeNativeType(nativeType) {
2315
+ if (!SAFE_NATIVE_TYPE_PATTERN.test(nativeType)) throw new Error(`Unsafe native type name in contract: "${nativeType}". Native type names must match /^[a-zA-Z][a-zA-Z0-9_ ]*(\\[\\])?\$/`);
2316
+ }
2317
+ /**
2318
+ * Sanity check against accidental SQL injection from malformed contract files.
2319
+ * Rejects semicolons, SQL comment tokens, and dollar-quoting.
2320
+ * Not a comprehensive security boundary — the contract is developer-authored.
2321
+ */
2322
+ function assertSafeDefaultExpression(expression) {
2323
+ if (expression.includes(";") || /--|\/\*|\$\$|\bSELECT\b/i.test(expression)) throw new Error(`Unsafe default expression in contract: "${expression}". Default expressions must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.`);
2324
+ }
2325
+ /**
1944
2326
  * Builds the column type SQL, handling autoincrement as a special case.
1945
2327
  * For autoincrement on int4/int8, we use SERIAL/BIGSERIAL types.
1946
2328
  */
@@ -1952,6 +2334,7 @@ function buildColumnTypeSql(column) {
1952
2334
  if (column.nativeType === "int2" || column.nativeType === "smallint") return "SMALLSERIAL";
1953
2335
  }
1954
2336
  if (column.typeRef) return quoteIdentifier(column.nativeType);
2337
+ assertSafeNativeType(column.nativeType);
1955
2338
  return renderParameterizedTypeSql(column) ?? column.nativeType;
1956
2339
  }
1957
2340
  /**
@@ -1982,7 +2365,8 @@ function buildColumnDefaultSql(columnDefault, column) {
1982
2365
  case "literal": return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
1983
2366
  case "function":
1984
2367
  if (columnDefault.expression === "autoincrement()") return "";
1985
- return `DEFAULT ${columnDefault.expression}`;
2368
+ assertSafeDefaultExpression(columnDefault.expression);
2369
+ return `DEFAULT (${columnDefault.expression})`;
1986
2370
  case "sequence": return `DEFAULT nextval(${quoteIdentifier(columnDefault.name)}::regclass)`;
1987
2371
  }
1988
2372
  }
@@ -2001,6 +2385,14 @@ function renderDefaultLiteral(value, column) {
2001
2385
  if (isJsonColumn) return `'${escapeLiteral(json)}'::${column.nativeType}`;
2002
2386
  return `'${escapeLiteral(json)}'`;
2003
2387
  }
2388
+ function buildTargetDetails(objectType, name, schema, table) {
2389
+ return {
2390
+ schema,
2391
+ objectType,
2392
+ name,
2393
+ ...ifDefined("table", table)
2394
+ };
2395
+ }
2004
2396
  function qualifyTableName(schema, table) {
2005
2397
  return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
2006
2398
  }
@@ -2027,14 +2419,15 @@ function columnExistsCheck({ schema, table, column, exists = true }) {
2027
2419
  AND column_name = '${escapeLiteral(column)}'
2028
2420
  )`;
2029
2421
  }
2030
- function columnIsNotNullCheck({ schema, table, column }) {
2422
+ function columnNullabilityCheck({ schema, table, column, nullable }) {
2423
+ const expected = nullable ? "YES" : "NO";
2031
2424
  return `SELECT EXISTS (
2032
2425
  SELECT 1
2033
2426
  FROM information_schema.columns
2034
2427
  WHERE table_schema = '${escapeLiteral(schema)}'
2035
2428
  AND table_name = '${escapeLiteral(table)}'
2036
2429
  AND column_name = '${escapeLiteral(column)}'
2037
- AND is_nullable = 'NO'
2430
+ AND is_nullable = '${expected}'
2038
2431
  )`;
2039
2432
  }
2040
2433
  function tableIsEmptyCheck(qualifiedTableName) {
@@ -2065,61 +2458,29 @@ function tableHasPrimaryKeyCheck(schema, table, exists, constraintName) {
2065
2458
  ${constraintFilter}
2066
2459
  )`;
2067
2460
  }
2068
- /**
2069
- * Checks if table has a unique constraint satisfied by the given columns.
2070
- * Uses shared semantic satisfaction predicate from verify-helpers.
2071
- */
2072
- function hasUniqueConstraint(table, columns) {
2073
- return isUniqueConstraintSatisfied(table.uniques, table.indexes, columns);
2074
- }
2075
- /**
2076
- * Checks if table has an index satisfied by the given columns.
2077
- * Uses shared semantic satisfaction predicate from verify-helpers.
2078
- */
2079
- function hasIndex(table, columns) {
2080
- return isIndexSatisfied(table.indexes, table.uniques, columns);
2081
- }
2082
- function hasForeignKey(table, fk) {
2083
- return table.foreignKeys.some((candidate) => arraysEqual(candidate.columns, fk.columns) && candidate.referencedTable === fk.references.table && arraysEqual(candidate.referencedColumns, fk.references.columns));
2461
+ function buildSchemaLookupMap(schema) {
2462
+ const map = /* @__PURE__ */ new Map();
2463
+ for (const [tableName, table] of Object.entries(schema.tables)) map.set(tableName, buildSchemaTableLookup(table));
2464
+ return map;
2084
2465
  }
2085
- function isAdditiveIssue(issue) {
2086
- switch (issue.kind) {
2087
- case "type_missing":
2088
- case "type_values_mismatch":
2089
- case "missing_table":
2090
- case "missing_column":
2091
- case "extension_missing": return true;
2092
- case "primary_key_mismatch": return issue.actual === void 0;
2093
- case "unique_constraint_mismatch":
2094
- case "index_mismatch":
2095
- case "foreign_key_mismatch": return issue.indexOrConstraint === void 0;
2096
- default: return false;
2097
- }
2466
+ function buildSchemaTableLookup(table) {
2467
+ return {
2468
+ uniqueKeys: new Set(table.uniques.map((u) => u.columns.join(","))),
2469
+ indexKeys: new Set(table.indexes.map((i) => i.columns.join(","))),
2470
+ uniqueIndexKeys: new Set(table.indexes.filter((i) => i.unique).map((i) => i.columns.join(","))),
2471
+ fkKeys: new Set(table.foreignKeys.map((fk) => `${fk.columns.join(",")}|${fk.referencedTable}|${fk.referencedColumns.join(",")}`))
2472
+ };
2098
2473
  }
2099
- function buildConflictLocation(issue) {
2100
- const location = {};
2101
- if (issue.table) location.table = issue.table;
2102
- if (issue.column) location.column = issue.column;
2103
- if (issue.indexOrConstraint) location.constraint = issue.indexOrConstraint;
2104
- return Object.keys(location).length > 0 ? location : void 0;
2474
+ function hasUniqueConstraint(lookup, columns) {
2475
+ const key = columns.join(",");
2476
+ return lookup.uniqueKeys.has(key) || lookup.uniqueIndexKeys.has(key);
2105
2477
  }
2106
- function conflictComparator(a, b) {
2107
- if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
2108
- const aLocation = a.location ?? {};
2109
- const bLocation = b.location ?? {};
2110
- const tableCompare = compareStrings(aLocation.table, bLocation.table);
2111
- if (tableCompare !== 0) return tableCompare;
2112
- const columnCompare = compareStrings(aLocation.column, bLocation.column);
2113
- if (columnCompare !== 0) return columnCompare;
2114
- const constraintCompare = compareStrings(aLocation.constraint, bLocation.constraint);
2115
- if (constraintCompare !== 0) return constraintCompare;
2116
- return compareStrings(a.summary, b.summary);
2478
+ function hasIndex(lookup, columns) {
2479
+ const key = columns.join(",");
2480
+ return lookup.indexKeys.has(key) || lookup.uniqueKeys.has(key);
2117
2481
  }
2118
- function compareStrings(a, b) {
2119
- if (a === b) return 0;
2120
- if (a === void 0) return -1;
2121
- if (b === void 0) return 1;
2122
- return a < b ? -1 : 1;
2482
+ function hasForeignKey(lookup, fk) {
2483
+ return lookup.fkKeys.has(`${fk.columns.join(",")}|${fk.references.table}|${fk.references.columns.join(",")}`);
2123
2484
  }
2124
2485
  const REFERENTIAL_ACTION_SQL = {
2125
2486
  noAction: "NO ACTION",
@@ -2483,14 +2844,7 @@ var PostgresMigrationRunner = class {
2483
2844
  }
2484
2845
  ensureMarkerCompatibility(marker, plan) {
2485
2846
  const origin = plan.origin ?? null;
2486
- if (!origin) {
2487
- if (!marker) return okVoid();
2488
- if (this.markerMatchesDestination(marker, plan)) return okVoid();
2489
- return runnerFailure("MARKER_ORIGIN_MISMATCH", `Existing contract marker (${marker.storageHash}) does not match plan origin (no marker expected).`, { meta: {
2490
- markerStorageHash: marker.storageHash,
2491
- expectedOrigin: null
2492
- } });
2493
- }
2847
+ if (!origin) return okVoid();
2494
2848
  if (!marker) return runnerFailure("MARKER_ORIGIN_MISMATCH", `Missing contract marker: expected origin storage hash ${origin.storageHash}.`, { meta: { expectedOriginStorageHash: origin.storageHash } });
2495
2849
  if (marker.storageHash !== origin.storageHash) return runnerFailure("MARKER_ORIGIN_MISMATCH", `Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`, { meta: {
2496
2850
  markerStorageHash: marker.storageHash,