@prisma-next/adapter-postgres 0.3.0-dev.33 → 0.3.0-dev.36
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/README.md +64 -2
- package/dist/adapter-DB1CK2jM.mjs +265 -0
- package/dist/adapter-DB1CK2jM.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-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 +3 -0
- package/dist/codecs-DcC1nPzh.mjs +206 -0
- package/dist/codecs-DcC1nPzh.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 +405 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-D7pxo-wo.mjs +996 -0
- package/dist/descriptor-meta-D7pxo-wo.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/types-BY395pUv.d.mts +19 -0
- package/dist/types-BY395pUv.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +32 -41
- package/src/core/adapter.ts +90 -17
- package/src/core/codec-ids.ts +28 -0
- package/src/core/codecs.ts +316 -19
- package/src/core/control-adapter.ts +341 -180
- package/src/core/default-normalizer.ts +77 -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-Y6L4BBLR.js +0 -309
- package/dist/chunk-Y6L4BBLR.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
|
@@ -9,6 +9,9 @@ import type {
|
|
|
9
9
|
SqlTableIR,
|
|
10
10
|
SqlUniqueIR,
|
|
11
11
|
} from '@prisma-next/sql-schema-ir/types';
|
|
12
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
13
|
+
import { parsePostgresDefault } from './default-normalizer';
|
|
14
|
+
import { pgEnumControlHooks } from './enum-control-hooks';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Postgres control plane adapter for control-plane operations like introspection.
|
|
@@ -22,6 +25,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
22
25
|
*/
|
|
23
26
|
readonly target = 'postgres' as const;
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Target-specific normalizer for raw Postgres default expressions.
|
|
30
|
+
* Used by schema verification to normalize raw defaults before comparison.
|
|
31
|
+
*/
|
|
32
|
+
readonly normalizeDefault = parsePostgresDefault;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Target-specific normalizer for Postgres schema native type names.
|
|
36
|
+
* Used by schema verification to normalize introspected type names
|
|
37
|
+
* before comparison with contract native types.
|
|
38
|
+
*/
|
|
39
|
+
readonly normalizeNativeType = normalizeSchemaNativeType;
|
|
40
|
+
|
|
25
41
|
/**
|
|
26
42
|
* Introspects a Postgres database schema and returns a raw SqlSchemaIR.
|
|
27
43
|
*
|
|
@@ -29,6 +45,8 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
29
45
|
* and returns the schema structure without type mapping or contract enrichment.
|
|
30
46
|
* Type mapping and enrichment are handled separately by enrichment helpers.
|
|
31
47
|
*
|
|
48
|
+
* Uses batched queries to minimize database round trips (7 queries instead of 5T+3).
|
|
49
|
+
*
|
|
32
50
|
* @param driver - ControlDriverInstance<'sql', 'postgres'> instance for executing queries
|
|
33
51
|
* @param contractIR - Optional contract IR for contract-guided introspection (filtering, optimization)
|
|
34
52
|
* @param schema - Schema name to introspect (defaults to 'public')
|
|
@@ -39,25 +57,28 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
39
57
|
_contractIR?: unknown,
|
|
40
58
|
schema = 'public',
|
|
41
59
|
): Promise<SqlSchemaIR> {
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// Execute all queries in parallel for efficiency (7 queries instead of 5T+3)
|
|
61
|
+
const [
|
|
62
|
+
tablesResult,
|
|
63
|
+
columnsResult,
|
|
64
|
+
pkResult,
|
|
65
|
+
fkResult,
|
|
66
|
+
uniqueResult,
|
|
67
|
+
indexResult,
|
|
68
|
+
extensionsResult,
|
|
69
|
+
] = await Promise.all([
|
|
70
|
+
// Query all tables
|
|
71
|
+
driver.query<{ table_name: string }>(
|
|
72
|
+
`SELECT table_name
|
|
73
|
+
FROM information_schema.tables
|
|
74
|
+
WHERE table_schema = $1
|
|
75
|
+
AND table_type = 'BASE TABLE'
|
|
76
|
+
ORDER BY table_name`,
|
|
77
|
+
[schema],
|
|
78
|
+
),
|
|
79
|
+
// Query all columns for all tables in schema
|
|
80
|
+
driver.query<{
|
|
81
|
+
table_name: string;
|
|
61
82
|
column_name: string;
|
|
62
83
|
data_type: string;
|
|
63
84
|
udt_name: string;
|
|
@@ -65,58 +86,44 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
65
86
|
character_maximum_length: number | null;
|
|
66
87
|
numeric_precision: number | null;
|
|
67
88
|
numeric_scale: number | null;
|
|
89
|
+
column_default: string | null;
|
|
90
|
+
formatted_type: string | null;
|
|
68
91
|
}>(
|
|
69
92
|
`SELECT
|
|
93
|
+
c.table_name,
|
|
70
94
|
column_name,
|
|
71
95
|
data_type,
|
|
72
96
|
udt_name,
|
|
73
97
|
is_nullable,
|
|
74
98
|
character_maximum_length,
|
|
75
99
|
numeric_precision,
|
|
76
|
-
numeric_scale
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else if (colRow.numeric_precision) {
|
|
98
|
-
nativeType = `${colRow.data_type}(${colRow.numeric_precision})`;
|
|
99
|
-
} else {
|
|
100
|
-
nativeType = colRow.data_type;
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
nativeType = colRow.udt_name || colRow.data_type;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
columns[colRow.column_name] = {
|
|
107
|
-
name: colRow.column_name,
|
|
108
|
-
nativeType,
|
|
109
|
-
nullable: colRow.is_nullable === 'YES',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Query primary key
|
|
114
|
-
const pkResult = await driver.query<{
|
|
100
|
+
numeric_scale,
|
|
101
|
+
column_default,
|
|
102
|
+
format_type(a.atttypid, a.atttypmod) AS formatted_type
|
|
103
|
+
FROM information_schema.columns c
|
|
104
|
+
JOIN pg_catalog.pg_class cl
|
|
105
|
+
ON cl.relname = c.table_name
|
|
106
|
+
JOIN pg_catalog.pg_namespace ns
|
|
107
|
+
ON ns.nspname = c.table_schema
|
|
108
|
+
AND ns.oid = cl.relnamespace
|
|
109
|
+
JOIN pg_catalog.pg_attribute a
|
|
110
|
+
ON a.attrelid = cl.oid
|
|
111
|
+
AND a.attname = c.column_name
|
|
112
|
+
AND a.attnum > 0
|
|
113
|
+
AND NOT a.attisdropped
|
|
114
|
+
WHERE c.table_schema = $1
|
|
115
|
+
ORDER BY c.table_name, c.ordinal_position`,
|
|
116
|
+
[schema],
|
|
117
|
+
),
|
|
118
|
+
// Query all primary keys for all tables in schema
|
|
119
|
+
driver.query<{
|
|
120
|
+
table_name: string;
|
|
115
121
|
constraint_name: string;
|
|
116
122
|
column_name: string;
|
|
117
123
|
ordinal_position: number;
|
|
118
124
|
}>(
|
|
119
125
|
`SELECT
|
|
126
|
+
tc.table_name,
|
|
120
127
|
tc.constraint_name,
|
|
121
128
|
kcu.column_name,
|
|
122
129
|
kcu.ordinal_position
|
|
@@ -126,27 +133,13 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
126
133
|
AND tc.table_schema = kcu.table_schema
|
|
127
134
|
AND tc.table_name = kcu.table_name
|
|
128
135
|
WHERE tc.table_schema = $1
|
|
129
|
-
AND tc.table_name = $2
|
|
130
136
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
131
|
-
ORDER BY kcu.ordinal_position`,
|
|
132
|
-
[schema
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.map((row) => row.column_name);
|
|
138
|
-
const primaryKey: PrimaryKey | undefined =
|
|
139
|
-
primaryKeyColumns.length > 0
|
|
140
|
-
? {
|
|
141
|
-
columns: primaryKeyColumns,
|
|
142
|
-
...(pkResult.rows[0]?.constraint_name
|
|
143
|
-
? { name: pkResult.rows[0].constraint_name }
|
|
144
|
-
: {}),
|
|
145
|
-
}
|
|
146
|
-
: undefined;
|
|
147
|
-
|
|
148
|
-
// Query foreign keys
|
|
149
|
-
const fkResult = await driver.query<{
|
|
137
|
+
ORDER BY tc.table_name, kcu.ordinal_position`,
|
|
138
|
+
[schema],
|
|
139
|
+
),
|
|
140
|
+
// Query all foreign keys for all tables in schema
|
|
141
|
+
driver.query<{
|
|
142
|
+
table_name: string;
|
|
150
143
|
constraint_name: string;
|
|
151
144
|
column_name: string;
|
|
152
145
|
ordinal_position: number;
|
|
@@ -155,6 +148,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
155
148
|
referenced_column_name: string;
|
|
156
149
|
}>(
|
|
157
150
|
`SELECT
|
|
151
|
+
tc.table_name,
|
|
158
152
|
tc.constraint_name,
|
|
159
153
|
kcu.column_name,
|
|
160
154
|
kcu.ordinal_position,
|
|
@@ -170,52 +164,19 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
170
164
|
ON ccu.constraint_name = tc.constraint_name
|
|
171
165
|
AND ccu.table_schema = tc.table_schema
|
|
172
166
|
WHERE tc.table_schema = $1
|
|
173
|
-
AND tc.table_name = $2
|
|
174
167
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
175
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
176
|
-
[schema
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
string
|
|
181
|
-
{
|
|
182
|
-
columns: string[];
|
|
183
|
-
referencedTable: string;
|
|
184
|
-
referencedColumns: string[];
|
|
185
|
-
name: string;
|
|
186
|
-
}
|
|
187
|
-
>();
|
|
188
|
-
for (const fkRow of fkResult.rows) {
|
|
189
|
-
const existing = foreignKeysMap.get(fkRow.constraint_name);
|
|
190
|
-
if (existing) {
|
|
191
|
-
// Multi-column FK - add column
|
|
192
|
-
existing.columns.push(fkRow.column_name);
|
|
193
|
-
existing.referencedColumns.push(fkRow.referenced_column_name);
|
|
194
|
-
} else {
|
|
195
|
-
foreignKeysMap.set(fkRow.constraint_name, {
|
|
196
|
-
columns: [fkRow.column_name],
|
|
197
|
-
referencedTable: fkRow.referenced_table_name,
|
|
198
|
-
referencedColumns: [fkRow.referenced_column_name],
|
|
199
|
-
name: fkRow.constraint_name,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(foreignKeysMap.values()).map(
|
|
204
|
-
(fk) => ({
|
|
205
|
-
columns: Object.freeze([...fk.columns]) as readonly string[],
|
|
206
|
-
referencedTable: fk.referencedTable,
|
|
207
|
-
referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
|
|
208
|
-
name: fk.name,
|
|
209
|
-
}),
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// Query unique constraints (excluding PK)
|
|
213
|
-
const uniqueResult = await driver.query<{
|
|
168
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
169
|
+
[schema],
|
|
170
|
+
),
|
|
171
|
+
// Query all unique constraints for all tables in schema (excluding PKs)
|
|
172
|
+
driver.query<{
|
|
173
|
+
table_name: string;
|
|
214
174
|
constraint_name: string;
|
|
215
175
|
column_name: string;
|
|
216
176
|
ordinal_position: number;
|
|
217
177
|
}>(
|
|
218
178
|
`SELECT
|
|
179
|
+
tc.table_name,
|
|
219
180
|
tc.constraint_name,
|
|
220
181
|
kcu.column_name,
|
|
221
182
|
kcu.ordinal_position
|
|
@@ -225,50 +186,20 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
225
186
|
AND tc.table_schema = kcu.table_schema
|
|
226
187
|
AND tc.table_name = kcu.table_name
|
|
227
188
|
WHERE tc.table_schema = $1
|
|
228
|
-
AND tc.table_name = $2
|
|
229
189
|
AND tc.constraint_type = 'UNIQUE'
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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;
|
|
246
|
-
}
|
|
247
|
-
>();
|
|
248
|
-
for (const uniqueRow of uniqueResult.rows) {
|
|
249
|
-
const existing = uniquesMap.get(uniqueRow.constraint_name);
|
|
250
|
-
if (existing) {
|
|
251
|
-
existing.columns.push(uniqueRow.column_name);
|
|
252
|
-
} else {
|
|
253
|
-
uniquesMap.set(uniqueRow.constraint_name, {
|
|
254
|
-
columns: [uniqueRow.column_name],
|
|
255
|
-
name: uniqueRow.constraint_name,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const uniques: readonly SqlUniqueIR[] = Array.from(uniquesMap.values()).map((uq) => ({
|
|
260
|
-
columns: Object.freeze([...uq.columns]) as readonly string[],
|
|
261
|
-
name: uq.name,
|
|
262
|
-
}));
|
|
263
|
-
|
|
264
|
-
// Query indexes (excluding PK and unique constraints)
|
|
265
|
-
const indexResult = await driver.query<{
|
|
190
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`,
|
|
191
|
+
[schema],
|
|
192
|
+
),
|
|
193
|
+
// Query all indexes for all tables in schema (excluding constraints)
|
|
194
|
+
driver.query<{
|
|
195
|
+
tablename: string;
|
|
266
196
|
indexname: string;
|
|
267
197
|
indisunique: boolean;
|
|
268
198
|
attname: string;
|
|
269
199
|
attnum: number;
|
|
270
200
|
}>(
|
|
271
201
|
`SELECT
|
|
202
|
+
i.tablename,
|
|
272
203
|
i.indexname,
|
|
273
204
|
ix.indisunique,
|
|
274
205
|
a.attname,
|
|
@@ -281,28 +212,150 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
281
212
|
JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
|
|
282
213
|
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND a.attnum > 0
|
|
283
214
|
WHERE i.schemaname = $1
|
|
284
|
-
AND i.tablename = $2
|
|
285
215
|
AND NOT EXISTS (
|
|
286
216
|
SELECT 1
|
|
287
217
|
FROM information_schema.table_constraints tc
|
|
288
218
|
WHERE tc.table_schema = $1
|
|
289
|
-
AND tc.table_name =
|
|
219
|
+
AND tc.table_name = i.tablename
|
|
290
220
|
AND tc.constraint_name = i.indexname
|
|
291
221
|
)
|
|
292
|
-
ORDER BY i.indexname, a.attnum`,
|
|
293
|
-
[schema
|
|
294
|
-
)
|
|
222
|
+
ORDER BY i.tablename, i.indexname, a.attnum`,
|
|
223
|
+
[schema],
|
|
224
|
+
),
|
|
225
|
+
// Query extensions
|
|
226
|
+
driver.query<{ extname: string }>(
|
|
227
|
+
`SELECT extname
|
|
228
|
+
FROM pg_extension
|
|
229
|
+
ORDER BY extname`,
|
|
230
|
+
[],
|
|
231
|
+
),
|
|
232
|
+
]);
|
|
295
233
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
234
|
+
// Group results by table name for efficient lookup
|
|
235
|
+
const columnsByTable = groupBy(columnsResult.rows, 'table_name');
|
|
236
|
+
const pksByTable = groupBy(pkResult.rows, 'table_name');
|
|
237
|
+
const fksByTable = groupBy(fkResult.rows, 'table_name');
|
|
238
|
+
const uniquesByTable = groupBy(uniqueResult.rows, 'table_name');
|
|
239
|
+
const indexesByTable = groupBy(indexResult.rows, 'tablename');
|
|
240
|
+
|
|
241
|
+
// Get set of PK constraint names per table (to exclude from uniques)
|
|
242
|
+
const pkConstraintsByTable = new Map<string, Set<string>>();
|
|
243
|
+
for (const row of pkResult.rows) {
|
|
244
|
+
let constraints = pkConstraintsByTable.get(row.table_name);
|
|
245
|
+
if (!constraints) {
|
|
246
|
+
constraints = new Set();
|
|
247
|
+
pkConstraintsByTable.set(row.table_name, constraints);
|
|
248
|
+
}
|
|
249
|
+
constraints.add(row.constraint_name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
253
|
+
|
|
254
|
+
for (const tableRow of tablesResult.rows) {
|
|
255
|
+
const tableName = tableRow.table_name;
|
|
256
|
+
|
|
257
|
+
// Process columns for this table
|
|
258
|
+
const columns: Record<string, SqlColumnIR> = {};
|
|
259
|
+
for (const colRow of columnsByTable.get(tableName) ?? []) {
|
|
260
|
+
let nativeType = colRow.udt_name;
|
|
261
|
+
const formattedType = colRow.formatted_type
|
|
262
|
+
? normalizeFormattedType(colRow.formatted_type, colRow.data_type, colRow.udt_name)
|
|
263
|
+
: null;
|
|
264
|
+
if (formattedType) {
|
|
265
|
+
nativeType = formattedType;
|
|
266
|
+
} else if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
|
|
267
|
+
if (colRow.character_maximum_length) {
|
|
268
|
+
nativeType = `${colRow.data_type}(${colRow.character_maximum_length})`;
|
|
269
|
+
} else {
|
|
270
|
+
nativeType = colRow.data_type;
|
|
271
|
+
}
|
|
272
|
+
} else if (colRow.data_type === 'numeric' || colRow.data_type === 'decimal') {
|
|
273
|
+
if (colRow.numeric_precision && colRow.numeric_scale !== null) {
|
|
274
|
+
nativeType = `${colRow.data_type}(${colRow.numeric_precision},${colRow.numeric_scale})`;
|
|
275
|
+
} else if (colRow.numeric_precision) {
|
|
276
|
+
nativeType = `${colRow.data_type}(${colRow.numeric_precision})`;
|
|
277
|
+
} else {
|
|
278
|
+
nativeType = colRow.data_type;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
nativeType = colRow.udt_name || colRow.data_type;
|
|
302
282
|
}
|
|
283
|
+
|
|
284
|
+
columns[colRow.column_name] = {
|
|
285
|
+
name: colRow.column_name,
|
|
286
|
+
nativeType,
|
|
287
|
+
nullable: colRow.is_nullable === 'YES',
|
|
288
|
+
...ifDefined('default', colRow.column_default ?? undefined),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Process primary key
|
|
293
|
+
const pkRows = [...(pksByTable.get(tableName) ?? [])];
|
|
294
|
+
const primaryKeyColumns = pkRows
|
|
295
|
+
.sort((a, b) => a.ordinal_position - b.ordinal_position)
|
|
296
|
+
.map((row) => row.column_name);
|
|
297
|
+
const primaryKey: PrimaryKey | undefined =
|
|
298
|
+
primaryKeyColumns.length > 0
|
|
299
|
+
? {
|
|
300
|
+
columns: primaryKeyColumns,
|
|
301
|
+
...(pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}),
|
|
302
|
+
}
|
|
303
|
+
: undefined;
|
|
304
|
+
|
|
305
|
+
// Process foreign keys
|
|
306
|
+
const foreignKeysMap = new Map<
|
|
307
|
+
string,
|
|
308
|
+
{ columns: string[]; referencedTable: string; referencedColumns: string[]; name: string }
|
|
303
309
|
>();
|
|
304
|
-
for (const
|
|
305
|
-
|
|
310
|
+
for (const fkRow of fksByTable.get(tableName) ?? []) {
|
|
311
|
+
const existing = foreignKeysMap.get(fkRow.constraint_name);
|
|
312
|
+
if (existing) {
|
|
313
|
+
existing.columns.push(fkRow.column_name);
|
|
314
|
+
existing.referencedColumns.push(fkRow.referenced_column_name);
|
|
315
|
+
} else {
|
|
316
|
+
foreignKeysMap.set(fkRow.constraint_name, {
|
|
317
|
+
columns: [fkRow.column_name],
|
|
318
|
+
referencedTable: fkRow.referenced_table_name,
|
|
319
|
+
referencedColumns: [fkRow.referenced_column_name],
|
|
320
|
+
name: fkRow.constraint_name,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const foreignKeys: readonly SqlForeignKeyIR[] = Array.from(foreignKeysMap.values()).map(
|
|
325
|
+
(fk) => ({
|
|
326
|
+
columns: Object.freeze([...fk.columns]) as readonly string[],
|
|
327
|
+
referencedTable: fk.referencedTable,
|
|
328
|
+
referencedColumns: Object.freeze([...fk.referencedColumns]) as readonly string[],
|
|
329
|
+
name: fk.name,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Process unique constraints (excluding those that are also PKs)
|
|
334
|
+
const pkConstraints = pkConstraintsByTable.get(tableName) ?? new Set();
|
|
335
|
+
const uniquesMap = new Map<string, { columns: string[]; name: string }>();
|
|
336
|
+
for (const uniqueRow of uniquesByTable.get(tableName) ?? []) {
|
|
337
|
+
// Skip if this constraint is also a primary key
|
|
338
|
+
if (pkConstraints.has(uniqueRow.constraint_name)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const existing = uniquesMap.get(uniqueRow.constraint_name);
|
|
342
|
+
if (existing) {
|
|
343
|
+
existing.columns.push(uniqueRow.column_name);
|
|
344
|
+
} else {
|
|
345
|
+
uniquesMap.set(uniqueRow.constraint_name, {
|
|
346
|
+
columns: [uniqueRow.column_name],
|
|
347
|
+
name: uniqueRow.constraint_name,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const uniques: readonly SqlUniqueIR[] = Array.from(uniquesMap.values()).map((uq) => ({
|
|
352
|
+
columns: Object.freeze([...uq.columns]) as readonly string[],
|
|
353
|
+
name: uq.name,
|
|
354
|
+
}));
|
|
355
|
+
|
|
356
|
+
// Process indexes
|
|
357
|
+
const indexesMap = new Map<string, { columns: string[]; name: string; unique: boolean }>();
|
|
358
|
+
for (const idxRow of indexesByTable.get(tableName) ?? []) {
|
|
306
359
|
if (!idxRow.attname) {
|
|
307
360
|
continue;
|
|
308
361
|
}
|
|
@@ -326,30 +379,26 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
326
379
|
tables[tableName] = {
|
|
327
380
|
name: tableName,
|
|
328
381
|
columns,
|
|
329
|
-
...(primaryKey
|
|
382
|
+
...ifDefined('primaryKey', primaryKey),
|
|
330
383
|
foreignKeys,
|
|
331
384
|
uniques,
|
|
332
385
|
indexes,
|
|
333
386
|
};
|
|
334
387
|
}
|
|
335
388
|
|
|
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
389
|
const extensions = extensionsResult.rows.map((row) => row.extname);
|
|
347
390
|
|
|
348
|
-
|
|
391
|
+
const storageTypes =
|
|
392
|
+
(await pgEnumControlHooks.introspectTypes?.({ driver, schemaName: schema })) ?? {};
|
|
393
|
+
|
|
349
394
|
const annotations = {
|
|
350
395
|
pg: {
|
|
351
396
|
schema,
|
|
352
397
|
version: await this.getPostgresVersion(driver),
|
|
398
|
+
...ifDefined(
|
|
399
|
+
'storageTypes',
|
|
400
|
+
Object.keys(storageTypes).length > 0 ? storageTypes : undefined,
|
|
401
|
+
),
|
|
353
402
|
},
|
|
354
403
|
};
|
|
355
404
|
|
|
@@ -373,3 +422,115 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
373
422
|
return match?.[1] ?? 'unknown';
|
|
374
423
|
}
|
|
375
424
|
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Pre-computed lookup map for simple prefix-based type normalization.
|
|
428
|
+
* Maps short Postgres type names to their canonical SQL names.
|
|
429
|
+
* Using a Map for O(1) lookup instead of multiple startsWith checks.
|
|
430
|
+
*/
|
|
431
|
+
const TYPE_PREFIX_MAP: ReadonlyMap<string, string> = new Map([
|
|
432
|
+
['varchar', 'character varying'],
|
|
433
|
+
['bpchar', 'character'],
|
|
434
|
+
['varbit', 'bit varying'],
|
|
435
|
+
]);
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Normalizes a Postgres schema native type to its canonical form for comparison.
|
|
439
|
+
*
|
|
440
|
+
* Uses a pre-computed lookup map for simple prefix replacements (O(1))
|
|
441
|
+
* and handles complex temporal type normalization separately.
|
|
442
|
+
*/
|
|
443
|
+
export function normalizeSchemaNativeType(nativeType: string): string {
|
|
444
|
+
const trimmed = nativeType.trim();
|
|
445
|
+
|
|
446
|
+
// Fast path: check simple prefix replacements using the lookup map
|
|
447
|
+
for (const [prefix, replacement] of TYPE_PREFIX_MAP) {
|
|
448
|
+
if (trimmed.startsWith(prefix)) {
|
|
449
|
+
return replacement + trimmed.slice(prefix.length);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Temporal types with time zone handling
|
|
454
|
+
// Check for 'with time zone' suffix first (more specific)
|
|
455
|
+
if (trimmed.includes(' with time zone')) {
|
|
456
|
+
if (trimmed.startsWith('timestamp')) {
|
|
457
|
+
return `timestamptz${trimmed.slice(9).replace(' with time zone', '')}`;
|
|
458
|
+
}
|
|
459
|
+
if (trimmed.startsWith('time')) {
|
|
460
|
+
return `timetz${trimmed.slice(4).replace(' with time zone', '')}`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Handle 'without time zone' suffix - just strip it
|
|
465
|
+
if (trimmed.includes(' without time zone')) {
|
|
466
|
+
return trimmed.replace(' without time zone', '');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return trimmed;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function normalizeFormattedType(formattedType: string, dataType: string, udtName: string): string {
|
|
473
|
+
if (formattedType === 'integer') {
|
|
474
|
+
return 'int4';
|
|
475
|
+
}
|
|
476
|
+
if (formattedType === 'smallint') {
|
|
477
|
+
return 'int2';
|
|
478
|
+
}
|
|
479
|
+
if (formattedType === 'bigint') {
|
|
480
|
+
return 'int8';
|
|
481
|
+
}
|
|
482
|
+
if (formattedType === 'real') {
|
|
483
|
+
return 'float4';
|
|
484
|
+
}
|
|
485
|
+
if (formattedType === 'double precision') {
|
|
486
|
+
return 'float8';
|
|
487
|
+
}
|
|
488
|
+
if (formattedType === 'boolean') {
|
|
489
|
+
return 'bool';
|
|
490
|
+
}
|
|
491
|
+
if (formattedType.startsWith('varchar')) {
|
|
492
|
+
return formattedType.replace('varchar', 'character varying');
|
|
493
|
+
}
|
|
494
|
+
if (formattedType.startsWith('bpchar')) {
|
|
495
|
+
return formattedType.replace('bpchar', 'character');
|
|
496
|
+
}
|
|
497
|
+
if (formattedType.startsWith('varbit')) {
|
|
498
|
+
return formattedType.replace('varbit', 'bit varying');
|
|
499
|
+
}
|
|
500
|
+
if (dataType === 'timestamp with time zone' || udtName === 'timestamptz') {
|
|
501
|
+
return formattedType.replace('timestamp', 'timestamptz').replace(' with time zone', '').trim();
|
|
502
|
+
}
|
|
503
|
+
if (dataType === 'timestamp without time zone' || udtName === 'timestamp') {
|
|
504
|
+
return formattedType.replace(' without time zone', '').trim();
|
|
505
|
+
}
|
|
506
|
+
if (dataType === 'time with time zone' || udtName === 'timetz') {
|
|
507
|
+
return formattedType.replace('time', 'timetz').replace(' with time zone', '').trim();
|
|
508
|
+
}
|
|
509
|
+
if (dataType === 'time without time zone' || udtName === 'time') {
|
|
510
|
+
return formattedType.replace(' without time zone', '').trim();
|
|
511
|
+
}
|
|
512
|
+
// Only dataType === 'USER-DEFINED' should ever be quoted, but this should be safe without
|
|
513
|
+
// checking that explicitly either way
|
|
514
|
+
if (formattedType.startsWith('"') && formattedType.endsWith('"')) {
|
|
515
|
+
return formattedType.slice(1, -1);
|
|
516
|
+
}
|
|
517
|
+
return formattedType;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Groups an array of objects by a specified key.
|
|
522
|
+
* Returns a Map for O(1) lookup by group key.
|
|
523
|
+
*/
|
|
524
|
+
function groupBy<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> {
|
|
525
|
+
const map = new Map<T[K], T[]>();
|
|
526
|
+
for (const item of items) {
|
|
527
|
+
const groupKey = item[key];
|
|
528
|
+
let group = map.get(groupKey);
|
|
529
|
+
if (!group) {
|
|
530
|
+
group = [];
|
|
531
|
+
map.set(groupKey, group);
|
|
532
|
+
}
|
|
533
|
+
group.push(item);
|
|
534
|
+
}
|
|
535
|
+
return map;
|
|
536
|
+
}
|