@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.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
function qualifiedTableName(table) {
|
|
3
|
+
return table.schema ? `${table.schema}.${table.name}` : table.name;
|
|
4
|
+
}
|
|
5
|
+
function qualifiedForeignTable(fk) {
|
|
6
|
+
return fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
|
|
7
|
+
}
|
|
2
8
|
function renderSqlLiteral(v) {
|
|
3
9
|
if (v === null || v === void 0) return "NULL";
|
|
4
10
|
if (typeof v === "number" || typeof v === "bigint") return String(v);
|
|
@@ -30,15 +36,23 @@ var GEOMETRIC_CLASS_SHAPES = {
|
|
|
30
36
|
// The object modes. `line({ mode: 'abc' })` is a `PgLineABC` and not a `PgLineObject`, and any
|
|
31
37
|
// mode but `'tuple'` builds the object class: `point({ mode: 'abc' })` is a `PgPointObject` too.
|
|
32
38
|
PgPointObject: { kind: "numberObject", fields: ["x", "y"] },
|
|
33
|
-
PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] }
|
|
39
|
+
PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] },
|
|
40
|
+
// `geometry()` and `geometry({ mode: 'xy' })` are two classes, not one class with a flag, and
|
|
41
|
+
// the fuzzer found both unnamed on this path. Their driver mappers disagree the same way the
|
|
42
|
+
// point ones do: the default hands back `[1, 2]` and the xy mode hands back `{ x: 1, y: 2 }`.
|
|
43
|
+
PgGeometry: { kind: "tuple", length: 2 },
|
|
44
|
+
PgGeometryObject: { kind: "numberObject", fields: ["x", "y"] }
|
|
34
45
|
};
|
|
35
46
|
var V1_ONLY_ENTITY_KINDS = /^(?:MsSql|Cockroach)/;
|
|
47
|
+
var NUMBER_VECTOR_CLASSES = /* @__PURE__ */ new Set(["PgVector", "PgHalfVector", "SingleStoreVector"]);
|
|
48
|
+
var BIT_STRING_CLASSES = /* @__PURE__ */ new Set(["PgBinaryVector"]);
|
|
36
49
|
var BYTE_STRING_CLASSES = /* @__PURE__ */ new Set([
|
|
37
50
|
"MySqlBinary",
|
|
38
51
|
"MySqlVarBinary",
|
|
39
52
|
"SingleStoreBinary",
|
|
40
53
|
"SingleStoreVarBinary"
|
|
41
54
|
]);
|
|
55
|
+
var BUFFER_CLASSES = /* @__PURE__ */ new Set(["SQLiteBlobBuffer"]);
|
|
42
56
|
function describeV1Column(column) {
|
|
43
57
|
const codec = column?.codec;
|
|
44
58
|
const dataType = column?.dataType;
|
|
@@ -57,14 +71,24 @@ function describeV1Column(column) {
|
|
|
57
71
|
const out = {};
|
|
58
72
|
switch (semantic) {
|
|
59
73
|
case "int8":
|
|
74
|
+
case "uint8":
|
|
60
75
|
case "int16":
|
|
61
76
|
case "int24":
|
|
62
77
|
case "int32":
|
|
63
78
|
case "int53":
|
|
64
79
|
case "uint53":
|
|
65
80
|
case "int64": {
|
|
81
|
+
if (DECIMAL_BIGINT_MODE.test(entityKind)) {
|
|
82
|
+
out.tsType = "bigint";
|
|
83
|
+
out.dbType = "NUMERIC";
|
|
84
|
+
out.integer = true;
|
|
85
|
+
const range2 = decimalModeRange(column, entityKind, "bigint");
|
|
86
|
+
if (range2) [out.min, out.max] = range2;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
66
89
|
const range = {
|
|
67
90
|
int8: ["-128", "127"],
|
|
91
|
+
uint8: ["0", "255"],
|
|
68
92
|
int16: ["-32768", "32767"],
|
|
69
93
|
int24: ["-8388608", "8388607"],
|
|
70
94
|
int32: ["-2147483648", "2147483647"],
|
|
@@ -77,7 +101,7 @@ function describeV1Column(column) {
|
|
|
77
101
|
[out.min, out.max] = range;
|
|
78
102
|
out.integer = true;
|
|
79
103
|
out.tsType = js === "bigint" ? "bigint" : "number";
|
|
80
|
-
out.dbType = semantic === "int8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
|
|
104
|
+
out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
|
|
81
105
|
break;
|
|
82
106
|
}
|
|
83
107
|
case "year":
|
|
@@ -93,6 +117,10 @@ function describeV1Column(column) {
|
|
|
93
117
|
out.integer = false;
|
|
94
118
|
out.tsType = "number";
|
|
95
119
|
out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
|
|
120
|
+
if (codec === "float4" || codec === "float8") {
|
|
121
|
+
out.allowsNaN = true;
|
|
122
|
+
out.allowsInfinity = true;
|
|
123
|
+
}
|
|
96
124
|
break;
|
|
97
125
|
}
|
|
98
126
|
case "uuid":
|
|
@@ -122,6 +150,16 @@ function describeV1Column(column) {
|
|
|
122
150
|
out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
|
|
123
151
|
break;
|
|
124
152
|
case "timestamp":
|
|
153
|
+
// `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
|
|
154
|
+
// stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
|
|
155
|
+
// are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
|
|
156
|
+
// `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
|
|
157
|
+
// `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
|
|
158
|
+
// A label only, since `dbType` is read outside this file in exactly one place,
|
|
159
|
+
// `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
|
|
160
|
+
// because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
|
|
161
|
+
// two majors disagreeing about a column is what the cross-major diff exists to catch.
|
|
162
|
+
case "datetime":
|
|
125
163
|
out.tsType = js === "string" ? "string" : "Date";
|
|
126
164
|
out.dbType = "TIMESTAMP";
|
|
127
165
|
break;
|
|
@@ -143,13 +181,25 @@ function describeV1Column(column) {
|
|
|
143
181
|
const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
|
|
144
182
|
const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
|
|
145
183
|
out.tsType = "string";
|
|
146
|
-
out.dbType =
|
|
184
|
+
out.dbType = bytes ? "BINARY" : "BIT";
|
|
147
185
|
out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
|
|
148
186
|
kind: "bitstring",
|
|
149
187
|
length: declaredLength(column),
|
|
150
188
|
// A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
|
|
151
189
|
// most that many, which is why `''` is valid there and not here.
|
|
152
|
-
|
|
190
|
+
//
|
|
191
|
+
// `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
|
|
192
|
+
// states no codec, so both of its builders came back `exact: false` and a `bit(3)`
|
|
193
|
+
// was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
|
|
194
|
+
// `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
|
|
195
|
+
// type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
|
|
196
|
+
// refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
|
|
197
|
+
// 1.0.0-rc.4 answers the same for both columns.
|
|
198
|
+
//
|
|
199
|
+
// The class rather than a prefix, because `CockroachVarbit` starts with neither
|
|
200
|
+
// `CockroachBit` nor anything else this could key on without catching the varying
|
|
201
|
+
// half too.
|
|
202
|
+
exact: codec === "bit" || entity === "CockroachBit"
|
|
153
203
|
};
|
|
154
204
|
break;
|
|
155
205
|
}
|
|
@@ -175,7 +225,16 @@ function describeV1Column(column) {
|
|
|
175
225
|
out.dbType = "LINE";
|
|
176
226
|
out.shape = js === "object" ? { kind: "numberObject", fields: ["a", "b", "c"] } : { kind: "tuple", length: 3 };
|
|
177
227
|
break;
|
|
228
|
+
// `halfvec` beside `vector`, because they differ in storage width and in nothing a validator
|
|
229
|
+
// can see: `mapFromDriverValue` on both hands back `[1, 2, 3]`. It had no arm and came back
|
|
230
|
+
// `unknown` on this path too, which the fuzzer found.
|
|
231
|
+
//
|
|
232
|
+
// `sparsevec` is deliberately not here. Its name says vector and its value is the string
|
|
233
|
+
// `{1:1.5,3:2}/3`, so typing it `number[]` for symmetry would reject every row the database
|
|
234
|
+
// returns. Its codec already answers `string` on its own, which is the same conclusion reached
|
|
235
|
+
// without this arm.
|
|
178
236
|
case "vector":
|
|
237
|
+
case "halfvec":
|
|
179
238
|
out.tsType = "number[]";
|
|
180
239
|
out.dbType = "VECTOR";
|
|
181
240
|
out.shape = { kind: "numberVector", length: declaredLength(column) };
|
|
@@ -192,7 +251,12 @@ function describeV1Column(column) {
|
|
|
192
251
|
out.tsType = "number";
|
|
193
252
|
out.dbType = "NUMERIC";
|
|
194
253
|
out.integer = false;
|
|
195
|
-
|
|
254
|
+
const range = decimalModeRange(column, entityKind, "number");
|
|
255
|
+
if (range) [out.min, out.max] = range;
|
|
256
|
+
if (codec === "numeric:number") {
|
|
257
|
+
out.allowsNaN = true;
|
|
258
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
259
|
+
}
|
|
196
260
|
} else if (js === "string") {
|
|
197
261
|
out.tsType = "string";
|
|
198
262
|
out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
|
|
@@ -220,6 +284,28 @@ function declaredLength(column) {
|
|
|
220
284
|
const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
|
|
221
285
|
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
222
286
|
}
|
|
287
|
+
function declaredDecimalRange(column) {
|
|
288
|
+
const cfg = column?.config ?? {};
|
|
289
|
+
const precision = column?.precision ?? cfg.precision;
|
|
290
|
+
const scale = column?.scale ?? cfg.scale ?? 0;
|
|
291
|
+
if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
|
|
292
|
+
return void 0;
|
|
293
|
+
if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
|
|
294
|
+
const nines = "9".repeat(precision);
|
|
295
|
+
const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
|
|
296
|
+
return [`-${max}`, max];
|
|
297
|
+
}
|
|
298
|
+
var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
|
|
299
|
+
var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
|
|
300
|
+
var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
|
|
301
|
+
function decimalModeRange(column, kind, mode) {
|
|
302
|
+
const declared = declaredDecimalRange(column);
|
|
303
|
+
if (declared) return declared;
|
|
304
|
+
if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
|
|
305
|
+
return MYSQL_IMPLICIT_DECIMAL_RANGE;
|
|
306
|
+
if (kind.startsWith("SQLite")) return void 0;
|
|
307
|
+
return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
|
|
308
|
+
}
|
|
223
309
|
var VIEW_CONFIG_FIELDS = {
|
|
224
310
|
"drizzle:Columns": "selectedFields",
|
|
225
311
|
"drizzle:Name": "name",
|
|
@@ -264,27 +350,43 @@ function isRelationsV2(val) {
|
|
|
264
350
|
)
|
|
265
351
|
);
|
|
266
352
|
}
|
|
353
|
+
function qualifiedNameOfDrizzleTable(tbl) {
|
|
354
|
+
const name = getSymbolOf(tbl, "drizzle:Name");
|
|
355
|
+
if (typeof name !== "string" || !name) return void 0;
|
|
356
|
+
const schema = getSymbolOf(tbl, "drizzle:Schema");
|
|
357
|
+
return typeof schema === "string" && schema ? `${schema}.${name}` : name;
|
|
358
|
+
}
|
|
267
359
|
function readRelationsV2(val, issues = []) {
|
|
268
360
|
const out = [];
|
|
269
361
|
for (const [tableKey, entry] of Object.entries(val)) {
|
|
270
|
-
const from =
|
|
362
|
+
const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
|
|
271
363
|
for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
|
|
272
|
-
const to = r?.targetTableName;
|
|
364
|
+
const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
|
|
273
365
|
if (typeof to !== "string" || !to) {
|
|
274
366
|
issues.push({
|
|
275
367
|
code: "DRZL_ANL_REL_V2",
|
|
276
368
|
level: "warn",
|
|
277
|
-
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped
|
|
369
|
+
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`,
|
|
370
|
+
path: from
|
|
278
371
|
});
|
|
279
372
|
continue;
|
|
280
373
|
}
|
|
281
|
-
const via =
|
|
374
|
+
const via = qualifiedNameOfDrizzleTable(r.throughTable) ?? qualifiedNameOfDrizzleTable(r.through?.sourceTable) ?? void 0;
|
|
282
375
|
if (via) out.push({ kind: "manyToMany", from, to, via });
|
|
283
376
|
else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
|
|
284
377
|
}
|
|
285
378
|
}
|
|
286
379
|
return out;
|
|
287
380
|
}
|
|
381
|
+
function unknownColumnHint(reason) {
|
|
382
|
+
if (reason === "custom") {
|
|
383
|
+
return "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type.";
|
|
384
|
+
}
|
|
385
|
+
if (reason === "gel-temporal") {
|
|
386
|
+
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.";
|
|
387
|
+
}
|
|
388
|
+
return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
|
|
389
|
+
}
|
|
288
390
|
var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
289
391
|
constructor(schemaPath) {
|
|
290
392
|
this.schemaPath = schemaPath;
|
|
@@ -335,6 +437,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
335
437
|
code: "DRZL_ANL_EXTRACONFIG",
|
|
336
438
|
level: "warn",
|
|
337
439
|
message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
|
|
440
|
+
path: tableName,
|
|
338
441
|
hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
|
|
339
442
|
});
|
|
340
443
|
return [];
|
|
@@ -388,9 +491,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
388
491
|
};
|
|
389
492
|
const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
|
|
390
493
|
const toForeignTs = this.dbToTsNames(foreignColumnsObj);
|
|
494
|
+
const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
|
|
391
495
|
return {
|
|
392
496
|
columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
|
|
393
497
|
foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
|
|
498
|
+
...foreignSchema ? { foreignSchema } : {},
|
|
394
499
|
foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
|
|
395
500
|
onDelete: action(fk?.onDelete, fk?._onDelete),
|
|
396
501
|
onUpdate: action(fk?.onUpdate, fk?._onUpdate),
|
|
@@ -439,7 +544,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
439
544
|
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
440
545
|
*/
|
|
441
546
|
readRelationsObject(val, exportName, issues) {
|
|
442
|
-
const from =
|
|
547
|
+
const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
|
|
443
548
|
const make = (kind) => (table, cfg) => ({
|
|
444
549
|
kind,
|
|
445
550
|
referencedTable: table,
|
|
@@ -453,7 +558,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
453
558
|
const built = val.config({ one: make("one"), many: make("many") });
|
|
454
559
|
const out = [];
|
|
455
560
|
for (const rel of Object.values(built ?? {})) {
|
|
456
|
-
const to =
|
|
561
|
+
const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
|
|
457
562
|
if (to) out.push({ kind: rel.kind, from, to });
|
|
458
563
|
}
|
|
459
564
|
return out;
|
|
@@ -462,6 +567,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
462
567
|
code: "DRZL_ANL_RELATIONS",
|
|
463
568
|
level: "warn",
|
|
464
569
|
message: `Could not read the relations declared in "${exportName}": ${e.message}`,
|
|
570
|
+
path: from,
|
|
465
571
|
hint: "Relations for this table will be missing from the analysis."
|
|
466
572
|
});
|
|
467
573
|
return [];
|
|
@@ -482,10 +588,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
482
588
|
if (fks.length < 2) continue;
|
|
483
589
|
const fkCols = new Set(fks.flatMap((f) => f.columns));
|
|
484
590
|
if (!t.columns.every((c) => fkCols.has(c.name))) continue;
|
|
485
|
-
const targets = [...new Set(fks.map(
|
|
591
|
+
const targets = [...new Set(fks.map(qualifiedForeignTable))];
|
|
486
592
|
if (targets.length !== 2) continue;
|
|
487
|
-
|
|
488
|
-
out.push({ kind: "manyToMany", from: targets[
|
|
593
|
+
const via = qualifiedTableName(t);
|
|
594
|
+
out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via });
|
|
595
|
+
out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via });
|
|
489
596
|
}
|
|
490
597
|
return out;
|
|
491
598
|
}
|
|
@@ -512,6 +619,24 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
512
619
|
if (inexact) [out.min, out.max] = inexact;
|
|
513
620
|
out.integer = false;
|
|
514
621
|
}
|
|
622
|
+
const nonFinite = _SchemaAnalyzer.PG_NON_FINITE[ctor];
|
|
623
|
+
if (nonFinite) {
|
|
624
|
+
out.allowsNaN = nonFinite.nan;
|
|
625
|
+
out.allowsInfinity = nonFinite.infinity;
|
|
626
|
+
}
|
|
627
|
+
if (DECIMAL_NUMBER_MODE.test(ctor)) {
|
|
628
|
+
const range2 = decimalModeRange(column, ctor, "number");
|
|
629
|
+
if (range2) [out.min, out.max] = range2;
|
|
630
|
+
out.integer = false;
|
|
631
|
+
if (ctor === "PgNumericNumber") {
|
|
632
|
+
out.allowsNaN = true;
|
|
633
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
634
|
+
}
|
|
635
|
+
} else if (DECIMAL_BIGINT_MODE.test(ctor)) {
|
|
636
|
+
const range2 = decimalModeRange(column, ctor, "bigint");
|
|
637
|
+
if (range2) [out.min, out.max] = range2;
|
|
638
|
+
out.integer = true;
|
|
639
|
+
}
|
|
515
640
|
if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
|
|
516
641
|
return out;
|
|
517
642
|
}
|
|
@@ -523,12 +648,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
523
648
|
tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
|
|
524
649
|
dbType: "INTEGER"
|
|
525
650
|
};
|
|
651
|
+
// Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
|
|
652
|
+
// `timestamp_ms` differ in the scale of the number on the wire, seconds against
|
|
653
|
+
// milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
|
|
654
|
+
// either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
|
|
655
|
+
// the class covers both, where the mode check that used to answer this fell through the
|
|
656
|
+
// switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
|
|
657
|
+
// The second came back `unknown`, and every generator emitted a schema accepting anything.
|
|
658
|
+
//
|
|
659
|
+
// `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
|
|
660
|
+
// column identically. `dbType` is read in exactly one place outside this file,
|
|
661
|
+
// `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
|
|
662
|
+
// relabel reaches no output. Measured rather than argued: emitting a `Date` column under
|
|
663
|
+
// both labels, nullable and not, through all five generators gives ten byte-identical pairs.
|
|
664
|
+
case "SQLiteTimestamp":
|
|
665
|
+
return { tsType: "Date", dbType: "DATE" };
|
|
526
666
|
case "SQLiteText":
|
|
527
667
|
return { tsType: "string", dbType: "TEXT" };
|
|
528
668
|
case "SQLiteReal":
|
|
529
669
|
return { tsType: "number", dbType: "REAL" };
|
|
670
|
+
// No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
|
|
671
|
+
// `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
|
|
672
|
+
// all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
|
|
673
|
+
// produces, which is why a real `blob()` reached neither it nor anything else.
|
|
530
674
|
case "SQLiteBlob":
|
|
531
675
|
return { tsType: "Uint8Array", dbType: "BLOB" };
|
|
676
|
+
// The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
|
|
677
|
+
// for the measurement; the answers here are v1's own for the same column, so this is the
|
|
678
|
+
// two majors agreeing rather than a new opinion.
|
|
679
|
+
case "SQLiteBlobBuffer":
|
|
680
|
+
return { tsType: "Buffer", dbType: "BYTEA" };
|
|
532
681
|
// SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
|
|
533
682
|
// is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
|
|
534
683
|
// wider than the `any` a json column at least used to get.
|
|
@@ -567,6 +716,34 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
567
716
|
case "MySqlEnumColumn":
|
|
568
717
|
case "SingleStoreEnumColumn":
|
|
569
718
|
return { tsType: "string", dbType: "TEXT" };
|
|
719
|
+
// The pgvector family, found by the analyzer fuzzer: all three came back `unknown` on this
|
|
720
|
+
// path, so their validators accepted anything. The answers are drizzle's own mappers rather
|
|
721
|
+
// than the type names, and the three do not agree with each other:
|
|
722
|
+
//
|
|
723
|
+
// vector(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
724
|
+
// halfvec(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
725
|
+
// sparsevec(3) SELECT gives "{1:1.5,3:2}/3" INSERT sends "{1:1.5,3:2}/3"
|
|
726
|
+
//
|
|
727
|
+
// So the two dense ones are number arrays and the sparse one is a string. Typing `sparsevec`
|
|
728
|
+
// as a vector for symmetry would reject every row the database returns, which is the defect
|
|
729
|
+
// this family was filed under to begin with. The `shape` carries the dimension count where
|
|
730
|
+
// one is declared, as the codec path already did for `vector`.
|
|
731
|
+
case "PgVector":
|
|
732
|
+
case "PgHalfVector":
|
|
733
|
+
case "SingleStoreVector":
|
|
734
|
+
return { tsType: "number[]", dbType: "VECTOR" };
|
|
735
|
+
// `BIT` rather than `TEXT`, which a first version of this arm returned. v1's codec says `BIT`
|
|
736
|
+
// for the same column, and the cross-major diff said so: naming the class made ten of its
|
|
737
|
+
// twelve entries go stale and left `c_bit.dbType` and its nullable twin standing, which is
|
|
738
|
+
// that check distinguishing a fix from a half fix.
|
|
739
|
+
case "PgBinaryVector":
|
|
740
|
+
return { tsType: "string", dbType: "BIT" };
|
|
741
|
+
case "PgGeometry":
|
|
742
|
+
return { tsType: "[number, number]", dbType: "GEOMETRY" };
|
|
743
|
+
case "PgGeometryObject":
|
|
744
|
+
return { tsType: "{ x: number; y: number }", dbType: "GEOMETRY" };
|
|
745
|
+
case "PgSparseVector":
|
|
746
|
+
return { tsType: "string", dbType: "TEXT" };
|
|
570
747
|
case "PgInteger":
|
|
571
748
|
case "PgSmallInt":
|
|
572
749
|
return { tsType: "number", dbType: "INTEGER" };
|
|
@@ -727,7 +904,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
727
904
|
if (/^Gel/i.test(ctor)) {
|
|
728
905
|
if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
|
|
729
906
|
if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
|
|
730
|
-
if (/
|
|
907
|
+
if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
|
|
908
|
+
if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
|
|
731
909
|
if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
|
|
732
910
|
if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
|
|
733
911
|
if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
|
|
@@ -736,7 +914,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
736
914
|
if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
|
|
737
915
|
if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
|
|
738
916
|
if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
|
|
739
|
-
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
917
|
+
return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
|
|
740
918
|
}
|
|
741
919
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
742
920
|
}
|
|
@@ -752,7 +930,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
752
930
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
753
931
|
for (const [colName, outerCol] of Object.entries(columnsObj)) {
|
|
754
932
|
const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
|
|
755
|
-
|
|
933
|
+
const mapped = this.mapColumnType(col);
|
|
934
|
+
let { tsType, dbType } = mapped;
|
|
756
935
|
if (tsType === "unknown" && /At$/.test(colName)) {
|
|
757
936
|
tsType = "Date";
|
|
758
937
|
dbType = "INTEGER";
|
|
@@ -778,11 +957,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
778
957
|
const v1 = describeV1Column(col);
|
|
779
958
|
const constraints = this.columnConstraints(col);
|
|
780
959
|
if (v1?.shape) delete constraints.maxLength;
|
|
781
|
-
const sqlKind = String(
|
|
960
|
+
const sqlKind = String(
|
|
961
|
+
outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
|
|
962
|
+
);
|
|
782
963
|
const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
|
|
783
964
|
const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
|
|
784
965
|
const ctorName = String(col?.constructor?.name ?? "");
|
|
785
|
-
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];
|
|
966
|
+
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];
|
|
786
967
|
if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
|
|
787
968
|
const shape = (v1?.shape ?? fallbackShape)?.kind;
|
|
788
969
|
const finalTs = v1?.tsType ?? tsType;
|
|
@@ -793,13 +974,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
793
974
|
code: "DRZL_ANL_UNKNOWN_COLUMN",
|
|
794
975
|
level: "warn",
|
|
795
976
|
message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
|
|
796
|
-
|
|
977
|
+
path: `${tsName}.${colName}`,
|
|
978
|
+
hint: unknownColumnHint(shape === "custom" ? "custom" : mapped.unnameable)
|
|
797
979
|
});
|
|
798
980
|
}
|
|
981
|
+
const dims = arrayDims || v1?.arrayDimensions || 0;
|
|
982
|
+
const declaredSqlType = (() => {
|
|
983
|
+
let raw;
|
|
984
|
+
try {
|
|
985
|
+
raw = typeof outerCol?.getSQLType === "function" ? outerCol.getSQLType() : void 0;
|
|
986
|
+
} catch {
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
if (typeof raw !== "string" || !raw) return void 0;
|
|
990
|
+
return raw.endsWith("]") ? raw : raw + "[]".repeat(dims);
|
|
991
|
+
})();
|
|
799
992
|
columns.push({
|
|
800
993
|
name: colName,
|
|
801
994
|
tsType,
|
|
802
995
|
dbType,
|
|
996
|
+
...declaredSqlType ? { sqlType: declaredSqlType } : {},
|
|
803
997
|
nullable,
|
|
804
998
|
hasDefault,
|
|
805
999
|
isGenerated,
|
|
@@ -876,6 +1070,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
876
1070
|
if (!col) continue;
|
|
877
1071
|
col.references = {
|
|
878
1072
|
table: fk.foreignTable,
|
|
1073
|
+
...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
|
|
879
1074
|
column: fk.foreignColumns[0],
|
|
880
1075
|
onDelete: fk.onDelete,
|
|
881
1076
|
onUpdate: fk.onUpdate
|
|
@@ -948,9 +1143,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
948
1143
|
}
|
|
949
1144
|
}
|
|
950
1145
|
if (opts.includeRelations) {
|
|
1146
|
+
const self = qualifiedTableName(table);
|
|
951
1147
|
for (const fk of table.foreignKeys ?? []) {
|
|
952
|
-
|
|
953
|
-
relations.push({ kind: "
|
|
1148
|
+
const target = qualifiedForeignTable(fk);
|
|
1149
|
+
relations.push({ kind: "one", from: self, to: target });
|
|
1150
|
+
relations.push({ kind: "many", from: target, to: self });
|
|
954
1151
|
}
|
|
955
1152
|
}
|
|
956
1153
|
} else if (this.isRelationsObject(val)) {
|
|
@@ -978,7 +1175,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
978
1175
|
issues.push({
|
|
979
1176
|
code: "DRZL_ANL_TABLE",
|
|
980
1177
|
level: "warn",
|
|
981
|
-
message: `Failed to analyze export ${name}: ${String(e)}
|
|
1178
|
+
message: `Failed to analyze export ${name}: ${String(e)}`,
|
|
1179
|
+
path: name
|
|
982
1180
|
});
|
|
983
1181
|
}
|
|
984
1182
|
}
|
|
@@ -1023,11 +1221,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1023
1221
|
relations.push(...this.inferManyToMany(tables));
|
|
1024
1222
|
}
|
|
1025
1223
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
1026
|
-
const
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1224
|
+
const byBareName = /* @__PURE__ */ new Map();
|
|
1225
|
+
for (const t of tables) {
|
|
1226
|
+
const list = byBareName.get(t.name);
|
|
1227
|
+
if (list) list.push(t);
|
|
1228
|
+
else byBareName.set(t.name, [t]);
|
|
1229
|
+
}
|
|
1230
|
+
const findTarget = (base, from) => {
|
|
1231
|
+
for (const candidate of [base, base + "s", base + "es"]) {
|
|
1232
|
+
const hits = byBareName.get(candidate);
|
|
1233
|
+
if (!hits?.length) continue;
|
|
1234
|
+
const sameSchema = hits.filter((t) => t.schema === from.schema);
|
|
1235
|
+
if (sameSchema.length === 1) return qualifiedTableName(sameSchema[0]);
|
|
1236
|
+
if (hits.length === 1) return qualifiedTableName(hits[0]);
|
|
1237
|
+
return void 0;
|
|
1238
|
+
}
|
|
1031
1239
|
return void 0;
|
|
1032
1240
|
};
|
|
1033
1241
|
for (const t of tables) {
|
|
@@ -1035,8 +1243,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1035
1243
|
if (c.references) continue;
|
|
1036
1244
|
if (c.name.endsWith("Id")) {
|
|
1037
1245
|
const base = c.name.slice(0, -2);
|
|
1038
|
-
const target = findTarget(base);
|
|
1039
|
-
if (target) relations.push({ kind: "one", from: t
|
|
1246
|
+
const target = findTarget(base, t);
|
|
1247
|
+
if (target) relations.push({ kind: "one", from: qualifiedTableName(t), to: target });
|
|
1040
1248
|
}
|
|
1041
1249
|
}
|
|
1042
1250
|
}
|
|
@@ -1145,10 +1353,58 @@ _SchemaAnalyzer.INEXACT_RANGES = {
|
|
|
1145
1353
|
SQLiteReal: null,
|
|
1146
1354
|
SingleStoreDouble: null,
|
|
1147
1355
|
SingleStoreReal: null,
|
|
1148
|
-
// `
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1356
|
+
// Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
|
|
1357
|
+
// used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
|
|
1358
|
+
// else at all, so a `real` column accepted 1e300 and the server refused it.
|
|
1359
|
+
//
|
|
1360
|
+
// Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
|
|
1361
|
+
// through the `gel` client, casting each literal so the server parses it, and again through a
|
|
1362
|
+
// stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
|
|
1363
|
+
//
|
|
1364
|
+
// 3.4028234663852886e38 accepted, returned unchanged
|
|
1365
|
+
// 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
|
|
1366
|
+
// 3.402823567797337e38 refused, "is out of range for type std::float32"
|
|
1367
|
+
// 1e300 refused, the same way
|
|
1368
|
+
//
|
|
1369
|
+
// The same value accepted, the same next double up refused, and the same rounding down of the
|
|
1370
|
+
// midpoint, so it takes the constant already here rather than a second name for one number.
|
|
1371
|
+
// float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
|
|
1372
|
+
// truthful finite bound.
|
|
1373
|
+
GelReal: PG_FLOAT4_RANGE,
|
|
1374
|
+
GelDoublePrecision: null
|
|
1375
|
+
};
|
|
1376
|
+
// `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
|
|
1377
|
+
// Its bound is not a fixed magnitude per class but the precision each column declares for itself,
|
|
1378
|
+
// which no table keyed on a class name can hold; see `declaredDecimalRange`.
|
|
1379
|
+
/**
|
|
1380
|
+
* The Postgres number columns that hold a non-finite double, and which of the three each holds.
|
|
1381
|
+
*
|
|
1382
|
+
* The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
|
|
1383
|
+
* fact stated on one path and not the other is a schema that changes when the user upgrades
|
|
1384
|
+
* drizzle, which the cross-major diff in `verify-packed.sh` fails on. These three class names are
|
|
1385
|
+
* the same on both majors, read off real `pgTable` columns on 0.45.2 and on 1.0.0-rc.4, so this
|
|
1386
|
+
* table also answers for a v1 column and the two answers are identical rather than merely
|
|
1387
|
+
* compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
|
|
1388
|
+
*
|
|
1389
|
+
* No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
|
|
1390
|
+
* `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
|
|
1391
|
+
* turning `NaN` into NULL, which is a different answer that has to arrive whole.
|
|
1392
|
+
*
|
|
1393
|
+
* Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
|
|
1394
|
+
* inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
|
|
1395
|
+
* `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
|
|
1396
|
+
* and again through a stored property. Without them every row of such a column failed validation.
|
|
1397
|
+
*
|
|
1398
|
+
* `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
|
|
1399
|
+
* `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
|
|
1400
|
+
* any width and an infinity only where no precision is declared, which is a per-column question
|
|
1401
|
+
* this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
|
|
1402
|
+
*/
|
|
1403
|
+
_SchemaAnalyzer.PG_NON_FINITE = {
|
|
1404
|
+
PgReal: { nan: true, infinity: true },
|
|
1405
|
+
PgDoublePrecision: { nan: true, infinity: true },
|
|
1406
|
+
GelReal: { nan: true, infinity: true },
|
|
1407
|
+
GelDoublePrecision: { nan: true, infinity: true }
|
|
1152
1408
|
};
|
|
1153
1409
|
var SchemaAnalyzer = _SchemaAnalyzer;
|
|
1154
1410
|
var index_default = SchemaAnalyzer;
|
|
@@ -1159,5 +1415,7 @@ export {
|
|
|
1159
1415
|
isDrizzleView,
|
|
1160
1416
|
isReadOnlyRelation,
|
|
1161
1417
|
isRelationsV2,
|
|
1418
|
+
qualifiedForeignTable,
|
|
1419
|
+
qualifiedTableName,
|
|
1162
1420
|
readRelationsV2
|
|
1163
1421
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"drizzle-orm": "^0.45.2",
|
|
31
|
+
"drizzle-orm-v1": "npm:drizzle-orm@1.0.0-rc.4",
|
|
31
32
|
"tsup": "^8.5.1",
|
|
32
33
|
"typescript": "^5.9.3"
|
|
33
34
|
},
|
|
@@ -51,6 +52,8 @@
|
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "tsup src/index.ts --dts --format esm,cjs --clean",
|
|
53
54
|
"lint": "eslint . --ext .ts",
|
|
54
|
-
"test": "vitest run --testTimeout=20000"
|
|
55
|
+
"test": "vitest run --testTimeout=20000",
|
|
56
|
+
"fuzz": "node test/fuzz/run.mjs",
|
|
57
|
+
"fuzz:gate": "node test/fuzz/run.mjs --gate"
|
|
55
58
|
}
|
|
56
59
|
}
|