@dbsp/adapter-pgsql 1.11.1 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -94,6 +94,332 @@ var init_naming_plugin = __esm({
94
94
  }
95
95
  });
96
96
 
97
+ // src/db-type.ts
98
+ function splitDbType(input) {
99
+ const trimmed = input.trim();
100
+ let rest = trimmed;
101
+ let isArray = false;
102
+ if (rest.endsWith("[]")) {
103
+ isArray = true;
104
+ rest = rest.slice(0, -2).trimEnd();
105
+ }
106
+ const lowerRest = rest.toLowerCase();
107
+ for (const suffix of TEMPORAL_TZ_SUFFIXES) {
108
+ if (!lowerRest.endsWith(suffix)) continue;
109
+ const beforeSuffix = rest.slice(0, -suffix.length).trimEnd();
110
+ const temporal = splitTrailingModifier(beforeSuffix);
111
+ const temporalBase = normalizeBase(temporal.base).toLowerCase();
112
+ if (temporalBase === "timestamp" || temporalBase === "time") {
113
+ const base2 = normalizeBase(`${temporal.base}${suffix}`);
114
+ return {
115
+ base: base2,
116
+ modifier: temporal.modifier,
117
+ isArray,
118
+ schemaQualified: isSchemaQualified(base2)
119
+ };
120
+ }
121
+ }
122
+ const split = splitTrailingModifier(rest);
123
+ const base = normalizeBase(split.base);
124
+ return {
125
+ base,
126
+ modifier: split.modifier,
127
+ isArray,
128
+ schemaQualified: isSchemaQualified(base)
129
+ };
130
+ }
131
+ function splitTrailingModifier(input) {
132
+ const match = input.match(/^(.*)\(([^()]*)\)$/);
133
+ if (!match) return { base: input.trim(), modifier: void 0 };
134
+ return {
135
+ base: (match[1] ?? "").trimEnd(),
136
+ modifier: (match[2] ?? "").trim()
137
+ };
138
+ }
139
+ function normalizeBase(base) {
140
+ const trimmed = base.trim();
141
+ return trimmed.includes('"') ? trimmed : trimmed.replace(/\s+/g, " ");
142
+ }
143
+ function isSchemaQualified(base) {
144
+ return QUALIFIED_IDENTIFIER_RE.test(base);
145
+ }
146
+ function canonicalizeDbType(type) {
147
+ const split = splitDbType(type);
148
+ const base = normalizeBase(split.base);
149
+ const canonicalBase = base.includes('"') ? base : TYPE_ALIASES.get(base.toLowerCase()) ?? base.toLowerCase();
150
+ const modifier = split.modifier === void 0 ? "" : `(${split.modifier.replace(/\s*,\s*/g, ",").trim()})`;
151
+ const arraySuffix = split.isArray ? "[]" : "";
152
+ return `${canonicalBase}${modifier}${arraySuffix}`;
153
+ }
154
+ function dbTypeCastTarget(originalDbType) {
155
+ const split = splitDbType(originalDbType);
156
+ const base = normalizeBase(split.base).toLowerCase();
157
+ const hasModifier = split.modifier !== void 0;
158
+ const arraySuffix = split.isArray ? "[]" : "";
159
+ if (base === "varchar" || base === "character varying") {
160
+ return hasModifier ? `varchar${arraySuffix}` : originalDbType;
161
+ }
162
+ if (base === "char" || base === "character" || base === "bpchar") {
163
+ return `text${arraySuffix}`;
164
+ }
165
+ if (base === "bit" || base === "bit varying" || base === "varbit") {
166
+ return hasModifier ? `bit varying${arraySuffix}` : originalDbType;
167
+ }
168
+ if (base === "numeric" || base === "decimal") {
169
+ return hasModifier ? `numeric${arraySuffix}` : originalDbType;
170
+ }
171
+ if (base === "timestamp with time zone" || base === "timestamptz") {
172
+ return hasModifier ? `timestamp with time zone${arraySuffix}` : originalDbType;
173
+ }
174
+ if (base === "timestamp without time zone") {
175
+ return hasModifier ? `timestamp without time zone${arraySuffix}` : originalDbType;
176
+ }
177
+ if (base === "timestamp") {
178
+ return hasModifier ? `timestamp without time zone${arraySuffix}` : originalDbType;
179
+ }
180
+ if (base === "time with time zone" || base === "timetz") {
181
+ return hasModifier ? `time with time zone${arraySuffix}` : originalDbType;
182
+ }
183
+ if (base === "time without time zone") {
184
+ return hasModifier ? `time without time zone${arraySuffix}` : originalDbType;
185
+ }
186
+ if (base === "time") {
187
+ return hasModifier ? `time${arraySuffix}` : originalDbType;
188
+ }
189
+ if (INTERVAL_BASE_RE.test(base)) {
190
+ return hasModifier ? `${base}${arraySuffix}` : originalDbType;
191
+ }
192
+ return originalDbType;
193
+ }
194
+ function dbTypesEqual(a, b) {
195
+ return canonicalizeDbType(a) === canonicalizeDbType(b);
196
+ }
197
+ function isPgBuiltInTypeName(type) {
198
+ const split = splitDbType(type);
199
+ const base = normalizeBase(split.base);
200
+ if (base.includes('"') || split.schemaQualified) return false;
201
+ const baseLower = base.toLowerCase();
202
+ return MULTIWORD_BASE_TYPES.has(baseLower) || INTERVAL_BASE_RE.test(base) || BUILTIN_BASE_TYPES.has(baseLower);
203
+ }
204
+ function quoteTypeIdentifier(rawIdentifier) {
205
+ if (SAFE_LOWERCASE_IDENTIFIER_RE.test(rawIdentifier)) return rawIdentifier;
206
+ return `"${rawIdentifier.replace(/"/g, '""')}"`;
207
+ }
208
+ function unquoteIdentifier(id) {
209
+ const t = id.trim();
210
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
211
+ return t.slice(1, -1).replace(/""/g, '"');
212
+ }
213
+ return t;
214
+ }
215
+ function enumReferenceKind(originalDbType, typname) {
216
+ const split = splitDbType(originalDbType);
217
+ if (split.schemaQualified) return null;
218
+ if (unquoteIdentifier(normalizeBase(split.base)) !== typname) return null;
219
+ return split.isArray ? "array" : "scalar";
220
+ }
221
+ function validateDbType(type) {
222
+ if (typeof type !== "string") {
223
+ throw new Error(
224
+ `Unsafe database type name: expected a string, received ${typeof type}`
225
+ );
226
+ }
227
+ if (!isStructurallyValidDbType(type)) {
228
+ throw new Error(
229
+ `Unsafe database type name: "${type}". Must be a structurally valid PostgreSQL type name.`
230
+ );
231
+ }
232
+ return type;
233
+ }
234
+ function isStructurallyValidDbType(type) {
235
+ const trimmed = type.trim();
236
+ if (trimmed.length === 0 || trimmed !== type) return false;
237
+ if (/[;]|--|\/\*|\*\//.test(trimmed)) return false;
238
+ if (trimmed.endsWith("[][]")) return false;
239
+ if (hasForbiddenTypeKeyword(trimmed)) return false;
240
+ const split = splitDbType(trimmed);
241
+ const base = split.base;
242
+ const baseLower = base.toLowerCase();
243
+ if (split.modifier !== void 0 && !isSafeModifier(split.modifier, baseLower)) {
244
+ return false;
245
+ }
246
+ if (MULTIWORD_BASE_TYPES.has(baseLower)) {
247
+ return true;
248
+ }
249
+ if (INTERVAL_BASE_RE.test(base)) {
250
+ return true;
251
+ }
252
+ return IDENTIFIER_RE.test(base) || split.schemaQualified;
253
+ }
254
+ function isSafeModifier(modifier, baseLower) {
255
+ const structurallySafe = modifier.length > 0 && !/[();]|--|\/\*|\*\//.test(modifier) && !MODIFIER_OPERATOR_RE.test(modifier) && MODIFIER_CONTENT_RE.test(modifier) && !hasForbiddenTypeKeyword(modifier);
256
+ if (!structurallySafe) return false;
257
+ if (PRECISION_SCALE_BUILTINS.has(baseLower)) {
258
+ return /^\d+(\s*,\s*-?\d+)?$/.test(modifier);
259
+ }
260
+ if (SINGLE_INT_MODIFIER_BUILTINS.has(baseLower) || INTERVAL_BASE_RE.test(baseLower)) {
261
+ return /^\d+$/.test(modifier.trim());
262
+ }
263
+ if (BUILTIN_BASE_TYPES.has(baseLower) || MULTIWORD_BASE_TYPES.has(baseLower)) {
264
+ return false;
265
+ }
266
+ return true;
267
+ }
268
+ function hasForbiddenTypeKeyword(type) {
269
+ const withoutQuotedIdentifiers = type.replace(QUOTED_IDENTIFIER_RE, '""');
270
+ return /\bnot\s+null\b|\b(default|references|check|primary|unique|constraint|select|insert|update|delete|drop|alter|create|truncate|grant|revoke|copy)\b/i.test(
271
+ withoutQuotedIdentifiers
272
+ );
273
+ }
274
+ var TEMPORAL_TZ_SUFFIXES, SAFE_LOWERCASE_IDENTIFIER_RE, MULTIWORD_BASE_TYPES, TYPE_ALIASES, BUILTIN_BASE_TYPES, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER, IDENTIFIER_RE, QUALIFIED_IDENTIFIER_RE, QUOTED_IDENTIFIER_RE, INTERVAL_FIELD, INTERVAL_BASE_RE, MODIFIER_CONTENT_RE, MODIFIER_OPERATOR_RE, SINGLE_INT_MODIFIER_BUILTINS, PRECISION_SCALE_BUILTINS;
275
+ var init_db_type = __esm({
276
+ "src/db-type.ts"() {
277
+ "use strict";
278
+ TEMPORAL_TZ_SUFFIXES = [" without time zone", " with time zone"];
279
+ SAFE_LOWERCASE_IDENTIFIER_RE = /^[a-z_][a-z0-9_]*$/;
280
+ MULTIWORD_BASE_TYPES = /* @__PURE__ */ new Set([
281
+ "timestamp with time zone",
282
+ "timestamp without time zone",
283
+ "time with time zone",
284
+ "time without time zone",
285
+ "double precision",
286
+ "character varying",
287
+ "bit varying"
288
+ ]);
289
+ TYPE_ALIASES = /* @__PURE__ */ new Map([
290
+ ["character varying", "varchar"],
291
+ ["character", "char"],
292
+ ["bpchar", "char"],
293
+ ["bit varying", "varbit"],
294
+ ["timestamp with time zone", "timestamptz"],
295
+ ["timestamp without time zone", "timestamp"],
296
+ ["time with time zone", "timetz"],
297
+ ["time without time zone", "time"],
298
+ ["int", "integer"],
299
+ ["int4", "integer"],
300
+ ["int8", "bigint"],
301
+ ["int2", "smallint"],
302
+ ["float", "double precision"],
303
+ ["float8", "double precision"],
304
+ ["float4", "real"],
305
+ ["bool", "boolean"],
306
+ ["decimal", "numeric"],
307
+ ["serial", "integer"],
308
+ ["serial4", "integer"],
309
+ ["bigserial", "bigint"],
310
+ ["serial8", "bigint"],
311
+ ["smallserial", "smallint"],
312
+ ["serial2", "smallint"]
313
+ ]);
314
+ BUILTIN_BASE_TYPES = /* @__PURE__ */ new Set([
315
+ ...TYPE_ALIASES.keys(),
316
+ ...TYPE_ALIASES.values(),
317
+ "bigint",
318
+ "bit",
319
+ "boolean",
320
+ "box",
321
+ "bytea",
322
+ "char",
323
+ "cidr",
324
+ "circle",
325
+ "date",
326
+ "float",
327
+ "inet",
328
+ "integer",
329
+ "interval",
330
+ "json",
331
+ "jsonb",
332
+ "line",
333
+ "lseg",
334
+ "macaddr",
335
+ "macaddr8",
336
+ "money",
337
+ "numeric",
338
+ "path",
339
+ "pg_lsn",
340
+ "point",
341
+ "polygon",
342
+ "real",
343
+ "text",
344
+ "time",
345
+ "timestamp",
346
+ "timetz",
347
+ "timestamptz",
348
+ "tsquery",
349
+ "tsvector",
350
+ "txid_snapshot",
351
+ "uuid",
352
+ "varbit",
353
+ "varchar",
354
+ "xml",
355
+ "jsonpath",
356
+ "name",
357
+ "oid",
358
+ "regclass",
359
+ "regtype",
360
+ "regrole",
361
+ "regnamespace",
362
+ "regproc",
363
+ "regprocedure",
364
+ "xid",
365
+ "xid8",
366
+ "cid",
367
+ "tid",
368
+ "daterange",
369
+ "int4range",
370
+ "int8range",
371
+ "numrange",
372
+ "tsrange",
373
+ "tstzrange",
374
+ "datemultirange",
375
+ "int4multirange",
376
+ "int8multirange",
377
+ "nummultirange",
378
+ "tsmultirange",
379
+ "tstzmultirange"
380
+ ]);
381
+ UNQUOTED_IDENTIFIER = "[A-Za-z_][A-Za-z0-9_]*";
382
+ QUOTED_IDENTIFIER = '"(?:[^"]|"")+"';
383
+ IDENTIFIER_RE = new RegExp(
384
+ `^(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})$`
385
+ );
386
+ QUALIFIED_IDENTIFIER_RE = new RegExp(
387
+ `^(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})\\.(?:${UNQUOTED_IDENTIFIER}|${QUOTED_IDENTIFIER})$`
388
+ );
389
+ QUOTED_IDENTIFIER_RE = new RegExp(QUOTED_IDENTIFIER, "g");
390
+ INTERVAL_FIELD = "(?:year|month|day|hour|minute|second)";
391
+ INTERVAL_BASE_RE = new RegExp(
392
+ `^interval(?: ${INTERVAL_FIELD}(?: to ${INTERVAL_FIELD})?)?$`,
393
+ "i"
394
+ );
395
+ MODIFIER_CONTENT_RE = /^[A-Za-z0-9_,.\- ]+$/;
396
+ MODIFIER_OPERATOR_RE = /[=<>+*/%|&!@#^~]/;
397
+ SINGLE_INT_MODIFIER_BUILTINS = /* @__PURE__ */ new Set([
398
+ "varchar",
399
+ "character varying",
400
+ "char",
401
+ "character",
402
+ "bpchar",
403
+ // float(p): PostgreSQL treats float(1..24) as real, float(25..53) as double
404
+ // precision — a single-integer mantissa precision.
405
+ "float",
406
+ "bit",
407
+ "bit varying",
408
+ "varbit",
409
+ "timestamp",
410
+ "timestamptz",
411
+ "timestamp with time zone",
412
+ "timestamp without time zone",
413
+ "time",
414
+ "timetz",
415
+ "time with time zone",
416
+ "time without time zone",
417
+ "interval"
418
+ ]);
419
+ PRECISION_SCALE_BUILTINS = /* @__PURE__ */ new Set(["numeric", "decimal"]);
420
+ }
421
+ });
422
+
97
423
  // src/validate.ts
98
424
  function assertString(value, context) {
99
425
  if (typeof value !== "string") {
@@ -494,17 +820,13 @@ function isDollarQuoteTagContinuation(ch) {
494
820
  }
495
821
  function validateDbTypeName(type) {
496
822
  assertString(type, "Unsafe database type name");
497
- if (!SAFE_TYPE_PATTERN.test(type)) {
498
- throw new Error(
499
- `Unsafe database type name: "${type}". Must match PostgreSQL type name rules.`
500
- );
501
- }
502
- return type;
823
+ return validateDbType(type);
503
824
  }
504
- var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_DECIMAL_LITERAL, SAFE_STORAGE_BOOLEAN_LITERAL, SAFE_SIMPLE_IDENTIFIER_VALUE, SAFE_TYPE_PATTERN;
825
+ var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_DECIMAL_LITERAL, SAFE_STORAGE_BOOLEAN_LITERAL, SAFE_SIMPLE_IDENTIFIER_VALUE;
505
826
  var init_validate = __esm({
506
827
  "src/validate.ts"() {
507
828
  "use strict";
829
+ init_db_type();
508
830
  SQL_RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
509
831
  "all",
510
832
  "analyse",
@@ -623,7 +945,6 @@ var init_validate = __esm({
623
945
  SAFE_DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
624
946
  SAFE_STORAGE_BOOLEAN_LITERAL = /^(?:true|false|on|off)$/i;
625
947
  SAFE_SIMPLE_IDENTIFIER_VALUE = /^[A-Za-z_][A-Za-z0-9_]*$/;
626
- SAFE_TYPE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/;
627
948
  }
628
949
  });
629
950
 
@@ -2909,7 +3230,7 @@ var init_raw_expression_parser = __esm({
2909
3230
  });
2910
3231
 
2911
3232
  // src/compiler-utils.ts
2912
- import { InvalidOperationError, validateTypeName as validateTypeName2 } from "@dbsp/core";
3233
+ import { InvalidOperationError } from "@dbsp/core";
2913
3234
  function inferPgArrayType(columnName, columnTypes, sampleValue) {
2914
3235
  if (columnTypes?.[columnName]) {
2915
3236
  const pgBase = mapToPgBaseType(columnTypes[columnName]);
@@ -2927,7 +3248,16 @@ function inferPgArrayType(columnName, columnTypes, sampleValue) {
2927
3248
  return "text[]";
2928
3249
  }
2929
3250
  function mapToPgBaseType(pgType) {
2930
- const normalized = pgType.toUpperCase().replace(/\(.*\)/, "").trim();
3251
+ const trimmedPgType = pgType.trim();
3252
+ try {
3253
+ validateDbType(trimmedPgType);
3254
+ } catch {
3255
+ throw new Error(
3256
+ `batchValues: invalid type name '${pgType}'. Must be a structurally valid PostgreSQL type name.`
3257
+ );
3258
+ }
3259
+ const elementType = stripArraySuffix(trimmedPgType);
3260
+ const normalized = elementType.toUpperCase().replace(/\(.*\)/, "").trim();
2931
3261
  switch (normalized) {
2932
3262
  // ColumnType aliases (lowercase ColumnType values from ModelIR)
2933
3263
  case "STRING":
@@ -2957,9 +3287,10 @@ function mapToPgBaseType(pgType) {
2957
3287
  case "DOUBLE PRECISION":
2958
3288
  case "FLOAT8":
2959
3289
  case "FLOAT":
3290
+ return "float8";
2960
3291
  case "NUMERIC":
2961
3292
  case "DECIMAL":
2962
- return "float8";
3293
+ return "numeric";
2963
3294
  case "TEXT":
2964
3295
  case "VARCHAR":
2965
3296
  case "CHAR":
@@ -2979,11 +3310,8 @@ function mapToPgBaseType(pgType) {
2979
3310
  return "timestamptz";
2980
3311
  case "DATE":
2981
3312
  return "date";
2982
- default: {
2983
- const customType = pgType.toLowerCase();
2984
- validateTypeName2(customType);
2985
- return customType;
2986
- }
3313
+ default:
3314
+ return elementType;
2987
3315
  }
2988
3316
  }
2989
3317
  function stripArraySuffix(pgArrayType) {
@@ -3032,6 +3360,7 @@ function parseRawExpression(sqlFragment) {
3032
3360
  var init_compiler_utils = __esm({
3033
3361
  "src/compiler-utils.ts"() {
3034
3362
  "use strict";
3363
+ init_db_type();
3035
3364
  init_raw_expression_parser();
3036
3365
  }
3037
3366
  });
@@ -3048,7 +3377,6 @@ var init_param_intent = __esm({
3048
3377
  });
3049
3378
 
3050
3379
  // src/handlers/where/utils.ts
3051
- import { validateTypeName as validateTypeName3 } from "@dbsp/core";
3052
3380
  import { isFieldRef, isParamIntent as isParamIntent3 } from "@dbsp/types";
3053
3381
  function buildColumnRef(column, ctx) {
3054
3382
  if (column.includes(".")) {
@@ -3112,8 +3440,8 @@ function resolveColumnPgType(columnName, ctx) {
3112
3440
  if (!column) return void 0;
3113
3441
  if (column.originalDbType) {
3114
3442
  const typeName = column.originalDbType.trim();
3115
- validateTypeName3(typeName);
3116
- return typeName;
3443
+ validateDbType(typeName);
3444
+ return dbTypeCastTarget(typeName);
3117
3445
  }
3118
3446
  return void 0;
3119
3447
  }
@@ -3121,6 +3449,7 @@ var init_utils = __esm({
3121
3449
  "src/handlers/where/utils.ts"() {
3122
3450
  "use strict";
3123
3451
  init_ast_helpers();
3452
+ init_db_type();
3124
3453
  init_param_intent();
3125
3454
  init_param_ref();
3126
3455
  init_types();
@@ -11753,16 +12082,37 @@ function generateRlsPhase(ctx) {
11753
12082
  init_validate();
11754
12083
 
11755
12084
  // src/ddl/type-mapping.ts
12085
+ init_db_type();
11756
12086
  init_validate();
11757
12087
  function mapColumnType(col) {
11758
12088
  if (col.originalDbType) {
11759
- return validateDbTypeName(col.originalDbType.toUpperCase()).toUpperCase();
12089
+ const originalDbType = validateDbTypeName(col.originalDbType);
12090
+ return isPgBuiltInTypeName(originalDbType) ? uppercaseOutsideQuotedIdentifiers(originalDbType) : originalDbType;
11760
12091
  }
11761
12092
  if (col.autoIncrement) {
11762
12093
  return col.type === "bigint" ? "BIGSERIAL" : "SERIAL";
11763
12094
  }
11764
12095
  return mapBaseType(col.type);
11765
12096
  }
12097
+ function uppercaseOutsideQuotedIdentifiers(type) {
12098
+ let result = "";
12099
+ let inQuotedIdentifier = false;
12100
+ for (let i = 0; i < type.length; i++) {
12101
+ const char = type[i];
12102
+ if (char === '"') {
12103
+ result += char;
12104
+ if (inQuotedIdentifier && type[i + 1] === '"') {
12105
+ result += type[i + 1];
12106
+ i++;
12107
+ continue;
12108
+ }
12109
+ inQuotedIdentifier = !inQuotedIdentifier;
12110
+ continue;
12111
+ }
12112
+ result += inQuotedIdentifier ? char : char.toUpperCase();
12113
+ }
12114
+ return result;
12115
+ }
11766
12116
  function mapBaseType(type) {
11767
12117
  switch (type) {
11768
12118
  case "string":
@@ -12265,9 +12615,12 @@ function upDropEnum(change, schemaName) {
12265
12615
  if (!enumDef) return void 0;
12266
12616
  const enumName = schemaName ? `${quoteIdent2(schemaName, "alias")}.${quoteIdent2(enumDef.name, "alias")}` : quoteIdent2(enumDef.name, "alias");
12267
12617
  const refs = change.meta?.referencingColumns;
12268
- const alterStatements = refs && refs.length > 0 ? refs.map(
12269
- (ref) => `ALTER TABLE ${qualifyTable(ref.table, schemaName)} ALTER COLUMN ${quoteIdent2(ref.column, "alias")} TYPE text;`
12270
- ) : [];
12618
+ const alterStatements = refs && refs.length > 0 ? refs.map((ref) => {
12619
+ const column = quoteIdent2(ref.column, "alias");
12620
+ const targetType = ref.isArray ? "text[]" : "text";
12621
+ const using = ref.isArray ? ` USING ${column}::text[]` : "";
12622
+ return `ALTER TABLE ${qualifyTable(ref.table, schemaName)} ALTER COLUMN ${column} TYPE ${targetType}${using};`;
12623
+ }) : [];
12271
12624
  return [...alterStatements, `DROP TYPE IF EXISTS ${enumName} CASCADE;`].join(
12272
12625
  "\n"
12273
12626
  );
@@ -13351,6 +13704,7 @@ async function removeMigrationRecord(pool, name) {
13351
13704
  }
13352
13705
 
13353
13706
  // src/ddl/schema-diff.ts
13707
+ init_db_type();
13354
13708
  init_naming_plugin();
13355
13709
  function compareSchemata(schema, db, options) {
13356
13710
  const changes = [];
@@ -13538,9 +13892,7 @@ function compareColumns(schema, db, changes) {
13538
13892
  }
13539
13893
  }
13540
13894
  function compareColumnDetails(tableName, schema, db, changes) {
13541
- const schemaDbType = schema.originalDbType?.toLowerCase();
13542
- const dbDbType = db.originalDbType?.toLowerCase();
13543
- if (schemaDbType && dbDbType && schemaDbType !== dbDbType) {
13895
+ if (schema.originalDbType && db.originalDbType && !dbTypesEqual(schema.originalDbType, db.originalDbType)) {
13544
13896
  changes.push({
13545
13897
  kind: "alter_column_type",
13546
13898
  table: tableName,
@@ -14011,8 +14363,13 @@ function compareEnums(schema, db, changes) {
14011
14363
  const referencingColumns = [];
14012
14364
  for (const [tableName, tableIR] of db.tables) {
14013
14365
  for (const col of tableIR.columns) {
14014
- if (col.originalDbType === name) {
14015
- referencingColumns.push({ table: tableName, column: col.name });
14366
+ const kind = col.originalDbType !== void 0 ? enumReferenceKind(col.originalDbType, name) : null;
14367
+ if (kind !== null) {
14368
+ referencingColumns.push({
14369
+ table: tableName,
14370
+ column: col.name,
14371
+ ...kind === "array" ? { isArray: true } : {}
14372
+ });
14016
14373
  }
14017
14374
  }
14018
14375
  }
@@ -14402,6 +14759,7 @@ function vectorDims(column) {
14402
14759
 
14403
14760
  // src/introspection.ts
14404
14761
  init_assert_field();
14762
+ init_db_type();
14405
14763
  import { ModelIRImpl } from "@dbsp/core";
14406
14764
  import { buildRelationKeyFields } from "@dbsp/types";
14407
14765
  function parseExpressionsList(raw) {
@@ -14438,7 +14796,8 @@ async function queryAllCatalogs(pool, schema) {
14438
14796
  extensionsResult,
14439
14797
  sequencesResult,
14440
14798
  rlsResult,
14441
- policiesResult
14799
+ policiesResult,
14800
+ formattedColumnTypesResult
14442
14801
  ] = await Promise.all([
14443
14802
  // 1. Columns (including identity and collation)
14444
14803
  pool.query(
@@ -14672,6 +15031,29 @@ async function queryAllCatalogs(pool, schema) {
14672
15031
  JOIN pg_namespace n ON n.oid = c.relnamespace
14673
15032
  WHERE n.nspname = $1`,
14674
15033
  [schema]
15034
+ ),
15035
+ // 14. Faithful SQL-facing column types via format_type, for two cases:
15036
+ // (a) typmod-bearing columns (atttypmod <> -1) — varchar(120), numeric(10,2),
15037
+ // timestamptz(3), bit(8), vector(768) — to preserve modifier fidelity (#261);
15038
+ // (b) array columns (typcategory 'A') — so integer[] is stored as `integer[]`
15039
+ // rather than the internal `_int4` udt_name.
15040
+ // Other columns (plain base types, enums/composites/domains) fall back to
15041
+ // quoteTypeIdentifier(udt_name) in buildTableIR, which keeps a bare typname for
15042
+ // lowercase types and quotes a case-sensitive custom type (`"Money"`).
15043
+ pool.query(
15044
+ `SELECT c.relname AS table_name,
15045
+ a.attname AS column_name,
15046
+ format_type(a.atttypid, a.atttypmod) AS db_type
15047
+ FROM pg_catalog.pg_attribute a
15048
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
15049
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
15050
+ JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
15051
+ WHERE n.nspname = $1
15052
+ AND c.relkind IN ('r','p','v','m','f')
15053
+ AND a.attnum > 0
15054
+ AND NOT a.attisdropped
15055
+ AND (a.atttypmod <> -1 OR t.typcategory = 'A')`,
15056
+ [schema]
14675
15057
  )
14676
15058
  ]);
14677
15059
  return {
@@ -14687,7 +15069,8 @@ async function queryAllCatalogs(pool, schema) {
14687
15069
  extensions: extensionsResult.rows,
14688
15070
  sequences: sequencesResult.rows,
14689
15071
  rls: rlsResult.rows,
14690
- policies: policiesResult.rows
15072
+ policies: policiesResult.rows,
15073
+ formattedColumnTypes: formattedColumnTypesResult.rows
14691
15074
  };
14692
15075
  }
14693
15076
  function buildPartitionMap(rows) {
@@ -14879,6 +15262,16 @@ function buildRLSMaps(rlsRows, policiesRows) {
14879
15262
  }
14880
15263
  return { tableRlsEnabled, tablePolicies };
14881
15264
  }
15265
+ function columnTypeMapKey(tableName, columnName) {
15266
+ return `${tableName}\0${columnName}`;
15267
+ }
15268
+ function buildFormattedColumnTypeMap(rows) {
15269
+ const result = /* @__PURE__ */ new Map();
15270
+ for (const row of rows) {
15271
+ result.set(columnTypeMapKey(row.table_name, row.column_name), row.db_type);
15272
+ }
15273
+ return result;
15274
+ }
14882
15275
  function buildTableIR(tableName, ctx) {
14883
15276
  const rawCols = ctx.tableColumns.get(tableName) ?? [];
14884
15277
  const pkCols = ctx.tablePKs.get(tableName);
@@ -14896,7 +15289,13 @@ function buildTableIR(tableName, ctx) {
14896
15289
  // it verbatim (e.g. CURRENT_TIMESTAMP, nextval('seq'::regclass)) instead
14897
15290
  // of quoting it as a string literal.
14898
15291
  ...col.column_default != null ? { default: { sql: col.column_default } } : {},
14899
- originalDbType: col.udt_name,
15292
+ // format_type output (typmod-bearing + array columns) is already
15293
+ // SQL-facing; the udt_name fallback is a bare catalog typname, so
15294
+ // quote it when case-sensitive (`"Money"`) — a lowercase typname
15295
+ // stays bare, preserving the common enum/UDT identity.
15296
+ originalDbType: ctx.formattedColumnTypes.get(
15297
+ columnTypeMapKey(tableName, col.column_name)
15298
+ ) ?? quoteTypeIdentifier(col.udt_name),
14900
15299
  ...uniqueConstraintName !== void 0 ? { unique: true, uniqueConstraintName } : {},
14901
15300
  ...collation ? { collation } : {},
14902
15301
  ...identity ? { identity } : {},
@@ -14966,6 +15365,9 @@ async function introspect(pool, options) {
14966
15365
  const tableIndexes = buildIndexMap(raw.indexes);
14967
15366
  const uniqueColumns = buildUniqueColumnMap(raw.uniqueColumns);
14968
15367
  const tableColumns = buildColumnMap(raw.columns);
15368
+ const formattedColumnTypes = buildFormattedColumnTypeMap(
15369
+ raw.formattedColumnTypes
15370
+ );
14969
15371
  const tablePKs = buildPKMap(raw.pks);
14970
15372
  const fksByConstraint = buildFKMap(raw.fks);
14971
15373
  const enumMap = buildEnumMap(raw.enums);
@@ -14980,6 +15382,7 @@ async function introspect(pool, options) {
14980
15382
  for (const tableName of tableNames) {
14981
15383
  const table = buildTableIR(tableName, {
14982
15384
  tableColumns,
15385
+ formattedColumnTypes,
14983
15386
  tablePKs,
14984
15387
  fksByConstraint,
14985
15388
  tableIndexes,
@@ -16027,11 +16430,7 @@ init_naming_plugin();
16027
16430
  init_param_ref();
16028
16431
 
16029
16432
  // src/pgsql-adapter.ts
16030
- import {
16031
- POSTGRESQL_CAPABILITIES as POSTGRESQL_CAPABILITIES2,
16032
- plan as planFn2,
16033
- validateTypeName as validateTypeName4
16034
- } from "@dbsp/core";
16433
+ import { POSTGRESQL_CAPABILITIES as POSTGRESQL_CAPABILITIES2, plan as planFn2 } from "@dbsp/core";
16035
16434
  import { getNqlBindingRefName as getNqlBindingRefName2, isNqlBindingRef as isNqlBindingRef2 } from "@dbsp/types/internal";
16036
16435
 
16037
16436
  // src/adapter-compiler-includes.ts
@@ -16251,6 +16650,7 @@ init_compile_where();
16251
16650
  import { countDistinctRelationPathsByName } from "@dbsp/core";
16252
16651
  import { toColumnList as toColumnList10 } from "@dbsp/types";
16253
16652
  init_compiler_utils();
16653
+ init_db_type();
16254
16654
  init_types();
16255
16655
  init_intent_to_decisions();
16256
16656
  init_param_ref();
@@ -17153,16 +17553,6 @@ function buildIncludeTree(allDecisions) {
17153
17553
  }
17154
17554
 
17155
17555
  // src/adapter-compiler-select.ts
17156
- var ADAPTER_MULTIWORD_BASE_TYPES = [
17157
- "timestamp with time zone",
17158
- "timestamp without time zone",
17159
- "time with time zone",
17160
- "time without time zone",
17161
- "double precision",
17162
- "character varying",
17163
- "bit varying"
17164
- ];
17165
- var ADAPTER_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
17166
17556
  function assertSafeTypeName(typeName, colIndex) {
17167
17557
  const raw = typeName.trim();
17168
17558
  if (raw.length === 0) {
@@ -17170,42 +17560,14 @@ function assertSafeTypeName(typeName, colIndex) {
17170
17560
  `BatchValues compile error: type name at column index ${colIndex} must not be empty.`
17171
17561
  );
17172
17562
  }
17173
- let rest = raw;
17174
- if (rest.endsWith("[]")) {
17175
- rest = rest.slice(0, -2);
17176
- if (rest.endsWith("[]")) {
17177
- throw new Error(
17178
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. At most one array suffix "[]" is allowed as a raw type-name input.`
17179
- );
17180
- }
17181
- }
17182
- const modifierMatch = rest.match(/\(([^)]*)\)$/);
17183
- if (modifierMatch) {
17184
- const inner = modifierMatch[1] ?? "";
17185
- if (!/^\d+(?:,\d+)?$/.test(inner)) {
17186
- throw new Error(
17187
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Type modifier must be "(N)" or "(N,M)" with digits only; got "(${inner})".`
17188
- );
17189
- }
17190
- rest = rest.slice(0, rest.length - modifierMatch[0].length).trimEnd();
17191
- }
17192
- const baseLower = rest.toLowerCase();
17193
- if (ADAPTER_MULTIWORD_BASE_TYPES.includes(baseLower)) {
17194
- return;
17195
- }
17196
- const parts = rest.split(".");
17197
- if (parts.length > 2) {
17563
+ try {
17564
+ validateDbType(raw);
17565
+ } catch (error) {
17566
+ const reason = error instanceof Error ? ` ${error.message}` : "";
17198
17567
  throw new Error(
17199
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Schema-qualified types allow at most one dot (schema.type).`
17568
+ `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}.${reason}`
17200
17569
  );
17201
17570
  }
17202
- for (const part of parts) {
17203
- if (!ADAPTER_IDENT_RE.test(part)) {
17204
- throw new Error(
17205
- `BatchValues compile error: unsafe type name '${typeName}' at column index ${colIndex}. Base type "${part}" is not a valid SQL identifier and is not in the multi-word type allowlist.`
17206
- );
17207
- }
17208
- }
17209
17571
  }
17210
17572
  function buildBatchValuesRangeFn(bv, startParamIndex, aliasOverride) {
17211
17573
  const params = [];
@@ -17668,6 +18030,7 @@ function compileWithIncludes(plan, options, deps) {
17668
18030
  init_binding_registry();
17669
18031
  init_compile_where();
17670
18032
  init_compiler_utils();
18033
+ init_db_type();
17671
18034
  init_handlers();
17672
18035
  init_cte_compiler();
17673
18036
  function whereIntentAsDecision(where) {
@@ -17780,7 +18143,14 @@ function getColumnTypes(tableName, columns, deps) {
17780
18143
  const columnIR = table.columns.find((c) => c.name === col);
17781
18144
  if (columnIR) {
17782
18145
  result ??= {};
17783
- result[col] = columnIR.originalDbType ?? columnIR.type;
18146
+ const authored = columnIR.originalDbType?.trim();
18147
+ const castTarget = authored !== void 0 && authored !== "" ? dbTypeCastTarget(validateDbType(authored)) : columnIR.type;
18148
+ if (castTarget.trim().endsWith("[]")) {
18149
+ throw new Error(
18150
+ `Batch mutation of array-typed column '${col}' (${castTarget}) is not supported: unnest flattens multi-dimensional arrays. Use single-row mutations for array columns.`
18151
+ );
18152
+ }
18153
+ result[col] = castTarget;
17784
18154
  }
17785
18155
  }
17786
18156
  return result;
@@ -18567,6 +18937,7 @@ function buildRecursiveAnchorWhere(where, tableAlias, deps, state) {
18567
18937
  init_assert_field();
18568
18938
  init_ast_helpers();
18569
18939
  init_binding_registry();
18940
+ init_db_type();
18570
18941
 
18571
18942
  // src/ddl/index-operations.ts
18572
18943
  init_validate();
@@ -19020,7 +19391,8 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
19020
19391
  `NQL runtime binding '${bindingName}' cannot resolve projected column '${columnName}' on source table '${sourceTable.name}'.`
19021
19392
  );
19022
19393
  }
19023
- const dbType = column.originalDbType?.trim() || mapRuntimeBindingColumnType(column.type);
19394
+ const originalDbType = column.originalDbType?.trim();
19395
+ const dbType = originalDbType || mapRuntimeBindingColumnType(column.type);
19024
19396
  if (dbType === void 0 || dbType.trim() === "") {
19025
19397
  throw new Error(
19026
19398
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${columnName}' on source table '${sourceTable.name}'.`
@@ -19028,14 +19400,14 @@ function resolveRuntimeBindingColumnType(bindingName, sourceTable, columnName) {
19028
19400
  }
19029
19401
  const typeName = dbType.trim();
19030
19402
  try {
19031
- validateTypeName4(typeName);
19403
+ validateDbType(typeName);
19032
19404
  } catch (error) {
19033
19405
  const reason = error instanceof Error ? error.message : String(error);
19034
19406
  throw new Error(
19035
19407
  `NQL runtime binding '${bindingName}' cannot use PostgreSQL cast type for projected column '${columnName}': ${reason}`
19036
19408
  );
19037
19409
  }
19038
- return typeName;
19410
+ return originalDbType !== void 0 ? dbTypeCastTarget(typeName) : typeName;
19039
19411
  }
19040
19412
  function resolveRuntimeBindingColumnTypes(name, binding, model, sourceTableName) {
19041
19413
  if (model === void 0) {
@@ -19069,7 +19441,8 @@ function resolvePgTypeForColumnTypeInfo(bindingName, column, info) {
19069
19441
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${column}': unsupported aggregate kind '${String(info.fn)}'.`
19070
19442
  );
19071
19443
  }
19072
- const rawType = info.kind === "aggregate" ? "bigint" : info.originalDbType?.trim() || mapRuntimeBindingColumnType(info.type);
19444
+ const originalDbType = info.kind === "aggregate" ? void 0 : info.originalDbType?.trim();
19445
+ const rawType = info.kind === "aggregate" ? "bigint" : originalDbType || mapRuntimeBindingColumnType(info.type);
19073
19446
  if (rawType === void 0 || rawType.trim() === "") {
19074
19447
  throw new Error(
19075
19448
  `NQL runtime binding '${bindingName}' cannot resolve a PostgreSQL type for projected column '${column}'.`
@@ -19077,14 +19450,14 @@ function resolvePgTypeForColumnTypeInfo(bindingName, column, info) {
19077
19450
  }
19078
19451
  const typeName = rawType.trim();
19079
19452
  try {
19080
- validateTypeName4(typeName);
19453
+ validateDbType(typeName);
19081
19454
  } catch (error) {
19082
19455
  const reason = error instanceof Error ? error.message : String(error);
19083
19456
  throw new Error(
19084
19457
  `NQL runtime binding '${bindingName}' cannot use PostgreSQL cast type for projected column '${column}': ${reason}`
19085
19458
  );
19086
19459
  }
19087
- return typeName;
19460
+ return originalDbType !== void 0 ? dbTypeCastTarget(typeName) : typeName;
19088
19461
  }
19089
19462
  function resolveRuntimeBindingCteColumnTypes(name, binding) {
19090
19463
  const columnTypes = binding.columnTypes;