@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.
- package/dist/{adapter-HCWARoL5.mjs → adapter-BpvkORJG.mjs} +2 -2
- package/dist/{adapter-HCWARoL5.mjs.map → adapter-BpvkORJG.mjs.map} +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/{control-adapter-CFnygVW-.mjs → control-adapter-BmVmHzER.mjs} +94 -4
- package/dist/control-adapter-BmVmHzER.mjs.map +1 -0
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1 -1
- package/dist/runtime.mjs +1 -1
- package/package.json +22 -22
- package/src/core/control-adapter.ts +235 -88
- package/dist/control-adapter-CFnygVW-.mjs.map +0 -1
|
@@ -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 (
|
|
664
|
-
const [
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
+
}
|