@drzl/analyzer 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +294 -34
- package/dist/index.d.cts +177 -3
- package/dist/index.d.ts +177 -3
- package/dist/index.js +292 -34
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -36,10 +36,18 @@ __export(index_exports, {
|
|
|
36
36
|
isDrizzleView: () => isDrizzleView,
|
|
37
37
|
isReadOnlyRelation: () => isReadOnlyRelation,
|
|
38
38
|
isRelationsV2: () => isRelationsV2,
|
|
39
|
+
qualifiedForeignTable: () => qualifiedForeignTable,
|
|
40
|
+
qualifiedTableName: () => qualifiedTableName,
|
|
39
41
|
readRelationsV2: () => readRelationsV2
|
|
40
42
|
});
|
|
41
43
|
module.exports = __toCommonJS(index_exports);
|
|
42
44
|
var import_meta = {};
|
|
45
|
+
function qualifiedTableName(table) {
|
|
46
|
+
return table.schema ? `${table.schema}.${table.name}` : table.name;
|
|
47
|
+
}
|
|
48
|
+
function qualifiedForeignTable(fk) {
|
|
49
|
+
return fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
|
|
50
|
+
}
|
|
43
51
|
function renderSqlLiteral(v) {
|
|
44
52
|
if (v === null || v === void 0) return "NULL";
|
|
45
53
|
if (typeof v === "number" || typeof v === "bigint") return String(v);
|
|
@@ -71,15 +79,23 @@ var GEOMETRIC_CLASS_SHAPES = {
|
|
|
71
79
|
// The object modes. `line({ mode: 'abc' })` is a `PgLineABC` and not a `PgLineObject`, and any
|
|
72
80
|
// mode but `'tuple'` builds the object class: `point({ mode: 'abc' })` is a `PgPointObject` too.
|
|
73
81
|
PgPointObject: { kind: "numberObject", fields: ["x", "y"] },
|
|
74
|
-
PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] }
|
|
82
|
+
PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] },
|
|
83
|
+
// `geometry()` and `geometry({ mode: 'xy' })` are two classes, not one class with a flag, and
|
|
84
|
+
// the fuzzer found both unnamed on this path. Their driver mappers disagree the same way the
|
|
85
|
+
// point ones do: the default hands back `[1, 2]` and the xy mode hands back `{ x: 1, y: 2 }`.
|
|
86
|
+
PgGeometry: { kind: "tuple", length: 2 },
|
|
87
|
+
PgGeometryObject: { kind: "numberObject", fields: ["x", "y"] }
|
|
75
88
|
};
|
|
76
89
|
var V1_ONLY_ENTITY_KINDS = /^(?:MsSql|Cockroach)/;
|
|
90
|
+
var NUMBER_VECTOR_CLASSES = /* @__PURE__ */ new Set(["PgVector", "PgHalfVector", "SingleStoreVector"]);
|
|
91
|
+
var BIT_STRING_CLASSES = /* @__PURE__ */ new Set(["PgBinaryVector"]);
|
|
77
92
|
var BYTE_STRING_CLASSES = /* @__PURE__ */ new Set([
|
|
78
93
|
"MySqlBinary",
|
|
79
94
|
"MySqlVarBinary",
|
|
80
95
|
"SingleStoreBinary",
|
|
81
96
|
"SingleStoreVarBinary"
|
|
82
97
|
]);
|
|
98
|
+
var BUFFER_CLASSES = /* @__PURE__ */ new Set(["SQLiteBlobBuffer"]);
|
|
83
99
|
function describeV1Column(column) {
|
|
84
100
|
const codec = column?.codec;
|
|
85
101
|
const dataType = column?.dataType;
|
|
@@ -98,14 +114,24 @@ function describeV1Column(column) {
|
|
|
98
114
|
const out = {};
|
|
99
115
|
switch (semantic) {
|
|
100
116
|
case "int8":
|
|
117
|
+
case "uint8":
|
|
101
118
|
case "int16":
|
|
102
119
|
case "int24":
|
|
103
120
|
case "int32":
|
|
104
121
|
case "int53":
|
|
105
122
|
case "uint53":
|
|
106
123
|
case "int64": {
|
|
124
|
+
if (DECIMAL_BIGINT_MODE.test(entityKind)) {
|
|
125
|
+
out.tsType = "bigint";
|
|
126
|
+
out.dbType = "NUMERIC";
|
|
127
|
+
out.integer = true;
|
|
128
|
+
const range2 = decimalModeRange(column, entityKind, "bigint");
|
|
129
|
+
if (range2) [out.min, out.max] = range2;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
107
132
|
const range = {
|
|
108
133
|
int8: ["-128", "127"],
|
|
134
|
+
uint8: ["0", "255"],
|
|
109
135
|
int16: ["-32768", "32767"],
|
|
110
136
|
int24: ["-8388608", "8388607"],
|
|
111
137
|
int32: ["-2147483648", "2147483647"],
|
|
@@ -118,7 +144,7 @@ function describeV1Column(column) {
|
|
|
118
144
|
[out.min, out.max] = range;
|
|
119
145
|
out.integer = true;
|
|
120
146
|
out.tsType = js === "bigint" ? "bigint" : "number";
|
|
121
|
-
out.dbType = semantic === "int8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
|
|
147
|
+
out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
|
|
122
148
|
break;
|
|
123
149
|
}
|
|
124
150
|
case "year":
|
|
@@ -134,6 +160,10 @@ function describeV1Column(column) {
|
|
|
134
160
|
out.integer = false;
|
|
135
161
|
out.tsType = "number";
|
|
136
162
|
out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
|
|
163
|
+
if (codec === "float4" || codec === "float8") {
|
|
164
|
+
out.allowsNaN = true;
|
|
165
|
+
out.allowsInfinity = true;
|
|
166
|
+
}
|
|
137
167
|
break;
|
|
138
168
|
}
|
|
139
169
|
case "uuid":
|
|
@@ -163,6 +193,16 @@ function describeV1Column(column) {
|
|
|
163
193
|
out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
|
|
164
194
|
break;
|
|
165
195
|
case "timestamp":
|
|
196
|
+
// `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
|
|
197
|
+
// stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
|
|
198
|
+
// are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
|
|
199
|
+
// `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
|
|
200
|
+
// `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
|
|
201
|
+
// A label only, since `dbType` is read outside this file in exactly one place,
|
|
202
|
+
// `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
|
|
203
|
+
// because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
|
|
204
|
+
// two majors disagreeing about a column is what the cross-major diff exists to catch.
|
|
205
|
+
case "datetime":
|
|
166
206
|
out.tsType = js === "string" ? "string" : "Date";
|
|
167
207
|
out.dbType = "TIMESTAMP";
|
|
168
208
|
break;
|
|
@@ -184,13 +224,25 @@ function describeV1Column(column) {
|
|
|
184
224
|
const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
|
|
185
225
|
const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
|
|
186
226
|
out.tsType = "string";
|
|
187
|
-
out.dbType =
|
|
227
|
+
out.dbType = bytes ? "BINARY" : "BIT";
|
|
188
228
|
out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
|
|
189
229
|
kind: "bitstring",
|
|
190
230
|
length: declaredLength(column),
|
|
191
231
|
// A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
|
|
192
232
|
// most that many, which is why `''` is valid there and not here.
|
|
193
|
-
|
|
233
|
+
//
|
|
234
|
+
// `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
|
|
235
|
+
// states no codec, so both of its builders came back `exact: false` and a `bit(3)`
|
|
236
|
+
// was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
|
|
237
|
+
// `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
|
|
238
|
+
// type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
|
|
239
|
+
// refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
|
|
240
|
+
// 1.0.0-rc.4 answers the same for both columns.
|
|
241
|
+
//
|
|
242
|
+
// The class rather than a prefix, because `CockroachVarbit` starts with neither
|
|
243
|
+
// `CockroachBit` nor anything else this could key on without catching the varying
|
|
244
|
+
// half too.
|
|
245
|
+
exact: codec === "bit" || entity === "CockroachBit"
|
|
194
246
|
};
|
|
195
247
|
break;
|
|
196
248
|
}
|
|
@@ -216,7 +268,16 @@ function describeV1Column(column) {
|
|
|
216
268
|
out.dbType = "LINE";
|
|
217
269
|
out.shape = js === "object" ? { kind: "numberObject", fields: ["a", "b", "c"] } : { kind: "tuple", length: 3 };
|
|
218
270
|
break;
|
|
271
|
+
// `halfvec` beside `vector`, because they differ in storage width and in nothing a validator
|
|
272
|
+
// can see: `mapFromDriverValue` on both hands back `[1, 2, 3]`. It had no arm and came back
|
|
273
|
+
// `unknown` on this path too, which the fuzzer found.
|
|
274
|
+
//
|
|
275
|
+
// `sparsevec` is deliberately not here. Its name says vector and its value is the string
|
|
276
|
+
// `{1:1.5,3:2}/3`, so typing it `number[]` for symmetry would reject every row the database
|
|
277
|
+
// returns. Its codec already answers `string` on its own, which is the same conclusion reached
|
|
278
|
+
// without this arm.
|
|
219
279
|
case "vector":
|
|
280
|
+
case "halfvec":
|
|
220
281
|
out.tsType = "number[]";
|
|
221
282
|
out.dbType = "VECTOR";
|
|
222
283
|
out.shape = { kind: "numberVector", length: declaredLength(column) };
|
|
@@ -233,7 +294,12 @@ function describeV1Column(column) {
|
|
|
233
294
|
out.tsType = "number";
|
|
234
295
|
out.dbType = "NUMERIC";
|
|
235
296
|
out.integer = false;
|
|
236
|
-
|
|
297
|
+
const range = decimalModeRange(column, entityKind, "number");
|
|
298
|
+
if (range) [out.min, out.max] = range;
|
|
299
|
+
if (codec === "numeric:number") {
|
|
300
|
+
out.allowsNaN = true;
|
|
301
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
302
|
+
}
|
|
237
303
|
} else if (js === "string") {
|
|
238
304
|
out.tsType = "string";
|
|
239
305
|
out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
|
|
@@ -261,6 +327,28 @@ function declaredLength(column) {
|
|
|
261
327
|
const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
|
|
262
328
|
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
263
329
|
}
|
|
330
|
+
function declaredDecimalRange(column) {
|
|
331
|
+
const cfg = column?.config ?? {};
|
|
332
|
+
const precision = column?.precision ?? cfg.precision;
|
|
333
|
+
const scale = column?.scale ?? cfg.scale ?? 0;
|
|
334
|
+
if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
|
|
335
|
+
return void 0;
|
|
336
|
+
if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
|
|
337
|
+
const nines = "9".repeat(precision);
|
|
338
|
+
const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
|
|
339
|
+
return [`-${max}`, max];
|
|
340
|
+
}
|
|
341
|
+
var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
|
|
342
|
+
var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
|
|
343
|
+
var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
|
|
344
|
+
function decimalModeRange(column, kind, mode) {
|
|
345
|
+
const declared = declaredDecimalRange(column);
|
|
346
|
+
if (declared) return declared;
|
|
347
|
+
if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
|
|
348
|
+
return MYSQL_IMPLICIT_DECIMAL_RANGE;
|
|
349
|
+
if (kind.startsWith("SQLite")) return void 0;
|
|
350
|
+
return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
|
|
351
|
+
}
|
|
264
352
|
var VIEW_CONFIG_FIELDS = {
|
|
265
353
|
"drizzle:Columns": "selectedFields",
|
|
266
354
|
"drizzle:Name": "name",
|
|
@@ -305,27 +393,43 @@ function isRelationsV2(val) {
|
|
|
305
393
|
)
|
|
306
394
|
);
|
|
307
395
|
}
|
|
396
|
+
function qualifiedNameOfDrizzleTable(tbl) {
|
|
397
|
+
const name = getSymbolOf(tbl, "drizzle:Name");
|
|
398
|
+
if (typeof name !== "string" || !name) return void 0;
|
|
399
|
+
const schema = getSymbolOf(tbl, "drizzle:Schema");
|
|
400
|
+
return typeof schema === "string" && schema ? `${schema}.${name}` : name;
|
|
401
|
+
}
|
|
308
402
|
function readRelationsV2(val, issues = []) {
|
|
309
403
|
const out = [];
|
|
310
404
|
for (const [tableKey, entry] of Object.entries(val)) {
|
|
311
|
-
const from =
|
|
405
|
+
const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
|
|
312
406
|
for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
|
|
313
|
-
const to = r?.targetTableName;
|
|
407
|
+
const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
|
|
314
408
|
if (typeof to !== "string" || !to) {
|
|
315
409
|
issues.push({
|
|
316
410
|
code: "DRZL_ANL_REL_V2",
|
|
317
411
|
level: "warn",
|
|
318
|
-
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped
|
|
412
|
+
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`,
|
|
413
|
+
path: from
|
|
319
414
|
});
|
|
320
415
|
continue;
|
|
321
416
|
}
|
|
322
|
-
const via =
|
|
417
|
+
const via = qualifiedNameOfDrizzleTable(r.throughTable) ?? qualifiedNameOfDrizzleTable(r.through?.sourceTable) ?? void 0;
|
|
323
418
|
if (via) out.push({ kind: "manyToMany", from, to, via });
|
|
324
419
|
else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
|
|
325
420
|
}
|
|
326
421
|
}
|
|
327
422
|
return out;
|
|
328
423
|
}
|
|
424
|
+
function unknownColumnHint(reason) {
|
|
425
|
+
if (reason === "custom") {
|
|
426
|
+
return "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type.";
|
|
427
|
+
}
|
|
428
|
+
if (reason === "gel-temporal") {
|
|
429
|
+
return "A Gel temporal column holds an instance of a class from the `gel` package, which DRZL cannot import, so it is left untyped on purpose rather than guessed at. Turn on typedColumns to recover the declared type, and validate the value yourself.";
|
|
430
|
+
}
|
|
431
|
+
return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
|
|
432
|
+
}
|
|
329
433
|
var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
330
434
|
constructor(schemaPath) {
|
|
331
435
|
this.schemaPath = schemaPath;
|
|
@@ -376,6 +480,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
376
480
|
code: "DRZL_ANL_EXTRACONFIG",
|
|
377
481
|
level: "warn",
|
|
378
482
|
message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
|
|
483
|
+
path: tableName,
|
|
379
484
|
hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
|
|
380
485
|
});
|
|
381
486
|
return [];
|
|
@@ -429,9 +534,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
429
534
|
};
|
|
430
535
|
const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
|
|
431
536
|
const toForeignTs = this.dbToTsNames(foreignColumnsObj);
|
|
537
|
+
const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
|
|
432
538
|
return {
|
|
433
539
|
columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
|
|
434
540
|
foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
|
|
541
|
+
...foreignSchema ? { foreignSchema } : {},
|
|
435
542
|
foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
|
|
436
543
|
onDelete: action(fk?.onDelete, fk?._onDelete),
|
|
437
544
|
onUpdate: action(fk?.onUpdate, fk?._onUpdate),
|
|
@@ -480,7 +587,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
480
587
|
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
481
588
|
*/
|
|
482
589
|
readRelationsObject(val, exportName, issues) {
|
|
483
|
-
const from =
|
|
590
|
+
const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
|
|
484
591
|
const make = (kind) => (table, cfg) => ({
|
|
485
592
|
kind,
|
|
486
593
|
referencedTable: table,
|
|
@@ -494,7 +601,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
494
601
|
const built = val.config({ one: make("one"), many: make("many") });
|
|
495
602
|
const out = [];
|
|
496
603
|
for (const rel of Object.values(built ?? {})) {
|
|
497
|
-
const to =
|
|
604
|
+
const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
|
|
498
605
|
if (to) out.push({ kind: rel.kind, from, to });
|
|
499
606
|
}
|
|
500
607
|
return out;
|
|
@@ -503,6 +610,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
503
610
|
code: "DRZL_ANL_RELATIONS",
|
|
504
611
|
level: "warn",
|
|
505
612
|
message: `Could not read the relations declared in "${exportName}": ${e.message}`,
|
|
613
|
+
path: from,
|
|
506
614
|
hint: "Relations for this table will be missing from the analysis."
|
|
507
615
|
});
|
|
508
616
|
return [];
|
|
@@ -523,10 +631,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
523
631
|
if (fks.length < 2) continue;
|
|
524
632
|
const fkCols = new Set(fks.flatMap((f) => f.columns));
|
|
525
633
|
if (!t.columns.every((c) => fkCols.has(c.name))) continue;
|
|
526
|
-
const targets = [...new Set(fks.map(
|
|
634
|
+
const targets = [...new Set(fks.map(qualifiedForeignTable))];
|
|
527
635
|
if (targets.length !== 2) continue;
|
|
528
|
-
|
|
529
|
-
out.push({ kind: "manyToMany", from: targets[
|
|
636
|
+
const via = qualifiedTableName(t);
|
|
637
|
+
out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via });
|
|
638
|
+
out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via });
|
|
530
639
|
}
|
|
531
640
|
return out;
|
|
532
641
|
}
|
|
@@ -553,6 +662,24 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
553
662
|
if (inexact) [out.min, out.max] = inexact;
|
|
554
663
|
out.integer = false;
|
|
555
664
|
}
|
|
665
|
+
const nonFinite = _SchemaAnalyzer.PG_NON_FINITE[ctor];
|
|
666
|
+
if (nonFinite) {
|
|
667
|
+
out.allowsNaN = nonFinite.nan;
|
|
668
|
+
out.allowsInfinity = nonFinite.infinity;
|
|
669
|
+
}
|
|
670
|
+
if (DECIMAL_NUMBER_MODE.test(ctor)) {
|
|
671
|
+
const range2 = decimalModeRange(column, ctor, "number");
|
|
672
|
+
if (range2) [out.min, out.max] = range2;
|
|
673
|
+
out.integer = false;
|
|
674
|
+
if (ctor === "PgNumericNumber") {
|
|
675
|
+
out.allowsNaN = true;
|
|
676
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
677
|
+
}
|
|
678
|
+
} else if (DECIMAL_BIGINT_MODE.test(ctor)) {
|
|
679
|
+
const range2 = decimalModeRange(column, ctor, "bigint");
|
|
680
|
+
if (range2) [out.min, out.max] = range2;
|
|
681
|
+
out.integer = true;
|
|
682
|
+
}
|
|
556
683
|
if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
|
|
557
684
|
return out;
|
|
558
685
|
}
|
|
@@ -564,12 +691,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
564
691
|
tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
|
|
565
692
|
dbType: "INTEGER"
|
|
566
693
|
};
|
|
694
|
+
// Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
|
|
695
|
+
// `timestamp_ms` differ in the scale of the number on the wire, seconds against
|
|
696
|
+
// milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
|
|
697
|
+
// either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
|
|
698
|
+
// the class covers both, where the mode check that used to answer this fell through the
|
|
699
|
+
// switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
|
|
700
|
+
// The second came back `unknown`, and every generator emitted a schema accepting anything.
|
|
701
|
+
//
|
|
702
|
+
// `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
|
|
703
|
+
// column identically. `dbType` is read in exactly one place outside this file,
|
|
704
|
+
// `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
|
|
705
|
+
// relabel reaches no output. Measured rather than argued: emitting a `Date` column under
|
|
706
|
+
// both labels, nullable and not, through all five generators gives ten byte-identical pairs.
|
|
707
|
+
case "SQLiteTimestamp":
|
|
708
|
+
return { tsType: "Date", dbType: "DATE" };
|
|
567
709
|
case "SQLiteText":
|
|
568
710
|
return { tsType: "string", dbType: "TEXT" };
|
|
569
711
|
case "SQLiteReal":
|
|
570
712
|
return { tsType: "number", dbType: "REAL" };
|
|
713
|
+
// No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
|
|
714
|
+
// `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
|
|
715
|
+
// all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
|
|
716
|
+
// produces, which is why a real `blob()` reached neither it nor anything else.
|
|
571
717
|
case "SQLiteBlob":
|
|
572
718
|
return { tsType: "Uint8Array", dbType: "BLOB" };
|
|
719
|
+
// The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
|
|
720
|
+
// for the measurement; the answers here are v1's own for the same column, so this is the
|
|
721
|
+
// two majors agreeing rather than a new opinion.
|
|
722
|
+
case "SQLiteBlobBuffer":
|
|
723
|
+
return { tsType: "Buffer", dbType: "BYTEA" };
|
|
573
724
|
// SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
|
|
574
725
|
// is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
|
|
575
726
|
// wider than the `any` a json column at least used to get.
|
|
@@ -608,6 +759,34 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
608
759
|
case "MySqlEnumColumn":
|
|
609
760
|
case "SingleStoreEnumColumn":
|
|
610
761
|
return { tsType: "string", dbType: "TEXT" };
|
|
762
|
+
// The pgvector family, found by the analyzer fuzzer: all three came back `unknown` on this
|
|
763
|
+
// path, so their validators accepted anything. The answers are drizzle's own mappers rather
|
|
764
|
+
// than the type names, and the three do not agree with each other:
|
|
765
|
+
//
|
|
766
|
+
// vector(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
767
|
+
// halfvec(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
768
|
+
// sparsevec(3) SELECT gives "{1:1.5,3:2}/3" INSERT sends "{1:1.5,3:2}/3"
|
|
769
|
+
//
|
|
770
|
+
// So the two dense ones are number arrays and the sparse one is a string. Typing `sparsevec`
|
|
771
|
+
// as a vector for symmetry would reject every row the database returns, which is the defect
|
|
772
|
+
// this family was filed under to begin with. The `shape` carries the dimension count where
|
|
773
|
+
// one is declared, as the codec path already did for `vector`.
|
|
774
|
+
case "PgVector":
|
|
775
|
+
case "PgHalfVector":
|
|
776
|
+
case "SingleStoreVector":
|
|
777
|
+
return { tsType: "number[]", dbType: "VECTOR" };
|
|
778
|
+
// `BIT` rather than `TEXT`, which a first version of this arm returned. v1's codec says `BIT`
|
|
779
|
+
// for the same column, and the cross-major diff said so: naming the class made ten of its
|
|
780
|
+
// twelve entries go stale and left `c_bit.dbType` and its nullable twin standing, which is
|
|
781
|
+
// that check distinguishing a fix from a half fix.
|
|
782
|
+
case "PgBinaryVector":
|
|
783
|
+
return { tsType: "string", dbType: "BIT" };
|
|
784
|
+
case "PgGeometry":
|
|
785
|
+
return { tsType: "[number, number]", dbType: "GEOMETRY" };
|
|
786
|
+
case "PgGeometryObject":
|
|
787
|
+
return { tsType: "{ x: number; y: number }", dbType: "GEOMETRY" };
|
|
788
|
+
case "PgSparseVector":
|
|
789
|
+
return { tsType: "string", dbType: "TEXT" };
|
|
611
790
|
case "PgInteger":
|
|
612
791
|
case "PgSmallInt":
|
|
613
792
|
return { tsType: "number", dbType: "INTEGER" };
|
|
@@ -768,7 +947,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
768
947
|
if (/^Gel/i.test(ctor)) {
|
|
769
948
|
if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
|
|
770
949
|
if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
|
|
771
|
-
if (/
|
|
950
|
+
if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
|
|
951
|
+
if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
|
|
772
952
|
if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
|
|
773
953
|
if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
|
|
774
954
|
if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
|
|
@@ -777,7 +957,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
777
957
|
if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
|
|
778
958
|
if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
|
|
779
959
|
if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
|
|
780
|
-
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
960
|
+
return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
|
|
781
961
|
}
|
|
782
962
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
783
963
|
}
|
|
@@ -793,7 +973,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
793
973
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
794
974
|
for (const [colName, outerCol] of Object.entries(columnsObj)) {
|
|
795
975
|
const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
|
|
796
|
-
|
|
976
|
+
const mapped = this.mapColumnType(col);
|
|
977
|
+
let { tsType, dbType } = mapped;
|
|
797
978
|
if (tsType === "unknown" && /At$/.test(colName)) {
|
|
798
979
|
tsType = "Date";
|
|
799
980
|
dbType = "INTEGER";
|
|
@@ -819,11 +1000,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
819
1000
|
const v1 = describeV1Column(col);
|
|
820
1001
|
const constraints = this.columnConstraints(col);
|
|
821
1002
|
if (v1?.shape) delete constraints.maxLength;
|
|
822
|
-
const sqlKind = String(
|
|
1003
|
+
const sqlKind = String(
|
|
1004
|
+
outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
|
|
1005
|
+
);
|
|
823
1006
|
const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
|
|
824
1007
|
const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
|
|
825
1008
|
const ctorName = String(col?.constructor?.name ?? "");
|
|
826
|
-
const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : GEOMETRIC_CLASS_SHAPES[ctorName];
|
|
1009
|
+
const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : BUFFER_CLASSES.has(ctorName) ? { kind: "buffer" } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
|
|
827
1010
|
if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
|
|
828
1011
|
const shape = (v1?.shape ?? fallbackShape)?.kind;
|
|
829
1012
|
const finalTs = v1?.tsType ?? tsType;
|
|
@@ -834,13 +1017,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
834
1017
|
code: "DRZL_ANL_UNKNOWN_COLUMN",
|
|
835
1018
|
level: "warn",
|
|
836
1019
|
message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
|
|
837
|
-
|
|
1020
|
+
path: `${tsName}.${colName}`,
|
|
1021
|
+
hint: unknownColumnHint(shape === "custom" ? "custom" : mapped.unnameable)
|
|
838
1022
|
});
|
|
839
1023
|
}
|
|
1024
|
+
const dims = arrayDims || v1?.arrayDimensions || 0;
|
|
1025
|
+
const declaredSqlType = (() => {
|
|
1026
|
+
let raw;
|
|
1027
|
+
try {
|
|
1028
|
+
raw = typeof outerCol?.getSQLType === "function" ? outerCol.getSQLType() : void 0;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return void 0;
|
|
1031
|
+
}
|
|
1032
|
+
if (typeof raw !== "string" || !raw) return void 0;
|
|
1033
|
+
return raw.endsWith("]") ? raw : raw + "[]".repeat(dims);
|
|
1034
|
+
})();
|
|
840
1035
|
columns.push({
|
|
841
1036
|
name: colName,
|
|
842
1037
|
tsType,
|
|
843
1038
|
dbType,
|
|
1039
|
+
...declaredSqlType ? { sqlType: declaredSqlType } : {},
|
|
844
1040
|
nullable,
|
|
845
1041
|
hasDefault,
|
|
846
1042
|
isGenerated,
|
|
@@ -917,6 +1113,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
917
1113
|
if (!col) continue;
|
|
918
1114
|
col.references = {
|
|
919
1115
|
table: fk.foreignTable,
|
|
1116
|
+
...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
|
|
920
1117
|
column: fk.foreignColumns[0],
|
|
921
1118
|
onDelete: fk.onDelete,
|
|
922
1119
|
onUpdate: fk.onUpdate
|
|
@@ -989,9 +1186,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
989
1186
|
}
|
|
990
1187
|
}
|
|
991
1188
|
if (opts.includeRelations) {
|
|
1189
|
+
const self = qualifiedTableName(table);
|
|
992
1190
|
for (const fk of table.foreignKeys ?? []) {
|
|
993
|
-
|
|
994
|
-
relations.push({ kind: "
|
|
1191
|
+
const target = qualifiedForeignTable(fk);
|
|
1192
|
+
relations.push({ kind: "one", from: self, to: target });
|
|
1193
|
+
relations.push({ kind: "many", from: target, to: self });
|
|
995
1194
|
}
|
|
996
1195
|
}
|
|
997
1196
|
} else if (this.isRelationsObject(val)) {
|
|
@@ -1019,7 +1218,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1019
1218
|
issues.push({
|
|
1020
1219
|
code: "DRZL_ANL_TABLE",
|
|
1021
1220
|
level: "warn",
|
|
1022
|
-
message: `Failed to analyze export ${name}: ${String(e)}
|
|
1221
|
+
message: `Failed to analyze export ${name}: ${String(e)}`,
|
|
1222
|
+
path: name
|
|
1023
1223
|
});
|
|
1024
1224
|
}
|
|
1025
1225
|
}
|
|
@@ -1064,11 +1264,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1064
1264
|
relations.push(...this.inferManyToMany(tables));
|
|
1065
1265
|
}
|
|
1066
1266
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1267
|
+
const byBareName = /* @__PURE__ */ new Map();
|
|
1268
|
+
for (const t of tables) {
|
|
1269
|
+
const list = byBareName.get(t.name);
|
|
1270
|
+
if (list) list.push(t);
|
|
1271
|
+
else byBareName.set(t.name, [t]);
|
|
1272
|
+
}
|
|
1273
|
+
const findTarget = (base, from) => {
|
|
1274
|
+
for (const candidate of [base, base + "s", base + "es"]) {
|
|
1275
|
+
const hits = byBareName.get(candidate);
|
|
1276
|
+
if (!hits?.length) continue;
|
|
1277
|
+
const sameSchema = hits.filter((t) => t.schema === from.schema);
|
|
1278
|
+
if (sameSchema.length === 1) return qualifiedTableName(sameSchema[0]);
|
|
1279
|
+
if (hits.length === 1) return qualifiedTableName(hits[0]);
|
|
1280
|
+
return void 0;
|
|
1281
|
+
}
|
|
1072
1282
|
return void 0;
|
|
1073
1283
|
};
|
|
1074
1284
|
for (const t of tables) {
|
|
@@ -1076,8 +1286,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1076
1286
|
if (c.references) continue;
|
|
1077
1287
|
if (c.name.endsWith("Id")) {
|
|
1078
1288
|
const base = c.name.slice(0, -2);
|
|
1079
|
-
const target = findTarget(base);
|
|
1080
|
-
if (target) relations.push({ kind: "one", from: t
|
|
1289
|
+
const target = findTarget(base, t);
|
|
1290
|
+
if (target) relations.push({ kind: "one", from: qualifiedTableName(t), to: target });
|
|
1081
1291
|
}
|
|
1082
1292
|
}
|
|
1083
1293
|
}
|
|
@@ -1186,10 +1396,58 @@ _SchemaAnalyzer.INEXACT_RANGES = {
|
|
|
1186
1396
|
SQLiteReal: null,
|
|
1187
1397
|
SingleStoreDouble: null,
|
|
1188
1398
|
SingleStoreReal: null,
|
|
1189
|
-
// `
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1399
|
+
// Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
|
|
1400
|
+
// used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
|
|
1401
|
+
// else at all, so a `real` column accepted 1e300 and the server refused it.
|
|
1402
|
+
//
|
|
1403
|
+
// Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
|
|
1404
|
+
// through the `gel` client, casting each literal so the server parses it, and again through a
|
|
1405
|
+
// stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
|
|
1406
|
+
//
|
|
1407
|
+
// 3.4028234663852886e38 accepted, returned unchanged
|
|
1408
|
+
// 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
|
|
1409
|
+
// 3.402823567797337e38 refused, "is out of range for type std::float32"
|
|
1410
|
+
// 1e300 refused, the same way
|
|
1411
|
+
//
|
|
1412
|
+
// The same value accepted, the same next double up refused, and the same rounding down of the
|
|
1413
|
+
// midpoint, so it takes the constant already here rather than a second name for one number.
|
|
1414
|
+
// float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
|
|
1415
|
+
// truthful finite bound.
|
|
1416
|
+
GelReal: PG_FLOAT4_RANGE,
|
|
1417
|
+
GelDoublePrecision: null
|
|
1418
|
+
};
|
|
1419
|
+
// `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
|
|
1420
|
+
// Its bound is not a fixed magnitude per class but the precision each column declares for itself,
|
|
1421
|
+
// which no table keyed on a class name can hold; see `declaredDecimalRange`.
|
|
1422
|
+
/**
|
|
1423
|
+
* The Postgres number columns that hold a non-finite double, and which of the three each holds.
|
|
1424
|
+
*
|
|
1425
|
+
* The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
|
|
1426
|
+
* fact stated on one path and not the other is a schema that changes when the user upgrades
|
|
1427
|
+
* drizzle, which the cross-major diff in `verify-packed.sh` fails on. These three class names are
|
|
1428
|
+
* the same on both majors, read off real `pgTable` columns on 0.45.2 and on 1.0.0-rc.4, so this
|
|
1429
|
+
* table also answers for a v1 column and the two answers are identical rather than merely
|
|
1430
|
+
* compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
|
|
1431
|
+
*
|
|
1432
|
+
* No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
|
|
1433
|
+
* `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
|
|
1434
|
+
* turning `NaN` into NULL, which is a different answer that has to arrive whole.
|
|
1435
|
+
*
|
|
1436
|
+
* Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
|
|
1437
|
+
* inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
|
|
1438
|
+
* `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
|
|
1439
|
+
* and again through a stored property. Without them every row of such a column failed validation.
|
|
1440
|
+
*
|
|
1441
|
+
* `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
|
|
1442
|
+
* `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
|
|
1443
|
+
* any width and an infinity only where no precision is declared, which is a per-column question
|
|
1444
|
+
* this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
|
|
1445
|
+
*/
|
|
1446
|
+
_SchemaAnalyzer.PG_NON_FINITE = {
|
|
1447
|
+
PgReal: { nan: true, infinity: true },
|
|
1448
|
+
PgDoublePrecision: { nan: true, infinity: true },
|
|
1449
|
+
GelReal: { nan: true, infinity: true },
|
|
1450
|
+
GelDoublePrecision: { nan: true, infinity: true }
|
|
1193
1451
|
};
|
|
1194
1452
|
var SchemaAnalyzer = _SchemaAnalyzer;
|
|
1195
1453
|
var index_default = SchemaAnalyzer;
|
|
@@ -1200,5 +1458,7 @@ var index_default = SchemaAnalyzer;
|
|
|
1200
1458
|
isDrizzleView,
|
|
1201
1459
|
isReadOnlyRelation,
|
|
1202
1460
|
isRelationsV2,
|
|
1461
|
+
qualifiedForeignTable,
|
|
1462
|
+
qualifiedTableName,
|
|
1203
1463
|
readRelationsV2
|
|
1204
1464
|
});
|