@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.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +64 -2
  3. package/dist/adapter-7pXt8ej9.mjs +369 -0
  4. package/dist/adapter-7pXt8ej9.mjs.map +1 -0
  5. package/dist/adapter.d.mts +23 -0
  6. package/dist/adapter.d.mts.map +1 -0
  7. package/dist/adapter.mjs +3 -0
  8. package/dist/codec-ids-BwjcIf74.mjs +29 -0
  9. package/dist/codec-ids-BwjcIf74.mjs.map +1 -0
  10. package/dist/codec-types.d.mts +107 -0
  11. package/dist/codec-types.d.mts.map +1 -0
  12. package/dist/codec-types.mjs +3 -0
  13. package/dist/codecs-C3wlpdV7.mjs +385 -0
  14. package/dist/codecs-C3wlpdV7.mjs.map +1 -0
  15. package/dist/column-types.d.mts +122 -0
  16. package/dist/column-types.d.mts.map +1 -0
  17. package/dist/column-types.mjs +180 -0
  18. package/dist/column-types.mjs.map +1 -0
  19. package/dist/control.d.mts +77 -0
  20. package/dist/control.d.mts.map +1 -0
  21. package/dist/control.mjs +776 -0
  22. package/dist/control.mjs.map +1 -0
  23. package/dist/descriptor-meta-DemWrTfB.mjs +768 -0
  24. package/dist/descriptor-meta-DemWrTfB.mjs.map +1 -0
  25. package/dist/runtime.d.mts +19 -0
  26. package/dist/runtime.d.mts.map +1 -0
  27. package/dist/runtime.mjs +98 -0
  28. package/dist/runtime.mjs.map +1 -0
  29. package/dist/sql-utils-CSfAGEwF.mjs +78 -0
  30. package/dist/sql-utils-CSfAGEwF.mjs.map +1 -0
  31. package/dist/types-DxaTd7aP.d.mts +20 -0
  32. package/dist/types-DxaTd7aP.d.mts.map +1 -0
  33. package/dist/types.d.mts +2 -0
  34. package/dist/types.mjs +1 -0
  35. package/package.json +33 -42
  36. package/src/core/adapter.ts +535 -256
  37. package/src/core/codec-ids.ts +30 -0
  38. package/src/core/codecs.ts +487 -36
  39. package/src/core/control-adapter.ts +405 -184
  40. package/src/core/control-mutation-defaults.ts +335 -0
  41. package/src/core/default-normalizer.ts +145 -0
  42. package/src/core/descriptor-meta.ts +227 -9
  43. package/src/core/enum-control-hooks.ts +739 -0
  44. package/src/core/json-schema-type-expression.ts +131 -0
  45. package/src/core/json-schema-validator.ts +53 -0
  46. package/src/core/sql-utils.ts +111 -0
  47. package/src/core/standard-schema.ts +71 -0
  48. package/src/core/types.ts +8 -10
  49. package/src/exports/codec-types.ts +34 -1
  50. package/src/exports/column-types.ts +223 -27
  51. package/src/exports/control.ts +19 -9
  52. package/src/exports/runtime.ts +75 -19
  53. package/dist/chunk-HD5YISNQ.js +0 -47
  54. package/dist/chunk-HD5YISNQ.js.map +0 -1
  55. package/dist/chunk-J3XSOAM2.js +0 -162
  56. package/dist/chunk-J3XSOAM2.js.map +0 -1
  57. package/dist/chunk-T6S3A6VT.js +0 -301
  58. package/dist/chunk-T6S3A6VT.js.map +0 -1
  59. package/dist/core/adapter.d.ts +0 -19
  60. package/dist/core/adapter.d.ts.map +0 -1
  61. package/dist/core/codecs.d.ts +0 -110
  62. package/dist/core/codecs.d.ts.map +0 -1
  63. package/dist/core/control-adapter.d.ts +0 -33
  64. package/dist/core/control-adapter.d.ts.map +0 -1
  65. package/dist/core/descriptor-meta.d.ts +0 -72
  66. package/dist/core/descriptor-meta.d.ts.map +0 -1
  67. package/dist/core/types.d.ts +0 -16
  68. package/dist/core/types.d.ts.map +0 -1
  69. package/dist/exports/adapter.d.ts +0 -2
  70. package/dist/exports/adapter.d.ts.map +0 -1
  71. package/dist/exports/adapter.js +0 -8
  72. package/dist/exports/adapter.js.map +0 -1
  73. package/dist/exports/codec-types.d.ts +0 -11
  74. package/dist/exports/codec-types.d.ts.map +0 -1
  75. package/dist/exports/codec-types.js +0 -7
  76. package/dist/exports/codec-types.js.map +0 -1
  77. package/dist/exports/column-types.d.ts +0 -17
  78. package/dist/exports/column-types.d.ts.map +0 -1
  79. package/dist/exports/column-types.js +0 -49
  80. package/dist/exports/column-types.js.map +0 -1
  81. package/dist/exports/control.d.ts +0 -8
  82. package/dist/exports/control.d.ts.map +0 -1
  83. package/dist/exports/control.js +0 -279
  84. package/dist/exports/control.js.map +0 -1
  85. package/dist/exports/runtime.d.ts +0 -15
  86. package/dist/exports/runtime.d.ts.map +0 -1
  87. package/dist/exports/runtime.js +0 -20
  88. package/dist/exports/runtime.js.map +0 -1
  89. package/dist/exports/types.d.ts +0 -2
  90. package/dist/exports/types.d.ts.map +0 -1
  91. package/dist/exports/types.js +0 -1
  92. 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
- * @deprecated Use targetId instead
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 target = 'postgres' as const;
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 contractIR - Optional contract IR for contract-guided introspection (filtering, optimization)
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
- _contractIR?: unknown,
55
+ _contract?: unknown,
40
56
  schema = 'public',
41
57
  ): Promise<SqlSchemaIR> {
42
- // Query tables
43
- const tablesResult = await driver.query<{
44
- table_name: string;
45
- }>(
46
- `SELECT table_name
47
- FROM information_schema.tables
48
- WHERE table_schema = $1
49
- AND table_type = 'BASE TABLE'
50
- ORDER BY table_name`,
51
- [schema],
52
- );
53
-
54
- const tables: Record<string, SqlTableIR> = {};
55
-
56
- for (const tableRow of tablesResult.rows) {
57
- const tableName = tableRow.table_name;
58
-
59
- // Query columns for this table
60
- const columnsResult = await driver.query<{
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
- FROM information_schema.columns
78
- WHERE table_schema = $1
79
- AND table_name = $2
80
- ORDER BY ordinal_position`,
81
- [schema, tableName],
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 columnsResult.rows) {
86
- // Build native type string from catalog data
276
+ for (const colRow of columnsByTable.get(tableName) ?? []) {
87
277
  let nativeType = colRow.udt_name;
88
- if (colRow.data_type === 'character varying' || colRow.data_type === 'character') {
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
- // Query primary key
114
- const pkResult = await driver.query<{
115
- constraint_name: string;
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
- ...(pkResult.rows[0]?.constraint_name
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
- // Query foreign keys
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 fkResult.rows) {
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
- // Query unique constraints (excluding PK)
213
- const uniqueResult = await driver.query<{
214
- constraint_name: string;
215
- column_name: string;
216
- ordinal_position: number;
217
- }>(
218
- `SELECT
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
- // Query indexes (excluding PK and unique constraints)
265
- const indexResult = await driver.query<{
266
- indexname: string;
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 ? { primaryKey } : {}),
410
+ ...ifDefined('primaryKey', primaryKey),
330
411
  foreignKeys,
331
412
  uniques,
332
413
  indexes,
333
414
  };
334
415
  }
335
416
 
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
- );
417
+ const dependencies: readonly DependencyIR[] = extensionsResult.rows.map((row) => ({
418
+ id: `postgres.extension.${row.extname}`,
419
+ }));
345
420
 
346
- const extensions = extensionsResult.rows.map((row) => row.extname);
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
- extensions,
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
+ }