@prisma-next/target-postgres 0.3.0-dev.113 → 0.3.0-dev.114

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
@@ -5,6 +5,7 @@ import { SQL_CHAR_CODEC_ID, SQL_FLOAT_CODEC_ID, SQL_INT_CODEC_ID, SQL_VARCHAR_CO
5
5
  import { arraysEqual, verifySqlSchema } from "@prisma-next/family-sql/schema-verify";
6
6
  import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
7
7
  import { bigintJsonReplacer, isTaggedBigInt } from "@prisma-next/contract/types";
8
+ import { invariant } from "@prisma-next/utils/assertions";
8
9
  import { readMarker } from "@prisma-next/family-sql/verify";
9
10
  import { SqlQueryError } from "@prisma-next/sql-errors";
10
11
  import { ok, okVoid } from "@prisma-next/utils/result";
@@ -393,7 +394,7 @@ function collectAllEnumColumns(contract, schema, typeName, nativeType) {
393
394
  /**
394
395
  * Builds a SQL check to verify a column's type matches an expected type.
395
396
  */
396
- function columnTypeCheck(options) {
397
+ function columnTypeCheck$1(options) {
397
398
  return `SELECT EXISTS (
398
399
  SELECT 1
399
400
  FROM information_schema.columns
@@ -480,7 +481,7 @@ USING ${quoteIdentifier(ref.column)}::text::${qualifiedTemp}`
480
481
  },
481
482
  ...columnRefs.map((ref) => ({
482
483
  description: `verify ${ref.table}.${ref.column} uses type "${options.nativeType}"`,
483
- sql: columnTypeCheck({
484
+ sql: columnTypeCheck$1({
484
485
  schemaName: options.schemaName,
485
486
  tableName: ref.table,
486
487
  columnName: ref.column,
@@ -2009,12 +2010,13 @@ function qualifyTableName(schema, table) {
2009
2010
  function toRegclassLiteral(schema, name) {
2010
2011
  return `'${escapeLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(name)}`)}'`;
2011
2012
  }
2012
- function constraintExistsCheck({ constraintName, schema, exists = true }) {
2013
+ function constraintExistsCheck({ constraintName, schema, table, exists = true }) {
2013
2014
  return `SELECT ${exists ? "EXISTS" : "NOT EXISTS"} (
2014
2015
  SELECT 1 FROM pg_constraint c
2015
2016
  JOIN pg_namespace n ON c.connamespace = n.oid
2016
2017
  WHERE c.conname = '${escapeLiteral(constraintName)}'
2017
2018
  AND n.nspname = '${escapeLiteral(schema)}'
2019
+ AND c.conrelid = to_regclass(${toRegclassLiteral(schema, table)})
2018
2020
  )`;
2019
2021
  }
2020
2022
  function columnExistsCheck({ schema, table, column, exists = true }) {
@@ -2037,6 +2039,70 @@ function columnNullabilityCheck({ schema, table, column, nullable }) {
2037
2039
  AND is_nullable = '${expected}'
2038
2040
  )`;
2039
2041
  }
2042
+ /**
2043
+ * Maps contract native type names to the display form returned by PostgreSQL's
2044
+ * `format_type()`. Base types use short names in the contract (e.g., `int4`)
2045
+ * but `format_type()` returns SQL-standard names (e.g., `integer`).
2046
+ *
2047
+ * NOTE: The inverse mapping lives in `normalizeFormattedType` in control-adapter.ts.
2048
+ * These two maps must stay in sync. A shared bidirectional map in
2049
+ * @prisma-next/adapter-postgres would eliminate the drift risk.
2050
+ */
2051
+ const FORMAT_TYPE_DISPLAY = new Map([
2052
+ ["int2", "smallint"],
2053
+ ["int4", "integer"],
2054
+ ["int8", "bigint"],
2055
+ ["float4", "real"],
2056
+ ["float8", "double precision"],
2057
+ ["bool", "boolean"],
2058
+ ["timestamp", "timestamp without time zone"],
2059
+ ["timestamptz", "timestamp with time zone"],
2060
+ ["time", "time without time zone"],
2061
+ ["timetz", "time with time zone"]
2062
+ ]);
2063
+ /**
2064
+ * Builds the string that `format_type(atttypid, atttypmod)` would return for a
2065
+ * contract column. Used for postchecks — separate from `buildColumnTypeSql` which
2066
+ * produces DDL-safe strings (e.g., quoted identifiers, SERIAL).
2067
+ */
2068
+ function buildExpectedFormatType(column, codecHooks) {
2069
+ if (column.typeParams && column.codecId) {
2070
+ const hooks = codecHooks.get(column.codecId);
2071
+ if (hooks?.expandNativeType) return hooks.expandNativeType({
2072
+ nativeType: column.nativeType,
2073
+ codecId: column.codecId,
2074
+ typeParams: column.typeParams
2075
+ });
2076
+ }
2077
+ if (column.typeRef) return column.nativeType !== column.nativeType.toLowerCase() ? `"${column.nativeType}"` : column.nativeType;
2078
+ return FORMAT_TYPE_DISPLAY.get(column.nativeType) ?? column.nativeType;
2079
+ }
2080
+ /** Checks that the column's full type (including typmods) matches the expected type via `format_type()`. */
2081
+ function columnTypeCheck({ schema, table, column, expectedType }) {
2082
+ return `SELECT EXISTS (
2083
+ SELECT 1
2084
+ FROM pg_attribute a
2085
+ JOIN pg_class c ON c.oid = a.attrelid
2086
+ JOIN pg_namespace n ON n.oid = c.relnamespace
2087
+ WHERE n.nspname = '${escapeLiteral(schema)}'
2088
+ AND c.relname = '${escapeLiteral(table)}'
2089
+ AND a.attname = '${escapeLiteral(column)}'
2090
+ AND format_type(a.atttypid, a.atttypmod) = '${escapeLiteral(expectedType)}'
2091
+ AND NOT a.attisdropped
2092
+ )`;
2093
+ }
2094
+ /** Checks that a column default exists (or does not exist) via `information_schema.columns.column_default`. */
2095
+ function columnDefaultExistsCheck({ schema, table, column, exists = true }) {
2096
+ const nullCheck = exists ? "IS NOT NULL" : "IS NULL";
2097
+ return `SELECT EXISTS (
2098
+ SELECT 1
2099
+ FROM information_schema.columns
2100
+ WHERE table_schema = '${escapeLiteral(schema)}'
2101
+ AND table_name = '${escapeLiteral(table)}'
2102
+ AND column_name = '${escapeLiteral(column)}'
2103
+ AND column_default ${nullCheck}
2104
+ )`;
2105
+ }
2040
2106
  function tableIsEmptyCheck(qualifiedTableName) {
2041
2107
  return `SELECT NOT EXISTS (SELECT 1 FROM ${qualifiedTableName} LIMIT 1)`;
2042
2108
  }
@@ -2243,6 +2309,25 @@ function buildReconciliationOperationFromIssue(options) {
2243
2309
  if (!contractColumn) return null;
2244
2310
  return buildAlterColumnTypeOperation(schemaName, issue.table, issue.column, contractColumn, codecHooks);
2245
2311
  }
2312
+ case "default_missing": {
2313
+ if (!issue.table || !issue.column) return null;
2314
+ const contractColMissing = getContractColumn(contract, issue.table, issue.column);
2315
+ if (!contractColMissing) return null;
2316
+ invariant(contractColMissing.default !== void 0, `default_missing issue for "${issue.table}"."${issue.column}" but contract column has no default`);
2317
+ return buildDefaultOperation(schemaName, issue.table, issue.column, contractColMissing, contractColMissing.default, "additive", "Set");
2318
+ }
2319
+ case "default_mismatch": {
2320
+ if (!issue.table || !issue.column) return null;
2321
+ if (!mode.allowWidening) return null;
2322
+ const contractColMismatch = getContractColumn(contract, issue.table, issue.column);
2323
+ if (!contractColMismatch) return null;
2324
+ invariant(contractColMismatch.default !== void 0, `default_mismatch issue for "${issue.table}"."${issue.column}" but contract column has no default`);
2325
+ return buildDefaultOperation(schemaName, issue.table, issue.column, contractColMismatch, contractColMismatch.default, "widening", "Change");
2326
+ }
2327
+ case "extra_default":
2328
+ if (!issue.table || !issue.column) return null;
2329
+ if (!mode.allowDestructive) return null;
2330
+ return buildDropDefaultOperation(schemaName, issue.table, issue.column);
2246
2331
  default: return null;
2247
2332
  }
2248
2333
  }
@@ -2346,7 +2431,8 @@ function buildDropConstraintOperation(schemaName, tableName, constraintName, con
2346
2431
  description: `ensure constraint "${constraintName}" exists`,
2347
2432
  sql: constraintExistsCheck({
2348
2433
  constraintName,
2349
- schema: schemaName
2434
+ schema: schemaName,
2435
+ table: tableName
2350
2436
  })
2351
2437
  }],
2352
2438
  execute: [{
@@ -2359,6 +2445,7 @@ DROP CONSTRAINT ${quoteIdentifier(constraintName)}`
2359
2445
  sql: constraintExistsCheck({
2360
2446
  constraintName,
2361
2447
  schema: schemaName,
2448
+ table: tableName,
2362
2449
  exists: false
2363
2450
  })
2364
2451
  }]
@@ -2472,12 +2559,84 @@ TYPE ${expectedType}
2472
2559
  USING ${quoteIdentifier(columnName)}::${expectedType}`
2473
2560
  }],
2474
2561
  postcheck: [{
2475
- description: `verify column "${columnName}" exists after type change`,
2562
+ description: `verify column "${columnName}" has type ${expectedType}`,
2563
+ sql: columnTypeCheck({
2564
+ schema: schemaName,
2565
+ table: tableName,
2566
+ column: columnName,
2567
+ expectedType: buildExpectedFormatType(column, codecHooks)
2568
+ })
2569
+ }]
2570
+ };
2571
+ }
2572
+ function buildDefaultOperation(schemaName, tableName, columnName, column, columnDefault, operationClass, verb) {
2573
+ const qualified = qualifyTableName(schemaName, tableName);
2574
+ const defaultClause = buildColumnDefaultSql(columnDefault, column);
2575
+ if (!defaultClause) return null;
2576
+ const verbLower = verb.toLowerCase();
2577
+ return {
2578
+ id: `setDefault.${tableName}.${columnName}`,
2579
+ label: `${verb} default for ${columnName} on ${tableName}`,
2580
+ summary: `${verb}s default on column ${columnName} of table ${tableName}`,
2581
+ operationClass,
2582
+ target: {
2583
+ id: "postgres",
2584
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
2585
+ },
2586
+ precheck: [{
2587
+ description: `ensure column "${columnName}" exists`,
2588
+ sql: columnExistsCheck({
2589
+ schema: schemaName,
2590
+ table: tableName,
2591
+ column: columnName
2592
+ })
2593
+ }],
2594
+ execute: [{
2595
+ description: `${verbLower} default on "${columnName}"`,
2596
+ sql: `ALTER TABLE ${qualified}\nALTER COLUMN ${quoteIdentifier(columnName)} SET ${defaultClause}`
2597
+ }],
2598
+ postcheck: [{
2599
+ description: `verify column "${columnName}" has a default`,
2600
+ sql: columnDefaultExistsCheck({
2601
+ schema: schemaName,
2602
+ table: tableName,
2603
+ column: columnName,
2604
+ exists: true
2605
+ })
2606
+ }]
2607
+ };
2608
+ }
2609
+ function buildDropDefaultOperation(schemaName, tableName, columnName) {
2610
+ const qualified = qualifyTableName(schemaName, tableName);
2611
+ return {
2612
+ id: `dropDefault.${tableName}.${columnName}`,
2613
+ label: `Drop default for ${columnName} on ${tableName}`,
2614
+ summary: `Drops default on column ${columnName} of table ${tableName}`,
2615
+ operationClass: "destructive",
2616
+ target: {
2617
+ id: "postgres",
2618
+ details: buildTargetDetails("column", columnName, schemaName, tableName)
2619
+ },
2620
+ precheck: [{
2621
+ description: `ensure column "${columnName}" exists`,
2476
2622
  sql: columnExistsCheck({
2477
2623
  schema: schemaName,
2478
2624
  table: tableName,
2479
2625
  column: columnName
2480
2626
  })
2627
+ }],
2628
+ execute: [{
2629
+ description: `drop default on "${columnName}"`,
2630
+ sql: `ALTER TABLE ${qualified}\nALTER COLUMN ${quoteIdentifier(columnName)} DROP DEFAULT`
2631
+ }],
2632
+ postcheck: [{
2633
+ description: `verify column "${columnName}" has no default`,
2634
+ sql: columnDefaultExistsCheck({
2635
+ schema: schemaName,
2636
+ table: tableName,
2637
+ column: columnName,
2638
+ exists: false
2639
+ })
2481
2640
  }]
2482
2641
  };
2483
2642
  }
@@ -2487,6 +2646,7 @@ function convertIssueToConflict(issue) {
2487
2646
  case "nullability_mismatch": return buildConflict("nullabilityConflict", issue);
2488
2647
  case "default_missing":
2489
2648
  case "default_mismatch":
2649
+ case "extra_default":
2490
2650
  case "extra_table":
2491
2651
  case "extra_column":
2492
2652
  case "extra_primary_key":
@@ -2832,6 +2992,7 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(", ")})`
2832
2992
  sql: constraintExistsCheck({
2833
2993
  constraintName,
2834
2994
  schema: schemaName,
2995
+ table: tableName,
2835
2996
  exists: false
2836
2997
  })
2837
2998
  }],
@@ -2845,7 +3006,8 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
2845
3006
  description: `verify unique constraint "${constraintName}" exists`,
2846
3007
  sql: constraintExistsCheck({
2847
3008
  constraintName,
2848
- schema: schemaName
3009
+ schema: schemaName,
3010
+ table: tableName
2849
3011
  })
2850
3012
  }]
2851
3013
  });
@@ -2948,6 +3110,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
2948
3110
  sql: constraintExistsCheck({
2949
3111
  constraintName: fkName,
2950
3112
  schema: schemaName,
3113
+ table: tableName,
2951
3114
  exists: false
2952
3115
  })
2953
3116
  }],
@@ -2959,7 +3122,8 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(", ")})`
2959
3122
  description: `verify foreign key "${fkName}" exists`,
2960
3123
  sql: constraintExistsCheck({
2961
3124
  constraintName: fkName,
2962
- schema: schemaName
3125
+ schema: schemaName,
3126
+ table: tableName
2963
3127
  })
2964
3128
  }]
2965
3129
  });