@prisma-next/adapter-postgres 0.3.0-dev.6 → 0.3.0-dev.64
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/LICENSE +201 -0
- package/README.md +64 -2
- package/dist/adapter-DtehReRR.mjs +271 -0
- package/dist/adapter-DtehReRR.mjs.map +1 -0
- package/dist/adapter.d.mts +23 -0
- package/dist/adapter.d.mts.map +1 -0
- package/dist/adapter.mjs +5 -0
- package/dist/codec-ids-Bsm9c7ns.mjs +29 -0
- package/dist/codec-ids-Bsm9c7ns.mjs.map +1 -0
- package/dist/codec-types.d.mts +141 -0
- package/dist/codec-types.d.mts.map +1 -0
- package/dist/codec-types.mjs +4 -0
- package/dist/codecs-BfC_5c-4.mjs +207 -0
- package/dist/codecs-BfC_5c-4.mjs.map +1 -0
- package/dist/column-types.d.mts +110 -0
- package/dist/column-types.d.mts.map +1 -0
- package/dist/column-types.mjs +180 -0
- package/dist/column-types.mjs.map +1 -0
- package/dist/control.d.mts +111 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +463 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-ilnFI7bx.mjs +921 -0
- package/dist/descriptor-meta-ilnFI7bx.mjs.map +1 -0
- package/dist/runtime.d.mts +19 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +85 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/sql-utils-CSfAGEwF.mjs +78 -0
- package/dist/sql-utils-CSfAGEwF.mjs.map +1 -0
- package/dist/types-CXO7EB60.d.mts +19 -0
- package/dist/types-CXO7EB60.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +37 -46
- package/src/core/adapter.ts +139 -28
- package/src/core/codec-ids.ts +28 -0
- package/src/core/codecs.ts +325 -23
- package/src/core/control-adapter.ts +400 -178
- package/src/core/default-normalizer.ts +90 -0
- package/src/core/descriptor-meta.ts +221 -9
- package/src/core/enum-control-hooks.ts +735 -0
- package/src/core/json-schema-type-expression.ts +131 -0
- package/src/core/json-schema-validator.ts +53 -0
- package/src/core/parameterized-types.ts +118 -0
- package/src/core/sql-utils.ts +111 -0
- package/src/core/standard-schema.ts +71 -0
- package/src/exports/codec-types.ts +73 -1
- package/src/exports/column-types.ts +233 -9
- package/src/exports/control.ts +16 -9
- package/src/exports/runtime.ts +61 -18
- package/dist/chunk-HD5YISNQ.js +0 -47
- package/dist/chunk-HD5YISNQ.js.map +0 -1
- package/dist/chunk-J3XSOAM2.js +0 -162
- package/dist/chunk-J3XSOAM2.js.map +0 -1
- package/dist/chunk-T6S3A6VT.js +0 -301
- package/dist/chunk-T6S3A6VT.js.map +0 -1
- package/dist/core/adapter.d.ts +0 -19
- package/dist/core/adapter.d.ts.map +0 -1
- package/dist/core/codecs.d.ts +0 -110
- package/dist/core/codecs.d.ts.map +0 -1
- package/dist/core/control-adapter.d.ts +0 -33
- package/dist/core/control-adapter.d.ts.map +0 -1
- package/dist/core/descriptor-meta.d.ts +0 -72
- package/dist/core/descriptor-meta.d.ts.map +0 -1
- package/dist/core/types.d.ts +0 -16
- package/dist/core/types.d.ts.map +0 -1
- package/dist/exports/adapter.d.ts +0 -2
- package/dist/exports/adapter.d.ts.map +0 -1
- package/dist/exports/adapter.js +0 -8
- package/dist/exports/adapter.js.map +0 -1
- package/dist/exports/codec-types.d.ts +0 -11
- package/dist/exports/codec-types.d.ts.map +0 -1
- package/dist/exports/codec-types.js +0 -7
- package/dist/exports/codec-types.js.map +0 -1
- package/dist/exports/column-types.d.ts +0 -17
- package/dist/exports/column-types.d.ts.map +0 -1
- package/dist/exports/column-types.js +0 -49
- package/dist/exports/column-types.js.map +0 -1
- package/dist/exports/control.d.ts +0 -8
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -279
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -15
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -20
- package/dist/exports/runtime.js.map +0 -1
- package/dist/exports/types.d.ts +0 -2
- package/dist/exports/types.d.ts.map +0 -1
- package/dist/exports/types.js +0 -1
- package/dist/exports/types.js.map +0 -1
|
@@ -5,10 +5,14 @@ import type {
|
|
|
5
5
|
SqlColumnIR,
|
|
6
6
|
SqlForeignKeyIR,
|
|
7
7
|
SqlIndexIR,
|
|
8
|
+
SqlReferentialAction,
|
|
8
9
|
SqlSchemaIR,
|
|
9
10
|
SqlTableIR,
|
|
10
11
|
SqlUniqueIR,
|
|
11
12
|
} from '@prisma-next/sql-schema-ir/types';
|
|
13
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
14
|
+
import { parsePostgresDefault } from './default-normalizer';
|
|
15
|
+
import { pgEnumControlHooks } from './enum-control-hooks';
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Postgres control plane adapter for control-plane operations like introspection.
|
|
@@ -22,6 +26,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
22
26
|
*/
|
|
23
27
|
readonly target = 'postgres' as const;
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Target-specific normalizer for raw Postgres default expressions.
|
|
31
|
+
* Used by schema verification to normalize raw defaults before comparison.
|
|
32
|
+
*/
|
|
33
|
+
readonly normalizeDefault = parsePostgresDefault;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Target-specific normalizer for Postgres schema native type names.
|
|
37
|
+
* Used by schema verification to normalize introspected type names
|
|
38
|
+
* before comparison with contract native types.
|
|
39
|
+
*/
|
|
40
|
+
readonly normalizeNativeType = normalizeSchemaNativeType;
|
|
41
|
+
|
|
25
42
|
/**
|
|
26
43
|
* Introspects a Postgres database schema and returns a raw SqlSchemaIR.
|
|
27
44
|
*
|
|
@@ -29,6 +46,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
29
46
|
* and returns the schema structure without type mapping or contract enrichment.
|
|
30
47
|
* Type mapping and enrichment are handled separately by enrichment helpers.
|
|
31
48
|
*
|
|
49
|
+
* Uses batched queries to minimize database round trips (7 queries instead of 5T+3).
|
|
50
|
+
*
|
|
32
51
|
* @param driver - ControlDriverInstance<'sql', 'postgres'> instance for executing queries
|
|
33
52
|
* @param contractIR - Optional contract IR for contract-guided introspection (filtering, optimization)
|
|
34
53
|
* @param schema - Schema name to introspect (defaults to 'public')
|
|
@@ -39,25 +58,28 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
39
58
|
_contractIR?: unknown,
|
|
40
59
|
schema = 'public',
|
|
41
60
|
): Promise<SqlSchemaIR> {
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
// Execute all queries in parallel for efficiency (7 queries instead of 5T+3)
|
|
62
|
+
const [
|
|
63
|
+
tablesResult,
|
|
64
|
+
columnsResult,
|
|
65
|
+
pkResult,
|
|
66
|
+
fkResult,
|
|
67
|
+
uniqueResult,
|
|
68
|
+
indexResult,
|
|
69
|
+
extensionsResult,
|
|
70
|
+
] = await Promise.all([
|
|
71
|
+
// Query all tables
|
|
72
|
+
driver.query<{ table_name: string }>(
|
|
73
|
+
`SELECT table_name
|
|
74
|
+
FROM information_schema.tables
|
|
75
|
+
WHERE table_schema = $1
|
|
76
|
+
AND table_type = 'BASE TABLE'
|
|
77
|
+
ORDER BY table_name`,
|
|
78
|
+
[schema],
|
|
79
|
+
),
|
|
80
|
+
// Query all columns for all tables in schema
|
|
81
|
+
driver.query<{
|
|
82
|
+
table_name: string;
|
|
61
83
|
column_name: string;
|
|
62
84
|
data_type: string;
|
|
63
85
|
udt_name: string;
|
|
@@ -65,27 +87,203 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
65
87
|
character_maximum_length: number | null;
|
|
66
88
|
numeric_precision: number | null;
|
|
67
89
|
numeric_scale: number | null;
|
|
90
|
+
column_default: string | null;
|
|
91
|
+
formatted_type: string | null;
|
|
68
92
|
}>(
|
|
69
93
|
`SELECT
|
|
94
|
+
c.table_name,
|
|
70
95
|
column_name,
|
|
71
96
|
data_type,
|
|
72
97
|
udt_name,
|
|
73
98
|
is_nullable,
|
|
74
99
|
character_maximum_length,
|
|
75
100
|
numeric_precision,
|
|
76
|
-
numeric_scale
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
numeric_scale,
|
|
102
|
+
column_default,
|
|
103
|
+
format_type(a.atttypid, a.atttypmod) AS formatted_type
|
|
104
|
+
FROM information_schema.columns c
|
|
105
|
+
JOIN pg_catalog.pg_class cl
|
|
106
|
+
ON cl.relname = c.table_name
|
|
107
|
+
JOIN pg_catalog.pg_namespace ns
|
|
108
|
+
ON ns.nspname = c.table_schema
|
|
109
|
+
AND ns.oid = cl.relnamespace
|
|
110
|
+
JOIN pg_catalog.pg_attribute a
|
|
111
|
+
ON a.attrelid = cl.oid
|
|
112
|
+
AND a.attname = c.column_name
|
|
113
|
+
AND a.attnum > 0
|
|
114
|
+
AND NOT a.attisdropped
|
|
115
|
+
WHERE c.table_schema = $1
|
|
116
|
+
ORDER BY c.table_name, c.ordinal_position`,
|
|
117
|
+
[schema],
|
|
118
|
+
),
|
|
119
|
+
// Query all primary keys for all tables in schema
|
|
120
|
+
driver.query<{
|
|
121
|
+
table_name: string;
|
|
122
|
+
constraint_name: string;
|
|
123
|
+
column_name: string;
|
|
124
|
+
ordinal_position: number;
|
|
125
|
+
}>(
|
|
126
|
+
`SELECT
|
|
127
|
+
tc.table_name,
|
|
128
|
+
tc.constraint_name,
|
|
129
|
+
kcu.column_name,
|
|
130
|
+
kcu.ordinal_position
|
|
131
|
+
FROM information_schema.table_constraints tc
|
|
132
|
+
JOIN information_schema.key_column_usage kcu
|
|
133
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
134
|
+
AND tc.table_schema = kcu.table_schema
|
|
135
|
+
AND tc.table_name = kcu.table_name
|
|
136
|
+
WHERE tc.table_schema = $1
|
|
137
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
138
|
+
ORDER BY tc.table_name, kcu.ordinal_position`,
|
|
139
|
+
[schema],
|
|
140
|
+
),
|
|
141
|
+
// Query all foreign keys for all tables in schema, including referential actions.
|
|
142
|
+
// Uses pg_catalog for correct positional pairing of composite FK columns
|
|
143
|
+
// (information_schema.constraint_column_usage lacks ordinal_position,
|
|
144
|
+
// which causes Cartesian products for multi-column FKs).
|
|
145
|
+
driver.query<{
|
|
146
|
+
table_name: string;
|
|
147
|
+
constraint_name: string;
|
|
148
|
+
column_name: string;
|
|
149
|
+
ordinal_position: number;
|
|
150
|
+
referenced_table_schema: string;
|
|
151
|
+
referenced_table_name: string;
|
|
152
|
+
referenced_column_name: string;
|
|
153
|
+
delete_rule: string;
|
|
154
|
+
update_rule: string;
|
|
155
|
+
}>(
|
|
156
|
+
`SELECT
|
|
157
|
+
tc.table_name,
|
|
158
|
+
tc.constraint_name,
|
|
159
|
+
kcu.column_name,
|
|
160
|
+
kcu.ordinal_position,
|
|
161
|
+
ref_ns.nspname AS referenced_table_schema,
|
|
162
|
+
ref_cl.relname AS referenced_table_name,
|
|
163
|
+
ref_att.attname AS referenced_column_name,
|
|
164
|
+
rc.delete_rule,
|
|
165
|
+
rc.update_rule
|
|
166
|
+
FROM information_schema.table_constraints tc
|
|
167
|
+
JOIN information_schema.key_column_usage kcu
|
|
168
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
169
|
+
AND tc.table_schema = kcu.table_schema
|
|
170
|
+
AND tc.table_name = kcu.table_name
|
|
171
|
+
JOIN pg_catalog.pg_constraint pgc
|
|
172
|
+
ON pgc.conname = tc.constraint_name
|
|
173
|
+
AND pgc.connamespace = (
|
|
174
|
+
SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
|
|
175
|
+
)
|
|
176
|
+
JOIN pg_catalog.pg_class ref_cl
|
|
177
|
+
ON ref_cl.oid = pgc.confrelid
|
|
178
|
+
JOIN pg_catalog.pg_namespace ref_ns
|
|
179
|
+
ON ref_ns.oid = ref_cl.relnamespace
|
|
180
|
+
JOIN pg_catalog.pg_attribute ref_att
|
|
181
|
+
ON ref_att.attrelid = pgc.confrelid
|
|
182
|
+
AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
|
|
183
|
+
JOIN information_schema.referential_constraints rc
|
|
184
|
+
ON rc.constraint_name = tc.constraint_name
|
|
185
|
+
AND rc.constraint_schema = tc.table_schema
|
|
186
|
+
WHERE tc.table_schema = $1
|
|
187
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
188
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
189
|
+
[schema],
|
|
190
|
+
),
|
|
191
|
+
// Query all unique constraints for all tables in schema (excluding PKs)
|
|
192
|
+
driver.query<{
|
|
193
|
+
table_name: string;
|
|
194
|
+
constraint_name: string;
|
|
195
|
+
column_name: string;
|
|
196
|
+
ordinal_position: number;
|
|
197
|
+
}>(
|
|
198
|
+
`SELECT
|
|
199
|
+
tc.table_name,
|
|
200
|
+
tc.constraint_name,
|
|
201
|
+
kcu.column_name,
|
|
202
|
+
kcu.ordinal_position
|
|
203
|
+
FROM information_schema.table_constraints tc
|
|
204
|
+
JOIN information_schema.key_column_usage kcu
|
|
205
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
206
|
+
AND tc.table_schema = kcu.table_schema
|
|
207
|
+
AND tc.table_name = kcu.table_name
|
|
208
|
+
WHERE tc.table_schema = $1
|
|
209
|
+
AND tc.constraint_type = 'UNIQUE'
|
|
210
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
211
|
+
[schema],
|
|
212
|
+
),
|
|
213
|
+
// Query all indexes for all tables in schema (excluding constraints)
|
|
214
|
+
driver.query<{
|
|
215
|
+
tablename: string;
|
|
216
|
+
indexname: string;
|
|
217
|
+
indisunique: boolean;
|
|
218
|
+
attname: string;
|
|
219
|
+
attnum: number;
|
|
220
|
+
}>(
|
|
221
|
+
`SELECT
|
|
222
|
+
i.tablename,
|
|
223
|
+
i.indexname,
|
|
224
|
+
ix.indisunique,
|
|
225
|
+
a.attname,
|
|
226
|
+
a.attnum
|
|
227
|
+
FROM pg_indexes i
|
|
228
|
+
JOIN pg_class ic ON ic.relname = i.indexname
|
|
229
|
+
JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
|
|
230
|
+
JOIN pg_index ix ON ix.indexrelid = ic.oid
|
|
231
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
232
|
+
JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
|
|
233
|
+
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
|
|
234
|
+
WHERE i.schemaname = $1
|
|
235
|
+
AND NOT EXISTS (
|
|
236
|
+
SELECT 1
|
|
237
|
+
FROM information_schema.table_constraints tc
|
|
238
|
+
WHERE tc.table_schema = $1
|
|
239
|
+
AND tc.table_name = i.tablename
|
|
240
|
+
AND tc.constraint_name = i.indexname
|
|
241
|
+
)
|
|
242
|
+
ORDER BY i.tablename, i.indexname, a.attnum`,
|
|
243
|
+
[schema],
|
|
244
|
+
),
|
|
245
|
+
// Query extensions
|
|
246
|
+
driver.query<{ extname: string }>(
|
|
247
|
+
`SELECT extname
|
|
248
|
+
FROM pg_extension
|
|
249
|
+
ORDER BY extname`,
|
|
250
|
+
[],
|
|
251
|
+
),
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
// Group results by table name for efficient lookup
|
|
255
|
+
const columnsByTable = groupBy(columnsResult.rows, 'table_name');
|
|
256
|
+
const pksByTable = groupBy(pkResult.rows, 'table_name');
|
|
257
|
+
const fksByTable = groupBy(fkResult.rows, 'table_name');
|
|
258
|
+
const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
|
|
259
|
+
const indexesByTable = groupBy(indexResult.rows, 'tablename');
|
|
83
260
|
|
|
261
|
+
// Get set of PK constraint names per table (to exclude from uniques)
|
|
262
|
+
const pkConstraintsByTable = new Map<string, Set<string>>();
|
|
263
|
+
for (const row of pkResult.rows) {
|
|
264
|
+
let constraints = pkConstraintsByTable.get(row.table_name);
|
|
265
|
+
if (!constraints) {
|
|
266
|
+
constraints = new Set();
|
|
267
|
+
pkConstraintsByTable.set(row.table_name, constraints);
|
|
268
|
+
}
|
|
269
|
+
constraints.add(row.constraint_name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
273
|
+
|
|
274
|
+
for (const tableRow of tablesResult.rows) {
|
|
275
|
+
const tableName = tableRow.table_name;
|
|
276
|
+
|
|
277
|
+
// Process columns for this table
|
|
84
278
|
const columns: Record<string, SqlColumnIR> = {};
|
|
85
|
-
for (const colRow of
|
|
86
|
-
// Build native type string from catalog data
|
|
279
|
+
for (const colRow of columnsByTable.get(tableName) ?? []) {
|
|
87
280
|
let nativeType = colRow.udt_name;
|
|
88
|
-
|
|
281
|
+
const formattedType = colRow.formatted_type
|
|
282
|
+
? normalizeFormattedType(colRow.formatted_type, colRow.data_type, colRow.udt_name)
|
|
283
|
+
: null;
|
|
284
|
+
if (formattedType) {
|
|
285
|
+
nativeType = formattedType;
|
|
286
|
+
} else if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
|
|
89
287
|
if (colRow.character_maximum_length) {
|
|
90
288
|
nativeType = `${colRow.data_type}(${colRow.character_maximum_length})`;
|
|
91
289
|
} else {
|
|
@@ -107,75 +305,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
107
305
|
name: colRow.column_name,
|
|
108
306
|
nativeType,
|
|
109
307
|
nullable: colRow.is_nullable === 'YES',
|
|
308
|
+
...ifDefined('default', colRow.column_default ?? undefined),
|
|
110
309
|
};
|
|
111
310
|
}
|
|
112
311
|
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
column_name: string;
|
|
117
|
-
ordinal_position: number;
|
|
118
|
-
}>(
|
|
119
|
-
`SELECT
|
|
120
|
-
tc.constraint_name,
|
|
121
|
-
kcu.column_name,
|
|
122
|
-
kcu.ordinal_position
|
|
123
|
-
FROM information_schema.table_constraints tc
|
|
124
|
-
JOIN information_schema.key_column_usage kcu
|
|
125
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
126
|
-
AND tc.table_schema = kcu.table_schema
|
|
127
|
-
AND tc.table_name = kcu.table_name
|
|
128
|
-
WHERE tc.table_schema = $1
|
|
129
|
-
AND tc.table_name = $2
|
|
130
|
-
AND tc.constraint_type = 'PRIMARY KEY'
|
|
131
|
-
ORDER BY kcu.ordinal_position`,
|
|
132
|
-
[schema, tableName],
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const primaryKeyColumns = pkResult.rows
|
|
312
|
+
// Process primary key
|
|
313
|
+
const pkRows = [...(pksByTable.get(tableName) ?? [])];
|
|
314
|
+
const primaryKeyColumns = pkRows
|
|
136
315
|
.sort((a, b) => a.ordinal_position - b.ordinal_position)
|
|
137
316
|
.map((row) => row.column_name);
|
|
138
317
|
const primaryKey: PrimaryKey | undefined =
|
|
139
318
|
primaryKeyColumns.length > 0
|
|
140
319
|
? {
|
|
141
320
|
columns: primaryKeyColumns,
|
|
142
|
-
...(
|
|
143
|
-
? { name: pkResult.rows[0].constraint_name }
|
|
144
|
-
: {}),
|
|
321
|
+
...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
|
|
145
322
|
}
|
|
146
323
|
: undefined;
|
|
147
324
|
|
|
148
|
-
//
|
|
149
|
-
const fkResult = await driver.query<{
|
|
150
|
-
constraint_name: string;
|
|
151
|
-
column_name: string;
|
|
152
|
-
ordinal_position: number;
|
|
153
|
-
referenced_table_schema: string;
|
|
154
|
-
referenced_table_name: string;
|
|
155
|
-
referenced_column_name: string;
|
|
156
|
-
}>(
|
|
157
|
-
`SELECT
|
|
158
|
-
tc.constraint_name,
|
|
159
|
-
kcu.column_name,
|
|
160
|
-
kcu.ordinal_position,
|
|
161
|
-
ccu.table_schema AS referenced_table_schema,
|
|
162
|
-
ccu.table_name AS referenced_table_name,
|
|
163
|
-
ccu.column_name AS referenced_column_name
|
|
164
|
-
FROM information_schema.table_constraints tc
|
|
165
|
-
JOIN information_schema.key_column_usage kcu
|
|
166
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
167
|
-
AND tc.table_schema = kcu.table_schema
|
|
168
|
-
AND tc.table_name = kcu.table_name
|
|
169
|
-
JOIN information_schema.constraint_column_usage ccu
|
|
170
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
171
|
-
AND ccu.table_schema = tc.table_schema
|
|
172
|
-
WHERE tc.table_schema = $1
|
|
173
|
-
AND tc.table_name = $2
|
|
174
|
-
AND tc.constraint_type = 'FOREIGN KEY'
|
|
175
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
176
|
-
[schema, tableName],
|
|
177
|
-
);
|
|
178
|
-
|
|
325
|
+
// Process foreign keys
|
|
179
326
|
const foreignKeysMap = new Map<
|
|
180
327
|
string,
|
|
181
328
|
{
|
|
@@ -183,12 +330,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
183
330
|
referencedTable: string;
|
|
184
331
|
referencedColumns: string[];
|
|
185
332
|
name: string;
|
|
333
|
+
deleteRule: string;
|
|
334
|
+
updateRule: string;
|
|
186
335
|
}
|
|
187
336
|
>();
|
|
188
|
-
for (const fkRow of
|
|
337
|
+
for (const fkRow of fksByTable.get(tableName) ?? []) {
|
|
189
338
|
const existing = foreignKeysMap.get(fkRow.constraint_name);
|
|
190
339
|
if (existing) {
|
|
191
|
-
// Multi-column FK - add column
|
|
192
340
|
existing.columns.push(fkRow.column_name);
|
|
193
341
|
existing.referencedColumns.push(fkRow.referenced_column_name);
|
|
194
342
|
} else {
|
|
@@ -197,6 +345,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
197
345
|
referencedTable: fkRow.referenced_table_name,
|
|
198
346
|
referencedColumns: [fkRow.referenced_column_name],
|
|
199
347
|
name: fkRow.constraint_name,
|
|
348
|
+
deleteRule: fkRow.delete_rule,
|
|
349
|
+
updateRule: fkRow.update_rule,
|
|
200
350
|
});
|
|
201
351
|
}
|
|
202
352
|
}
|
|
@@ -206,46 +356,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
206
356
|
referencedTable: fk.referencedTable,
|
|
207
357
|
referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
|
|
208
358
|
name: fk.name,
|
|
359
|
+
...ifDefined('onDelete', mapReferentialAction(fk.deleteRule)),
|
|
360
|
+
...ifDefined('onUpdate', mapReferentialAction(fk.updateRule)),
|
|
209
361
|
}),
|
|
210
362
|
);
|
|
211
363
|
|
|
212
|
-
//
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
tc.constraint_name,
|
|
220
|
-
kcu.column_name,
|
|
221
|
-
kcu.ordinal_position
|
|
222
|
-
FROM information_schema.table_constraints tc
|
|
223
|
-
JOIN information_schema.key_column_usage kcu
|
|
224
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
225
|
-
AND tc.table_schema = kcu.table_schema
|
|
226
|
-
AND tc.table_name = kcu.table_name
|
|
227
|
-
WHERE tc.table_schema = $1
|
|
228
|
-
AND tc.table_name = $2
|
|
229
|
-
AND tc.constraint_type = 'UNIQUE'
|
|
230
|
-
AND tc.constraint_name NOT IN (
|
|
231
|
-
SELECT constraint_name
|
|
232
|
-
FROM information_schema.table_constraints
|
|
233
|
-
WHERE table_schema = $1
|
|
234
|
-
AND table_name = $2
|
|
235
|
-
AND constraint_type = 'PRIMARY KEY'
|
|
236
|
-
)
|
|
237
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
238
|
-
[schema, tableName],
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const uniquesMap = new Map<
|
|
242
|
-
string,
|
|
243
|
-
{
|
|
244
|
-
columns: string[];
|
|
245
|
-
name: string;
|
|
364
|
+
// Process unique constraints (excluding those that are also PKs)
|
|
365
|
+
const pkConstraints = pkConstraintsByTable.get(tableName) ?? new Set();
|
|
366
|
+
const uniquesMap = new Map<string, { columns: string[]; name: string }>();
|
|
367
|
+
for (const uniqueRow of uniquesByTable.get(tableName) ?? []) {
|
|
368
|
+
// Skip if this constraint is also a primary key
|
|
369
|
+
if (pkConstraints.has(uniqueRow.constraint_name)) {
|
|
370
|
+
continue;
|
|
246
371
|
}
|
|
247
|
-
>();
|
|
248
|
-
for (const uniqueRow of uniqueResult.rows) {
|
|
249
372
|
const existing = uniquesMap.get(uniqueRow.constraint_name);
|
|
250
373
|
if (existing) {
|
|
251
374
|
existing.columns.push(uniqueRow.column_name);
|
|
@@ -261,48 +384,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
261
384
|
name: uq.name,
|
|
262
385
|
}));
|
|
263
386
|
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
indisunique: boolean;
|
|
268
|
-
attname: string;
|
|
269
|
-
attnum: number;
|
|
270
|
-
}>(
|
|
271
|
-
`SELECT
|
|
272
|
-
i.indexname,
|
|
273
|
-
ix.indisunique,
|
|
274
|
-
a.attname,
|
|
275
|
-
a.attnum
|
|
276
|
-
FROM pg_indexes i
|
|
277
|
-
JOIN pg_class ic ON ic.relname = i.indexname
|
|
278
|
-
JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
|
|
279
|
-
JOIN pg_index ix ON ix.indexrelid = ic.oid
|
|
280
|
-
JOIN pg_class t ON t.oid = ix.indrelid
|
|
281
|
-
JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
|
|
282
|
-
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
|
|
283
|
-
WHERE i.schemaname = $1
|
|
284
|
-
AND i.tablename = $2
|
|
285
|
-
AND NOT EXISTS (
|
|
286
|
-
SELECT 1
|
|
287
|
-
FROM information_schema.table_constraints tc
|
|
288
|
-
WHERE tc.table_schema = $1
|
|
289
|
-
AND tc.table_name = $2
|
|
290
|
-
AND tc.constraint_name = i.indexname
|
|
291
|
-
)
|
|
292
|
-
ORDER BY i.indexname, a.attnum`,
|
|
293
|
-
[schema, tableName],
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
const indexesMap = new Map<
|
|
297
|
-
string,
|
|
298
|
-
{
|
|
299
|
-
columns: string[];
|
|
300
|
-
name: string;
|
|
301
|
-
unique: boolean;
|
|
302
|
-
}
|
|
303
|
-
>();
|
|
304
|
-
for (const idxRow of indexResult.rows) {
|
|
305
|
-
// Skip rows where attname is null (system columns or invalid attnum)
|
|
387
|
+
// Process indexes
|
|
388
|
+
const indexesMap = new Map<string, { columns: string[]; name: string; unique: boolean }>();
|
|
389
|
+
for (const idxRow of indexesByTable.get(tableName) ?? []) {
|
|
306
390
|
if (!idxRow.attname) {
|
|
307
391
|
continue;
|
|
308
392
|
}
|
|
@@ -326,30 +410,26 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
326
410
|
tables[tableName] = {
|
|
327
411
|
name: tableName,
|
|
328
412
|
columns,
|
|
329
|
-
...(primaryKey
|
|
413
|
+
...ifDefined('primaryKey', primaryKey),
|
|
330
414
|
foreignKeys,
|
|
331
415
|
uniques,
|
|
332
416
|
indexes,
|
|
333
417
|
};
|
|
334
418
|
}
|
|
335
419
|
|
|
336
|
-
// Query extensions
|
|
337
|
-
const extensionsResult = await driver.query<{
|
|
338
|
-
extname: string;
|
|
339
|
-
}>(
|
|
340
|
-
`SELECT extname
|
|
341
|
-
FROM pg_extension
|
|
342
|
-
ORDER BY extname`,
|
|
343
|
-
[],
|
|
344
|
-
);
|
|
345
|
-
|
|
346
420
|
const extensions = extensionsResult.rows.map((row) => row.extname);
|
|
347
421
|
|
|
348
|
-
|
|
422
|
+
const storageTypes =
|
|
423
|
+
(await pgEnumControlHooks.introspectTypes?.({ driver, schemaName: schema })) ?? {};
|
|
424
|
+
|
|
349
425
|
const annotations = {
|
|
350
426
|
pg: {
|
|
351
427
|
schema,
|
|
352
428
|
version: await this.getPostgresVersion(driver),
|
|
429
|
+
...ifDefined(
|
|
430
|
+
'storageTypes',
|
|
431
|
+
Object.keys(storageTypes).length > 0 ? storageTypes : undefined,
|
|
432
|
+
),
|
|
353
433
|
},
|
|
354
434
|
};
|
|
355
435
|
|
|
@@ -373,3 +453,145 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
373
453
|
return match?.[1] ?? 'unknown';
|
|
374
454
|
}
|
|
375
455
|
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Pre-computed lookup map for simple prefix-based type normalization.
|
|
459
|
+
* Maps short Postgres type names to their canonical SQL names.
|
|
460
|
+
* Using a Map for O(1) lookup instead of multiple startsWith checks.
|
|
461
|
+
*/
|
|
462
|
+
const TYPE_PREFIX_MAP: ReadonlyMap<string, string> = new Map([
|
|
463
|
+
['varchar', 'character varying'],
|
|
464
|
+
['bpchar', 'character'],
|
|
465
|
+
['varbit', 'bit varying'],
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Normalizes a Postgres schema native type to its canonical form for comparison.
|
|
470
|
+
*
|
|
471
|
+
* Uses a pre-computed lookup map for simple prefix replacements (O(1))
|
|
472
|
+
* and handles complex temporal type normalization separately.
|
|
473
|
+
*/
|
|
474
|
+
export function normalizeSchemaNativeType(nativeType: string): string {
|
|
475
|
+
const trimmed = nativeType.trim();
|
|
476
|
+
|
|
477
|
+
// Fast path: check simple prefix replacements using the lookup map
|
|
478
|
+
for (const [prefix, replacement] of TYPE_PREFIX_MAP) {
|
|
479
|
+
if (trimmed.startsWith(prefix)) {
|
|
480
|
+
return replacement + trimmed.slice(prefix.length);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Temporal types with time zone handling
|
|
485
|
+
// Check for 'with time zone' suffix first (more specific)
|
|
486
|
+
if (trimmed.includes(' with time zone')) {
|
|
487
|
+
if (trimmed.startsWith('timestamp')) {
|
|
488
|
+
return `timestamptz${trimmed.slice(9).replace(' with time zone', '')}`;
|
|
489
|
+
}
|
|
490
|
+
if (trimmed.startsWith('time')) {
|
|
491
|
+
return `timetz${trimmed.slice(4).replace(' with time zone', '')}`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Handle 'without time zone' suffix - just strip it
|
|
496
|
+
if (trimmed.includes(' without time zone')) {
|
|
497
|
+
return trimmed.replace(' without time zone', '');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return trimmed;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
504
|
+
if (formattedType === 'integer') {
|
|
505
|
+
return 'int4';
|
|
506
|
+
}
|
|
507
|
+
if (formattedType === 'smallint') {
|
|
508
|
+
return 'int2';
|
|
509
|
+
}
|
|
510
|
+
if (formattedType === 'bigint') {
|
|
511
|
+
return 'int8';
|
|
512
|
+
}
|
|
513
|
+
if (formattedType === 'real') {
|
|
514
|
+
return 'float4';
|
|
515
|
+
}
|
|
516
|
+
if (formattedType === 'double precision') {
|
|
517
|
+
return 'float8';
|
|
518
|
+
}
|
|
519
|
+
if (formattedType === 'boolean') {
|
|
520
|
+
return 'bool';
|
|
521
|
+
}
|
|
522
|
+
if (formattedType.startsWith('varchar')) {
|
|
523
|
+
return formattedType.replace('varchar', 'character varying');
|
|
524
|
+
}
|
|
525
|
+
if (formattedType.startsWith('bpchar')) {
|
|
526
|
+
return formattedType.replace('bpchar', 'character');
|
|
527
|
+
}
|
|
528
|
+
if (formattedType.startsWith('varbit')) {
|
|
529
|
+
return formattedType.replace('varbit', 'bit varying');
|
|
530
|
+
}
|
|
531
|
+
if (dataType === 'timestamp with time zone' || udtName === 'timestamptz') {
|
|
532
|
+
return formattedType.replace('timestamp', 'timestamptz').replace(' with time zone', '').trim();
|
|
533
|
+
}
|
|
534
|
+
if (dataType === 'timestamp without time zone' || udtName === 'timestamp') {
|
|
535
|
+
return formattedType.replace(' without time zone', '').trim();
|
|
536
|
+
}
|
|
537
|
+
if (dataType === 'time with time zone' || udtName === 'timetz') {
|
|
538
|
+
return formattedType.replace('time', 'timetz').replace(' with time zone', '').trim();
|
|
539
|
+
}
|
|
540
|
+
if (dataType === 'time without time zone' || udtName === 'time') {
|
|
541
|
+
return formattedType.replace(' without time zone', '').trim();
|
|
542
|
+
}
|
|
543
|
+
// Only dataType === 'USER-DEFINED' should ever be quoted, but this should be safe without
|
|
544
|
+
// checking that explicitly either way
|
|
545
|
+
if (formattedType.startsWith('"') && formattedType.endsWith('"')) {
|
|
546
|
+
return formattedType.slice(1, -1);
|
|
547
|
+
}
|
|
548
|
+
return formattedType;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The five standard PostgreSQL referential action rules as returned by
|
|
553
|
+
* `information_schema.referential_constraints.delete_rule` / `update_rule`.
|
|
554
|
+
*/
|
|
555
|
+
type PgReferentialActionRule = 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT';
|
|
556
|
+
|
|
557
|
+
const PG_REFERENTIAL_ACTION_MAP: Record<PgReferentialActionRule, SqlReferentialAction> = {
|
|
558
|
+
'NO ACTION': 'noAction',
|
|
559
|
+
RESTRICT: 'restrict',
|
|
560
|
+
CASCADE: 'cascade',
|
|
561
|
+
'SET NULL': 'setNull',
|
|
562
|
+
'SET DEFAULT': 'setDefault',
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Maps a Postgres referential action rule to the canonical SqlReferentialAction.
|
|
567
|
+
* Returns undefined for 'NO ACTION' (the database default) to keep the IR sparse.
|
|
568
|
+
* Throws for unrecognized rules to prevent silent data loss.
|
|
569
|
+
*/
|
|
570
|
+
function mapReferentialAction(rule: string): SqlReferentialAction | undefined {
|
|
571
|
+
const mapped = PG_REFERENTIAL_ACTION_MAP[rule as PgReferentialActionRule];
|
|
572
|
+
if (mapped === undefined) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Unknown PostgreSQL referential action rule: "${rule}". Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (mapped === 'noAction') return undefined;
|
|
578
|
+
return mapped;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Groups an array of objects by a specified key.
|
|
583
|
+
* Returns a Map for O(1) lookup by group key.
|
|
584
|
+
*/
|
|
585
|
+
function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> {
|
|
586
|
+
const map = new Map<T[K], T[]>();
|
|
587
|
+
for (const item of items) {
|
|
588
|
+
const groupKey = item[key];
|
|
589
|
+
let group = map.get(groupKey);
|
|
590
|
+
if (!group) {
|
|
591
|
+
group = [];
|
|
592
|
+
map.set(groupKey, group);
|
|
593
|
+
}
|
|
594
|
+
group.push(item);
|
|
595
|
+
}
|
|
596
|
+
return map;
|
|
597
|
+
}
|