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