@prisma-next/adapter-postgres 0.12.0-dev.68 → 0.12.0-dev.69

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.
@@ -29,6 +29,7 @@ import type {
29
29
  import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
30
30
  import type {
31
31
  PrimaryKey,
32
+ SqlCheckConstraintIRInput,
32
33
  SqlColumnIR,
33
34
  SqlForeignKeyIR,
34
35
  SqlIndexIR,
@@ -660,32 +661,39 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
660
661
  driver: SqlControlDriverInstance<'postgres'>,
661
662
  schema: string,
662
663
  ): Promise<SqlSchemaIR> {
663
- // Execute all queries in parallel for efficiency (6 queries instead of 5T+1)
664
- const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult] =
665
- await Promise.all([
666
- // Query all tables
667
- driver.query<{ table_name: string }>(
668
- `SELECT table_name
664
+ // Execute all queries in parallel for efficiency (7 queries instead of 6T+1)
665
+ const [
666
+ tablesResult,
667
+ columnsResult,
668
+ pkResult,
669
+ fkResult,
670
+ uniqueResult,
671
+ indexResult,
672
+ checkResult,
673
+ ] = await Promise.all([
674
+ // Query all tables
675
+ driver.query<{ table_name: string }>(
676
+ `SELECT table_name
669
677
  FROM information_schema.tables
670
678
  WHERE table_schema = $1
671
679
  AND table_type = 'BASE TABLE'
672
680
  ORDER BY table_name`,
673
- [schema],
674
- ),
675
- // Query all columns for all tables in schema
676
- driver.query<{
677
- table_name: string;
678
- column_name: string;
679
- data_type: string;
680
- udt_name: string;
681
- is_nullable: string;
682
- character_maximum_length: number | null;
683
- numeric_precision: number | null;
684
- numeric_scale: number | null;
685
- column_default: string | null;
686
- formatted_type: string | null;
687
- }>(
688
- `SELECT
681
+ [schema],
682
+ ),
683
+ // Query all columns for all tables in schema
684
+ driver.query<{
685
+ table_name: string;
686
+ column_name: string;
687
+ data_type: string;
688
+ udt_name: string;
689
+ is_nullable: string;
690
+ character_maximum_length: number | null;
691
+ numeric_precision: number | null;
692
+ numeric_scale: number | null;
693
+ column_default: string | null;
694
+ formatted_type: string | null;
695
+ }>(
696
+ `SELECT
689
697
  c.table_name,
690
698
  column_name,
691
699
  data_type,
@@ -709,16 +717,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
709
717
  AND NOT a.attisdropped
710
718
  WHERE c.table_schema = $1
711
719
  ORDER BY c.table_name, c.ordinal_position`,
712
- [schema],
713
- ),
714
- // Query all primary keys for all tables in schema
715
- driver.query<{
716
- table_name: string;
717
- constraint_name: string;
718
- column_name: string;
719
- ordinal_position: number;
720
- }>(
721
- `SELECT
720
+ [schema],
721
+ ),
722
+ // Query all primary keys for all tables in schema
723
+ driver.query<{
724
+ table_name: string;
725
+ constraint_name: string;
726
+ column_name: string;
727
+ ordinal_position: number;
728
+ }>(
729
+ `SELECT
722
730
  tc.table_name,
723
731
  tc.constraint_name,
724
732
  kcu.column_name,
@@ -731,24 +739,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
731
739
  WHERE tc.table_schema = $1
732
740
  AND tc.constraint_type = 'PRIMARY KEY'
733
741
  ORDER BY tc.table_name, kcu.ordinal_position`,
734
- [schema],
735
- ),
736
- // Query all foreign keys for all tables in schema, including referential actions.
737
- // Uses pg_catalog for correct positional pairing of composite FK columns
738
- // (information_schema.constraint_column_usage lacks ordinal_position,
739
- // which causes Cartesian products for multi-column FKs).
740
- driver.query<{
741
- table_name: string;
742
- constraint_name: string;
743
- column_name: string;
744
- ordinal_position: number;
745
- referenced_table_schema: string;
746
- referenced_table_name: string;
747
- referenced_column_name: string;
748
- delete_rule: string;
749
- update_rule: string;
750
- }>(
751
- `SELECT
742
+ [schema],
743
+ ),
744
+ // Query all foreign keys for all tables in schema, including referential actions.
745
+ // Uses pg_catalog for correct positional pairing of composite FK columns
746
+ // (information_schema.constraint_column_usage lacks ordinal_position,
747
+ // which causes Cartesian products for multi-column FKs).
748
+ driver.query<{
749
+ table_name: string;
750
+ constraint_name: string;
751
+ column_name: string;
752
+ ordinal_position: number;
753
+ referenced_table_schema: string;
754
+ referenced_table_name: string;
755
+ referenced_column_name: string;
756
+ delete_rule: string;
757
+ update_rule: string;
758
+ }>(
759
+ `SELECT
752
760
  tc.table_name,
753
761
  tc.constraint_name,
754
762
  kcu.column_name,
@@ -781,16 +789,16 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
781
789
  WHERE tc.table_schema = $1
782
790
  AND tc.constraint_type = 'FOREIGN KEY'
783
791
  ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
784
- [schema],
785
- ),
786
- // Query all unique constraints for all tables in schema (excluding PKs)
787
- driver.query<{
788
- table_name: string;
789
- constraint_name: string;
790
- column_name: string;
791
- ordinal_position: number;
792
- }>(
793
- `SELECT
792
+ [schema],
793
+ ),
794
+ // Query all unique constraints for all tables in schema (excluding PKs)
795
+ driver.query<{
796
+ table_name: string;
797
+ constraint_name: string;
798
+ column_name: string;
799
+ ordinal_position: number;
800
+ }>(
801
+ `SELECT
794
802
  tc.table_name,
795
803
  tc.constraint_name,
796
804
  kcu.column_name,
@@ -803,31 +811,31 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
803
811
  WHERE tc.table_schema = $1
804
812
  AND tc.constraint_type = 'UNIQUE'
805
813
  ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
806
- [schema],
807
- ),
808
- // Query all indexes for all tables in schema (excluding constraints).
809
- // `index_position` is the column's position within the index (1-based),
810
- // derived from `pg_index.indkey` so composite indexes round-trip with
811
- // their declared column order intact.
812
- driver.query<{
813
- tablename: string;
814
- indexname: string;
815
- indisunique: boolean;
816
- attname: string | null;
817
- index_position: number;
818
- amname: string | null;
819
- reloptions: string[] | null;
820
- }>(
821
- // `ix.indkey` is an int2vector of column numbers in the order the
822
- // columns appear in the index definition. Unnest it WITH ORDINALITY
823
- // so each (index, column) row carries its position in the index,
824
- // then ORDER BY that position. Without this the rows come back in
825
- // table-column order (`a.attnum`), which silently shuffles the
826
- // columns of any composite index whose index order differs from
827
- // the table order — verification compares against the contract
828
- // with order-sensitive equality and reports a spurious
829
- // `index_mismatch`.
830
- `SELECT
814
+ [schema],
815
+ ),
816
+ // Query all indexes for all tables in schema (excluding constraints).
817
+ // `index_position` is the column's position within the index (1-based),
818
+ // derived from `pg_index.indkey` so composite indexes round-trip with
819
+ // their declared column order intact.
820
+ driver.query<{
821
+ tablename: string;
822
+ indexname: string;
823
+ indisunique: boolean;
824
+ attname: string | null;
825
+ index_position: number;
826
+ amname: string | null;
827
+ reloptions: string[] | null;
828
+ }>(
829
+ // `ix.indkey` is an int2vector of column numbers in the order the
830
+ // columns appear in the index definition. Unnest it WITH ORDINALITY
831
+ // so each (index, column) row carries its position in the index,
832
+ // then ORDER BY that position. Without this the rows come back in
833
+ // table-column order (`a.attnum`), which silently shuffles the
834
+ // columns of any composite index whose index order differs from
835
+ // the table order — verification compares against the contract
836
+ // with order-sensitive equality and reports a spurious
837
+ // `index_mismatch`.
838
+ `SELECT
831
839
  i.tablename,
832
840
  i.indexname,
833
841
  ix.indisunique,
@@ -853,9 +861,34 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
853
861
  AND tc.constraint_name = i.indexname
854
862
  )
855
863
  ORDER BY i.tablename, i.indexname, k.ord`,
856
- [schema],
857
- ),
858
- ]);
864
+ [schema],
865
+ ),
866
+ // Query all check constraints for enum-restricted columns.
867
+ // `pg_get_constraintdef(oid)` returns the predicate including the
868
+ // `CHECK (...)` wrapper. We parse the inner predicate to extract
869
+ // the column name and permitted values.
870
+ //
871
+ // Scope: only parses the `= ANY (ARRAY[...])` and `IN (...)` shapes
872
+ // that this slice emits. Arbitrary SQL predicates are left as-is
873
+ // and will not produce check IR entries (they are silently skipped).
874
+ driver.query<{
875
+ table_name: string;
876
+ constraint_name: string;
877
+ constraintdef: string;
878
+ }>(
879
+ `SELECT
880
+ cl.relname AS table_name,
881
+ c.conname AS constraint_name,
882
+ pg_get_constraintdef(c.oid) AS constraintdef
883
+ FROM pg_catalog.pg_constraint c
884
+ JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
885
+ JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
886
+ WHERE ns.nspname = $1
887
+ AND c.contype = 'c'
888
+ ORDER BY cl.relname, c.conname`,
889
+ [schema],
890
+ ),
891
+ ]);
859
892
 
860
893
  // Group results by table name for efficient lookup
861
894
  const columnsByTable = groupBy(columnsResult.rows, 'table_name');
@@ -863,6 +896,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
863
896
  const fksByTable = groupBy(fkResult.rows, 'table_name');
864
897
  const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
865
898
  const indexesByTable = groupBy(indexResult.rows, 'tablename');
899
+ const checksByTable = groupBy(checkResult.rows, 'table_name');
866
900
 
867
901
  // Get set of PK constraint names per table (to exclude from uniques)
868
902
  const pkConstraintsByTable = new Map<string, Set<string>>();
@@ -1034,6 +1068,21 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1034
1068
  ...(idx.options !== undefined && { options: idx.options }),
1035
1069
  }));
1036
1070
 
1071
+ // Process check constraints — parse each predicate into column + value set.
1072
+ // Only the two shapes emitted by this slice are recognised; free-form
1073
+ // predicates are silently skipped (they won't produce check IR entries).
1074
+ const checksForTable: SqlCheckConstraintIRInput[] = [];
1075
+ for (const checkRow of checksByTable.get(tableName) ?? []) {
1076
+ const parsed = parseCheckConstraintDef(checkRow.constraintdef);
1077
+ if (parsed) {
1078
+ checksForTable.push({
1079
+ name: checkRow.constraint_name,
1080
+ column: parsed.column,
1081
+ permittedValues: parsed.permittedValues,
1082
+ });
1083
+ }
1084
+ }
1085
+
1037
1086
  tables[tableName] = {
1038
1087
  name: tableName,
1039
1088
  columns,
@@ -1041,6 +1090,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
1041
1090
  foreignKeys,
1042
1091
  uniques,
1043
1092
  indexes,
1093
+ ...ifDefined('checks', checksForTable.length > 0 ? checksForTable : undefined),
1044
1094
  };
1045
1095
  }
1046
1096
 
@@ -1222,3 +1272,100 @@ function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T
1222
1272
  }
1223
1273
  return map;
1224
1274
  }
1275
+
1276
+ /**
1277
+ * Parses a Postgres check-constraint definition string (as returned by
1278
+ * `pg_get_constraintdef`) into a column name and permitted values array.
1279
+ *
1280
+ * Handles two shapes that Postgres emits for enum-style checks:
1281
+ *
1282
+ * 1. `= ANY (ARRAY[...])` — Postgres rewrites `col IN ('a','b')` to this form:
1283
+ * `CHECK ((col = ANY (ARRAY['a'::text, 'b'::text])))`
1284
+ *
1285
+ * 2. `IN (...)` — stays as-is when written directly:
1286
+ * `CHECK ((col IN ('a', 'b')))`
1287
+ *
1288
+ * Column names may be plain identifiers (`status`) or double-quoted identifiers
1289
+ * (`"my-col"`). Double-quoted identifiers with embedded `""` are un-escaped to a
1290
+ * single `"`.
1291
+ *
1292
+ * String literal values may contain Postgres-style doubled single-quotes (`''`),
1293
+ * which are un-escaped to a single `'` (e.g. `O''Brien` → `O'Brien`).
1294
+ *
1295
+ * Returns `{ column, permittedValues }` when the predicate matches one of
1296
+ * the two recognised shapes. Returns `undefined` for anything else (e.g.
1297
+ * a free-form SQL predicate that wasn't emitted by this slice).
1298
+ */
1299
+ export function parseCheckConstraintDef(
1300
+ constraintdef: string,
1301
+ ): { column: string; permittedValues: readonly string[] } | undefined {
1302
+ // Strip outer `CHECK (...)` wrapper and any extra parentheses.
1303
+ // pg_get_constraintdef returns e.g. `CHECK ((col = ANY (ARRAY[...])))` — note
1304
+ // the double parens: one from CHECK and one that Postgres wraps the predicate
1305
+ // in. Strip both outer layers.
1306
+ const afterCheck = constraintdef
1307
+ .replace(/^CHECK\s*\(/i, '')
1308
+ .replace(/\)$/, '')
1309
+ .trim();
1310
+ // Strip one more optional paren pair (the inner wrap Postgres adds)
1311
+ const inner =
1312
+ afterCheck.startsWith('(') && afterCheck.endsWith(')')
1313
+ ? afterCheck.slice(1, -1).trim()
1314
+ : afterCheck;
1315
+
1316
+ // Shape 1: col = ANY (ARRAY['a'::text, 'b'::text])
1317
+ // Accepts both plain identifiers and double-quoted identifiers for the column.
1318
+ // Anchored at the end so a composite predicate (e.g. `col = ANY (...) AND x > 0`)
1319
+ // does not partial-match.
1320
+ const anyArrayMatch = inner.match(
1321
+ /^(?:"((?:[^"]|"")*)"|(\w+))\s*=\s*ANY\s*\(\s*ARRAY\s*\[(.+)\]\s*\)\s*$/i,
1322
+ );
1323
+ if (anyArrayMatch) {
1324
+ const column =
1325
+ anyArrayMatch[1] !== undefined ? anyArrayMatch[1].replace(/""/g, '"') : anyArrayMatch[2];
1326
+ const arrayBody = anyArrayMatch[3];
1327
+ if (!column || !arrayBody) return undefined;
1328
+ const permittedValues = extractArrayLiterals(arrayBody);
1329
+ return permittedValues ? { column, permittedValues } : undefined;
1330
+ }
1331
+
1332
+ // Shape 2: col IN ('a', 'b')
1333
+ // Accepts both plain identifiers and double-quoted identifiers for the column.
1334
+ // Anchored at the end so a composite predicate (e.g. `col IN (...) AND x > 0`)
1335
+ // does not partial-match.
1336
+ const inMatch = inner.match(/^(?:"((?:[^"]|"")*)"|(\w+))\s+IN\s*\((.+)\)\s*$/i);
1337
+ if (inMatch) {
1338
+ const column = inMatch[1] !== undefined ? inMatch[1].replace(/""/g, '"') : inMatch[2];
1339
+ const listBody = inMatch[3];
1340
+ if (!column || !listBody) return undefined;
1341
+ const permittedValues = extractQuotedLiterals(listBody);
1342
+ return permittedValues ? { column, permittedValues } : undefined;
1343
+ }
1344
+
1345
+ return undefined;
1346
+ }
1347
+
1348
+ /**
1349
+ * Extracts string literals from an `ARRAY[...]` body.
1350
+ * Handles `'value'::type` casts by stripping the cast part.
1351
+ * Postgres stores single quotes inside values as doubled single-quotes (`''`);
1352
+ * each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
1353
+ */
1354
+ function extractArrayLiterals(arrayBody: string): readonly string[] | undefined {
1355
+ // Match 'value'::cast or 'value' (with possible spaces)
1356
+ const pattern = /'((?:[^'\\]|\\.|'')*)'\s*(?:::[^\s,\]]+)?/g;
1357
+ const values = [...arrayBody.matchAll(pattern)].map((m) => (m[1] ?? '').replace(/''/g, "'"));
1358
+ return values.length > 0 ? values : undefined;
1359
+ }
1360
+
1361
+ /**
1362
+ * Extracts string literals from an `IN (...)` list.
1363
+ * Handles single-quoted literals with possible escaped quotes.
1364
+ * Postgres stores single quotes inside values as doubled single-quotes (`''`);
1365
+ * each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
1366
+ */
1367
+ function extractQuotedLiterals(listBody: string): readonly string[] | undefined {
1368
+ const pattern = /'((?:[^'\\]|\\.|'')*)'/g;
1369
+ const values = [...listBody.matchAll(pattern)].map((m) => (m[1] ?? '').replace(/''/g, "'"));
1370
+ return values.length > 0 ? values : undefined;
1371
+ }