@drzl/analyzer 1.18.0 → 1.21.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/README.md +16 -4
- package/dist/index.cjs +506 -63
- package/dist/index.d.cts +240 -6
- package/dist/index.d.ts +240 -6
- package/dist/index.js +504 -63
- 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,27 +114,63 @@ function describeV1Column(column) {
|
|
|
98
114
|
const out = {};
|
|
99
115
|
switch (semantic) {
|
|
100
116
|
case "int8":
|
|
117
|
+
case "uint8":
|
|
101
118
|
case "int16":
|
|
119
|
+
case "uint16":
|
|
102
120
|
case "int24":
|
|
121
|
+
case "uint24":
|
|
103
122
|
case "int32":
|
|
123
|
+
case "uint32":
|
|
104
124
|
case "int53":
|
|
105
125
|
case "uint53":
|
|
106
|
-
case "int64":
|
|
126
|
+
case "int64":
|
|
127
|
+
case "uint64": {
|
|
128
|
+
if (DECIMAL_BIGINT_MODE.test(entityKind)) {
|
|
129
|
+
out.tsType = "bigint";
|
|
130
|
+
out.dbType = "NUMERIC";
|
|
131
|
+
out.integer = true;
|
|
132
|
+
const range2 = decimalModeRange(column, entityKind, "bigint");
|
|
133
|
+
if (range2) [out.min, out.max] = range2;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if ((semantic === "int64" || semantic === "uint64") && js === "string") {
|
|
137
|
+
out.tsType = "string";
|
|
138
|
+
out.dbType = "BIGINT";
|
|
139
|
+
if (entityKind.startsWith("Pg")) out.format = "pgBigint";
|
|
140
|
+
else if (entityKind.startsWith("MySql") || entityKind.startsWith("SingleStore"))
|
|
141
|
+
out.format = "mysqlBigint";
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
107
144
|
const range = {
|
|
108
145
|
int8: ["-128", "127"],
|
|
146
|
+
uint8: ["0", "255"],
|
|
109
147
|
int16: ["-32768", "32767"],
|
|
148
|
+
uint16: ["0", "65535"],
|
|
110
149
|
int24: ["-8388608", "8388607"],
|
|
150
|
+
uint24: ["0", "16777215"],
|
|
111
151
|
int32: ["-2147483648", "2147483647"],
|
|
152
|
+
uint32: ["0", "4294967295"],
|
|
112
153
|
int53: ["-9007199254740991", "9007199254740991"],
|
|
113
154
|
// MySQL `serial` is `bigint unsigned auto_increment`, so it starts at 0 rather than
|
|
114
|
-
// spanning the signed range.
|
|
155
|
+
// spanning the signed range. An explicit `bigint({ mode: 'number', unsigned: true })`
|
|
156
|
+
// states the same semantic and takes the same answer.
|
|
115
157
|
uint53: ["0", "9007199254740991"],
|
|
116
|
-
int64: ["-9223372036854775808", "9223372036854775807"]
|
|
158
|
+
int64: ["-9223372036854775808", "9223372036854775807"],
|
|
159
|
+
uint64: ["0", "18446744073709551615"]
|
|
117
160
|
}[semantic];
|
|
118
161
|
[out.min, out.max] = range;
|
|
119
162
|
out.integer = true;
|
|
120
163
|
out.tsType = js === "bigint" ? "bigint" : "number";
|
|
121
|
-
out.dbType =
|
|
164
|
+
out.dbType = {
|
|
165
|
+
int8: "TINYINT",
|
|
166
|
+
uint8: "TINYINT",
|
|
167
|
+
int16: "SMALLINT",
|
|
168
|
+
uint16: "SMALLINT",
|
|
169
|
+
int24: "MEDIUMINT",
|
|
170
|
+
uint24: "MEDIUMINT",
|
|
171
|
+
int32: "INTEGER",
|
|
172
|
+
uint32: "INTEGER"
|
|
173
|
+
}[semantic] ?? "BIGINT";
|
|
122
174
|
break;
|
|
123
175
|
}
|
|
124
176
|
case "year":
|
|
@@ -134,6 +186,14 @@ function describeV1Column(column) {
|
|
|
134
186
|
out.integer = false;
|
|
135
187
|
out.tsType = "number";
|
|
136
188
|
out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
|
|
189
|
+
if (codec === "float4" || codec === "float8") {
|
|
190
|
+
out.allowsNaN = true;
|
|
191
|
+
out.allowsInfinity = true;
|
|
192
|
+
}
|
|
193
|
+
if (codec === "float" || codec === "double" || codec === "real" || entityKind.startsWith("SingleStore")) {
|
|
194
|
+
out.allowsNaN = false;
|
|
195
|
+
out.allowsInfinity = false;
|
|
196
|
+
}
|
|
137
197
|
break;
|
|
138
198
|
}
|
|
139
199
|
case "uuid":
|
|
@@ -163,6 +223,16 @@ function describeV1Column(column) {
|
|
|
163
223
|
out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
|
|
164
224
|
break;
|
|
165
225
|
case "timestamp":
|
|
226
|
+
// `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
|
|
227
|
+
// stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
|
|
228
|
+
// are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
|
|
229
|
+
// `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
|
|
230
|
+
// `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
|
|
231
|
+
// A label only, since `dbType` is read outside this file in exactly one place,
|
|
232
|
+
// `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
|
|
233
|
+
// because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
|
|
234
|
+
// two majors disagreeing about a column is what the cross-major diff exists to catch.
|
|
235
|
+
case "datetime":
|
|
166
236
|
out.tsType = js === "string" ? "string" : "Date";
|
|
167
237
|
out.dbType = "TIMESTAMP";
|
|
168
238
|
break;
|
|
@@ -184,13 +254,25 @@ function describeV1Column(column) {
|
|
|
184
254
|
const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
|
|
185
255
|
const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
|
|
186
256
|
out.tsType = "string";
|
|
187
|
-
out.dbType =
|
|
257
|
+
out.dbType = bytes ? "BINARY" : "BIT";
|
|
188
258
|
out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
|
|
189
259
|
kind: "bitstring",
|
|
190
260
|
length: declaredLength(column),
|
|
191
261
|
// A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
|
|
192
262
|
// most that many, which is why `''` is valid there and not here.
|
|
193
|
-
|
|
263
|
+
//
|
|
264
|
+
// `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
|
|
265
|
+
// states no codec, so both of its builders came back `exact: false` and a `bit(3)`
|
|
266
|
+
// was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
|
|
267
|
+
// `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
|
|
268
|
+
// type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
|
|
269
|
+
// refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
|
|
270
|
+
// 1.0.0-rc.4 answers the same for both columns.
|
|
271
|
+
//
|
|
272
|
+
// The class rather than a prefix, because `CockroachVarbit` starts with neither
|
|
273
|
+
// `CockroachBit` nor anything else this could key on without catching the varying
|
|
274
|
+
// half too.
|
|
275
|
+
exact: codec === "bit" || entity === "CockroachBit"
|
|
194
276
|
};
|
|
195
277
|
break;
|
|
196
278
|
}
|
|
@@ -216,7 +298,16 @@ function describeV1Column(column) {
|
|
|
216
298
|
out.dbType = "LINE";
|
|
217
299
|
out.shape = js === "object" ? { kind: "numberObject", fields: ["a", "b", "c"] } : { kind: "tuple", length: 3 };
|
|
218
300
|
break;
|
|
301
|
+
// `halfvec` beside `vector`, because they differ in storage width and in nothing a validator
|
|
302
|
+
// can see: `mapFromDriverValue` on both hands back `[1, 2, 3]`. It had no arm and came back
|
|
303
|
+
// `unknown` on this path too, which the fuzzer found.
|
|
304
|
+
//
|
|
305
|
+
// `sparsevec` is deliberately not here. Its name says vector and its value is the string
|
|
306
|
+
// `{1:1.5,3:2}/3`, so typing it `number[]` for symmetry would reject every row the database
|
|
307
|
+
// returns. Its codec already answers `string` on its own, which is the same conclusion reached
|
|
308
|
+
// without this arm.
|
|
219
309
|
case "vector":
|
|
310
|
+
case "halfvec":
|
|
220
311
|
out.tsType = "number[]";
|
|
221
312
|
out.dbType = "VECTOR";
|
|
222
313
|
out.shape = { kind: "numberVector", length: declaredLength(column) };
|
|
@@ -233,7 +324,12 @@ function describeV1Column(column) {
|
|
|
233
324
|
out.tsType = "number";
|
|
234
325
|
out.dbType = "NUMERIC";
|
|
235
326
|
out.integer = false;
|
|
236
|
-
|
|
327
|
+
const range = decimalModeRange(column, entityKind, "number");
|
|
328
|
+
if (range) [out.min, out.max] = range;
|
|
329
|
+
if (codec === "numeric:number") {
|
|
330
|
+
out.allowsNaN = true;
|
|
331
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
332
|
+
}
|
|
237
333
|
} else if (js === "string") {
|
|
238
334
|
out.tsType = "string";
|
|
239
335
|
out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
|
|
@@ -261,6 +357,28 @@ function declaredLength(column) {
|
|
|
261
357
|
const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
|
|
262
358
|
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
263
359
|
}
|
|
360
|
+
function declaredDecimalRange(column) {
|
|
361
|
+
const cfg = column?.config ?? {};
|
|
362
|
+
const precision = column?.precision ?? cfg.precision;
|
|
363
|
+
const scale = column?.scale ?? cfg.scale ?? 0;
|
|
364
|
+
if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
|
|
365
|
+
return void 0;
|
|
366
|
+
if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
|
|
367
|
+
const nines = "9".repeat(precision);
|
|
368
|
+
const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
|
|
369
|
+
return [`-${max}`, max];
|
|
370
|
+
}
|
|
371
|
+
var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
|
|
372
|
+
var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
|
|
373
|
+
var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
|
|
374
|
+
function decimalModeRange(column, kind, mode) {
|
|
375
|
+
const declared = declaredDecimalRange(column);
|
|
376
|
+
if (declared) return declared;
|
|
377
|
+
if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
|
|
378
|
+
return MYSQL_IMPLICIT_DECIMAL_RANGE;
|
|
379
|
+
if (kind.startsWith("SQLite")) return void 0;
|
|
380
|
+
return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
|
|
381
|
+
}
|
|
264
382
|
var VIEW_CONFIG_FIELDS = {
|
|
265
383
|
"drizzle:Columns": "selectedFields",
|
|
266
384
|
"drizzle:Name": "name",
|
|
@@ -305,28 +423,51 @@ function isRelationsV2(val) {
|
|
|
305
423
|
)
|
|
306
424
|
);
|
|
307
425
|
}
|
|
426
|
+
function qualifiedNameOfDrizzleTable(tbl) {
|
|
427
|
+
const name = getSymbolOf(tbl, "drizzle:Name");
|
|
428
|
+
if (typeof name !== "string" || !name) return void 0;
|
|
429
|
+
const schema = getSymbolOf(tbl, "drizzle:Schema");
|
|
430
|
+
return typeof schema === "string" && schema ? `${schema}.${name}` : name;
|
|
431
|
+
}
|
|
308
432
|
function readRelationsV2(val, issues = []) {
|
|
309
433
|
const out = [];
|
|
310
434
|
for (const [tableKey, entry] of Object.entries(val)) {
|
|
311
|
-
const from =
|
|
435
|
+
const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
|
|
312
436
|
for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
|
|
313
|
-
const to = r?.targetTableName;
|
|
437
|
+
const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
|
|
314
438
|
if (typeof to !== "string" || !to) {
|
|
315
439
|
issues.push({
|
|
316
440
|
code: "DRZL_ANL_REL_V2",
|
|
317
441
|
level: "warn",
|
|
318
|
-
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped
|
|
442
|
+
message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`,
|
|
443
|
+
path: from
|
|
319
444
|
});
|
|
320
445
|
continue;
|
|
321
446
|
}
|
|
322
|
-
const via =
|
|
447
|
+
const via = qualifiedNameOfDrizzleTable(r.throughTable) ?? qualifiedNameOfDrizzleTable(r.through?.sourceTable) ?? void 0;
|
|
323
448
|
if (via) out.push({ kind: "manyToMany", from, to, via });
|
|
324
449
|
else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
|
|
325
450
|
}
|
|
326
451
|
}
|
|
327
452
|
return out;
|
|
328
453
|
}
|
|
454
|
+
function unknownColumnHint(reason) {
|
|
455
|
+
if (reason === "custom") {
|
|
456
|
+
return "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type.";
|
|
457
|
+
}
|
|
458
|
+
if (reason === "gel-temporal") {
|
|
459
|
+
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.";
|
|
460
|
+
}
|
|
461
|
+
return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
|
|
462
|
+
}
|
|
329
463
|
var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
464
|
+
/**
|
|
465
|
+
* One path or several. The plural exists for drizzle-kit interop: kit's `schema` key names
|
|
466
|
+
* files in the plural (arrays, globs), and the commonest multi-file layout is a directory of
|
|
467
|
+
* one file per table with no barrel, so there is no single module to point at. Entries are
|
|
468
|
+
* concrete files, never globs; expansion is the caller's job, so this class's contract stays
|
|
469
|
+
* "load exactly these modules and read their exports as one schema".
|
|
470
|
+
*/
|
|
330
471
|
constructor(schemaPath) {
|
|
331
472
|
this.schemaPath = schemaPath;
|
|
332
473
|
}
|
|
@@ -376,6 +517,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
376
517
|
code: "DRZL_ANL_EXTRACONFIG",
|
|
377
518
|
level: "warn",
|
|
378
519
|
message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
|
|
520
|
+
path: tableName,
|
|
379
521
|
hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
|
|
380
522
|
});
|
|
381
523
|
return [];
|
|
@@ -429,9 +571,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
429
571
|
};
|
|
430
572
|
const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
|
|
431
573
|
const toForeignTs = this.dbToTsNames(foreignColumnsObj);
|
|
574
|
+
const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
|
|
432
575
|
return {
|
|
433
576
|
columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
|
|
434
577
|
foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
|
|
578
|
+
...foreignSchema ? { foreignSchema } : {},
|
|
435
579
|
foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
|
|
436
580
|
onDelete: action(fk?.onDelete, fk?._onDelete),
|
|
437
581
|
onUpdate: action(fk?.onUpdate, fk?._onUpdate),
|
|
@@ -480,7 +624,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
480
624
|
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
481
625
|
*/
|
|
482
626
|
readRelationsObject(val, exportName, issues) {
|
|
483
|
-
const from =
|
|
627
|
+
const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
|
|
484
628
|
const make = (kind) => (table, cfg) => ({
|
|
485
629
|
kind,
|
|
486
630
|
referencedTable: table,
|
|
@@ -494,7 +638,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
494
638
|
const built = val.config({ one: make("one"), many: make("many") });
|
|
495
639
|
const out = [];
|
|
496
640
|
for (const rel of Object.values(built ?? {})) {
|
|
497
|
-
const to =
|
|
641
|
+
const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
|
|
498
642
|
if (to) out.push({ kind: rel.kind, from, to });
|
|
499
643
|
}
|
|
500
644
|
return out;
|
|
@@ -503,6 +647,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
503
647
|
code: "DRZL_ANL_RELATIONS",
|
|
504
648
|
level: "warn",
|
|
505
649
|
message: `Could not read the relations declared in "${exportName}": ${e.message}`,
|
|
650
|
+
path: from,
|
|
506
651
|
hint: "Relations for this table will be missing from the analysis."
|
|
507
652
|
});
|
|
508
653
|
return [];
|
|
@@ -523,10 +668,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
523
668
|
if (fks.length < 2) continue;
|
|
524
669
|
const fkCols = new Set(fks.flatMap((f) => f.columns));
|
|
525
670
|
if (!t.columns.every((c) => fkCols.has(c.name))) continue;
|
|
526
|
-
const targets = [...new Set(fks.map(
|
|
671
|
+
const targets = [...new Set(fks.map(qualifiedForeignTable))];
|
|
527
672
|
if (targets.length !== 2) continue;
|
|
528
|
-
|
|
529
|
-
out.push({ kind: "manyToMany", from: targets[
|
|
673
|
+
const via = qualifiedTableName(t);
|
|
674
|
+
out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via });
|
|
675
|
+
out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via });
|
|
530
676
|
}
|
|
531
677
|
return out;
|
|
532
678
|
}
|
|
@@ -543,7 +689,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
543
689
|
if (typeof length === "number" && Number.isFinite(length) && length > 0) {
|
|
544
690
|
out.maxLength = length;
|
|
545
691
|
}
|
|
546
|
-
const
|
|
692
|
+
const unsignedRange = column?.config?.unsigned === true ? _SchemaAnalyzer.UNSIGNED_INT_RANGES[ctor] : void 0;
|
|
693
|
+
const range = unsignedRange ?? _SchemaAnalyzer.INT_RANGES[ctor];
|
|
547
694
|
if (range) {
|
|
548
695
|
[out.min, out.max] = range;
|
|
549
696
|
out.integer = true;
|
|
@@ -553,6 +700,24 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
553
700
|
if (inexact) [out.min, out.max] = inexact;
|
|
554
701
|
out.integer = false;
|
|
555
702
|
}
|
|
703
|
+
const nonFinite = _SchemaAnalyzer.NON_FINITE_BY_CLASS[ctor];
|
|
704
|
+
if (nonFinite) {
|
|
705
|
+
out.allowsNaN = nonFinite.nan;
|
|
706
|
+
out.allowsInfinity = nonFinite.infinity;
|
|
707
|
+
}
|
|
708
|
+
if (DECIMAL_NUMBER_MODE.test(ctor)) {
|
|
709
|
+
const range2 = decimalModeRange(column, ctor, "number");
|
|
710
|
+
if (range2) [out.min, out.max] = range2;
|
|
711
|
+
out.integer = false;
|
|
712
|
+
if (ctor === "PgNumericNumber") {
|
|
713
|
+
out.allowsNaN = true;
|
|
714
|
+
out.allowsInfinity = !declaredDecimalRange(column);
|
|
715
|
+
}
|
|
716
|
+
} else if (DECIMAL_BIGINT_MODE.test(ctor)) {
|
|
717
|
+
const range2 = decimalModeRange(column, ctor, "bigint");
|
|
718
|
+
if (range2) [out.min, out.max] = range2;
|
|
719
|
+
out.integer = true;
|
|
720
|
+
}
|
|
556
721
|
if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
|
|
557
722
|
return out;
|
|
558
723
|
}
|
|
@@ -564,12 +729,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
564
729
|
tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
|
|
565
730
|
dbType: "INTEGER"
|
|
566
731
|
};
|
|
732
|
+
// Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
|
|
733
|
+
// `timestamp_ms` differ in the scale of the number on the wire, seconds against
|
|
734
|
+
// milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
|
|
735
|
+
// either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
|
|
736
|
+
// the class covers both, where the mode check that used to answer this fell through the
|
|
737
|
+
// switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
|
|
738
|
+
// The second came back `unknown`, and every generator emitted a schema accepting anything.
|
|
739
|
+
//
|
|
740
|
+
// `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
|
|
741
|
+
// column identically. `dbType` is read in exactly one place outside this file,
|
|
742
|
+
// `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
|
|
743
|
+
// relabel reaches no output. Measured rather than argued: emitting a `Date` column under
|
|
744
|
+
// both labels, nullable and not, through all five generators gives ten byte-identical pairs.
|
|
745
|
+
case "SQLiteTimestamp":
|
|
746
|
+
return { tsType: "Date", dbType: "DATE" };
|
|
567
747
|
case "SQLiteText":
|
|
568
748
|
return { tsType: "string", dbType: "TEXT" };
|
|
569
749
|
case "SQLiteReal":
|
|
570
750
|
return { tsType: "number", dbType: "REAL" };
|
|
751
|
+
// No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
|
|
752
|
+
// `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
|
|
753
|
+
// all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
|
|
754
|
+
// produces, which is why a real `blob()` reached neither it nor anything else.
|
|
571
755
|
case "SQLiteBlob":
|
|
572
756
|
return { tsType: "Uint8Array", dbType: "BLOB" };
|
|
757
|
+
// The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
|
|
758
|
+
// for the measurement; the answers here are v1's own for the same column, so this is the
|
|
759
|
+
// two majors agreeing rather than a new opinion.
|
|
760
|
+
case "SQLiteBlobBuffer":
|
|
761
|
+
return { tsType: "Buffer", dbType: "BYTEA" };
|
|
573
762
|
// SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
|
|
574
763
|
// is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
|
|
575
764
|
// wider than the `any` a json column at least used to get.
|
|
@@ -608,6 +797,34 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
608
797
|
case "MySqlEnumColumn":
|
|
609
798
|
case "SingleStoreEnumColumn":
|
|
610
799
|
return { tsType: "string", dbType: "TEXT" };
|
|
800
|
+
// The pgvector family, found by the analyzer fuzzer: all three came back `unknown` on this
|
|
801
|
+
// path, so their validators accepted anything. The answers are drizzle's own mappers rather
|
|
802
|
+
// than the type names, and the three do not agree with each other:
|
|
803
|
+
//
|
|
804
|
+
// vector(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
805
|
+
// halfvec(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
|
|
806
|
+
// sparsevec(3) SELECT gives "{1:1.5,3:2}/3" INSERT sends "{1:1.5,3:2}/3"
|
|
807
|
+
//
|
|
808
|
+
// So the two dense ones are number arrays and the sparse one is a string. Typing `sparsevec`
|
|
809
|
+
// as a vector for symmetry would reject every row the database returns, which is the defect
|
|
810
|
+
// this family was filed under to begin with. The `shape` carries the dimension count where
|
|
811
|
+
// one is declared, as the codec path already did for `vector`.
|
|
812
|
+
case "PgVector":
|
|
813
|
+
case "PgHalfVector":
|
|
814
|
+
case "SingleStoreVector":
|
|
815
|
+
return { tsType: "number[]", dbType: "VECTOR" };
|
|
816
|
+
// `BIT` rather than `TEXT`, which a first version of this arm returned. v1's codec says `BIT`
|
|
817
|
+
// for the same column, and the cross-major diff said so: naming the class made ten of its
|
|
818
|
+
// twelve entries go stale and left `c_bit.dbType` and its nullable twin standing, which is
|
|
819
|
+
// that check distinguishing a fix from a half fix.
|
|
820
|
+
case "PgBinaryVector":
|
|
821
|
+
return { tsType: "string", dbType: "BIT" };
|
|
822
|
+
case "PgGeometry":
|
|
823
|
+
return { tsType: "[number, number]", dbType: "GEOMETRY" };
|
|
824
|
+
case "PgGeometryObject":
|
|
825
|
+
return { tsType: "{ x: number; y: number }", dbType: "GEOMETRY" };
|
|
826
|
+
case "PgSparseVector":
|
|
827
|
+
return { tsType: "string", dbType: "TEXT" };
|
|
611
828
|
case "PgInteger":
|
|
612
829
|
case "PgSmallInt":
|
|
613
830
|
return { tsType: "number", dbType: "INTEGER" };
|
|
@@ -728,7 +945,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
728
945
|
}
|
|
729
946
|
if (/Numeric|Float|Double|Real/i.test(ctor))
|
|
730
947
|
return { tsType: "number", dbType: "NUMERIC" };
|
|
731
|
-
if (/
|
|
948
|
+
if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
|
|
949
|
+
if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
|
|
732
950
|
return { tsType: "number", dbType: "INTEGER" };
|
|
733
951
|
if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
|
|
734
952
|
if (/TimestampString|DateTimeString|DateString/i.test(ctor))
|
|
@@ -752,7 +970,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
752
970
|
}
|
|
753
971
|
if (/Numeric|Float|Double|Real/i.test(ctor))
|
|
754
972
|
return { tsType: "number", dbType: "NUMERIC" };
|
|
755
|
-
if (/
|
|
973
|
+
if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
|
|
974
|
+
if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
|
|
756
975
|
return { tsType: "number", dbType: "INTEGER" };
|
|
757
976
|
if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
|
|
758
977
|
if (/TimestampString|DateTimeString|DateString/i.test(ctor))
|
|
@@ -768,7 +987,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
768
987
|
if (/^Gel/i.test(ctor)) {
|
|
769
988
|
if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
|
|
770
989
|
if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
|
|
771
|
-
if (/
|
|
990
|
+
if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
|
|
991
|
+
if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
|
|
772
992
|
if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
|
|
773
993
|
if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
|
|
774
994
|
if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
|
|
@@ -777,7 +997,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
777
997
|
if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
|
|
778
998
|
if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
|
|
779
999
|
if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
|
|
780
|
-
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
1000
|
+
return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
|
|
781
1001
|
}
|
|
782
1002
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
783
1003
|
}
|
|
@@ -793,7 +1013,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
793
1013
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
794
1014
|
for (const [colName, outerCol] of Object.entries(columnsObj)) {
|
|
795
1015
|
const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
|
|
796
|
-
|
|
1016
|
+
const mapped = this.mapColumnType(col);
|
|
1017
|
+
let { tsType, dbType } = mapped;
|
|
797
1018
|
if (tsType === "unknown" && /At$/.test(colName)) {
|
|
798
1019
|
tsType = "Date";
|
|
799
1020
|
dbType = "INTEGER";
|
|
@@ -819,11 +1040,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
819
1040
|
const v1 = describeV1Column(col);
|
|
820
1041
|
const constraints = this.columnConstraints(col);
|
|
821
1042
|
if (v1?.shape) delete constraints.maxLength;
|
|
822
|
-
const sqlKind = String(
|
|
1043
|
+
const sqlKind = String(
|
|
1044
|
+
outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
|
|
1045
|
+
);
|
|
823
1046
|
const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
|
|
824
1047
|
const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
|
|
825
1048
|
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];
|
|
1049
|
+
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
1050
|
if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
|
|
828
1051
|
const shape = (v1?.shape ?? fallbackShape)?.kind;
|
|
829
1052
|
const finalTs = v1?.tsType ?? tsType;
|
|
@@ -834,13 +1057,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
834
1057
|
code: "DRZL_ANL_UNKNOWN_COLUMN",
|
|
835
1058
|
level: "warn",
|
|
836
1059
|
message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
|
|
837
|
-
|
|
1060
|
+
path: `${tsName}.${colName}`,
|
|
1061
|
+
hint: unknownColumnHint(shape === "custom" ? "custom" : mapped.unnameable)
|
|
838
1062
|
});
|
|
839
1063
|
}
|
|
1064
|
+
const dims = arrayDims || v1?.arrayDimensions || 0;
|
|
1065
|
+
const declaredSqlType = (() => {
|
|
1066
|
+
let raw;
|
|
1067
|
+
try {
|
|
1068
|
+
raw = typeof outerCol?.getSQLType === "function" ? outerCol.getSQLType() : void 0;
|
|
1069
|
+
} catch {
|
|
1070
|
+
return void 0;
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof raw !== "string" || !raw) return void 0;
|
|
1073
|
+
return raw.endsWith("]") ? raw : raw + "[]".repeat(dims);
|
|
1074
|
+
})();
|
|
840
1075
|
columns.push({
|
|
841
1076
|
name: colName,
|
|
842
1077
|
tsType,
|
|
843
1078
|
dbType,
|
|
1079
|
+
...declaredSqlType ? { sqlType: declaredSqlType } : {},
|
|
844
1080
|
nullable,
|
|
845
1081
|
hasDefault,
|
|
846
1082
|
isGenerated,
|
|
@@ -917,6 +1153,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
917
1153
|
if (!col) continue;
|
|
918
1154
|
col.references = {
|
|
919
1155
|
table: fk.foreignTable,
|
|
1156
|
+
...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
|
|
920
1157
|
column: fk.foreignColumns[0],
|
|
921
1158
|
onDelete: fk.onDelete,
|
|
922
1159
|
onUpdate: fk.onUpdate
|
|
@@ -945,31 +1182,85 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
945
1182
|
const fs = await import("fs/promises");
|
|
946
1183
|
const path = await import("path");
|
|
947
1184
|
const issues = [];
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1185
|
+
const listed = Array.isArray(this.schemaPath);
|
|
1186
|
+
const inputs = listed ? this.schemaPath : [this.schemaPath];
|
|
1187
|
+
const fulls = inputs.map((p) => path.resolve(process.cwd(), p));
|
|
1188
|
+
let missing = false;
|
|
1189
|
+
for (let i = 0; i < fulls.length; i++) {
|
|
1190
|
+
try {
|
|
1191
|
+
await fs.access(fulls[i]);
|
|
1192
|
+
} catch (_e) {
|
|
1193
|
+
missing = true;
|
|
1194
|
+
issues.push({
|
|
1195
|
+
code: "DRZL_ANL_NOFILE",
|
|
1196
|
+
level: "error",
|
|
1197
|
+
message: `Schema file not found: ${inputs[i]}`
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
958
1200
|
}
|
|
959
|
-
|
|
960
|
-
try {
|
|
961
|
-
const { default: jiti } = await import("jiti");
|
|
962
|
-
const jit = jiti(import_meta.url, { moduleCache: false });
|
|
963
|
-
mod = jit(full);
|
|
964
|
-
} catch (e) {
|
|
965
|
-
issues.push({
|
|
966
|
-
code: "DRZL_ANL_IMPORT",
|
|
967
|
-
level: "error",
|
|
968
|
-
message: `Failed to import schema: ${String(e)}`
|
|
969
|
-
});
|
|
1201
|
+
if (missing) {
|
|
970
1202
|
return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
|
|
971
1203
|
}
|
|
972
|
-
const
|
|
1204
|
+
const { default: jiti } = await import("jiti");
|
|
1205
|
+
const jit = jiti(import_meta.url, { moduleCache: false });
|
|
1206
|
+
const exportsObj = {};
|
|
1207
|
+
const exportOrigin = /* @__PURE__ */ new Map();
|
|
1208
|
+
const duplicateDisagreement = (a, b) => {
|
|
1209
|
+
if (Object.is(a, b)) return null;
|
|
1210
|
+
const aCols = this.getSymbol(a, "drizzle:Columns");
|
|
1211
|
+
const bCols = this.getSymbol(b, "drizzle:Columns");
|
|
1212
|
+
if (aCols && bCols) {
|
|
1213
|
+
const aName = this.getSymbol(a, "drizzle:Name");
|
|
1214
|
+
const bName = this.getSymbol(b, "drizzle:Name");
|
|
1215
|
+
const aSchema = this.getSymbol(a, "drizzle:Schema");
|
|
1216
|
+
const bSchema = this.getSymbol(b, "drizzle:Schema");
|
|
1217
|
+
if (aName !== bName || aSchema !== bSchema) {
|
|
1218
|
+
return `two different tables ("${String(aName)}" and "${String(bName)}")`;
|
|
1219
|
+
}
|
|
1220
|
+
if (Object.keys(aCols).join(",") !== Object.keys(bCols).join(",")) {
|
|
1221
|
+
return `two declarations of table "${String(aName)}" with different columns`;
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
if (!!aCols !== !!bCols) return "a table and a non-table";
|
|
1226
|
+
const aEnum = a?.enumValues;
|
|
1227
|
+
const bEnum = b?.enumValues;
|
|
1228
|
+
if (Array.isArray(aEnum) && Array.isArray(bEnum)) {
|
|
1229
|
+
return JSON.stringify(aEnum) === JSON.stringify(bEnum) ? null : "two enums with different values";
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
};
|
|
1233
|
+
for (let i = 0; i < fulls.length; i++) {
|
|
1234
|
+
let mod;
|
|
1235
|
+
try {
|
|
1236
|
+
mod = jit(fulls[i]);
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
issues.push({
|
|
1239
|
+
code: "DRZL_ANL_IMPORT",
|
|
1240
|
+
level: "error",
|
|
1241
|
+
// The single-path message keeps its historical bytes; a list names the file, since
|
|
1242
|
+
// "the schema" no longer identifies one.
|
|
1243
|
+
message: listed ? `Failed to import schema ${inputs[i]}: ${String(e)}` : `Failed to import schema: ${String(e)}`
|
|
1244
|
+
});
|
|
1245
|
+
return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
|
|
1246
|
+
}
|
|
1247
|
+
const one = mod?.default && typeof mod.default === "object" ? mod.default : mod;
|
|
1248
|
+
for (const [name, val] of Object.entries(one)) {
|
|
1249
|
+
if (!(name in exportsObj)) {
|
|
1250
|
+
exportsObj[name] = val;
|
|
1251
|
+
exportOrigin.set(name, inputs[i]);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
const disagreement = duplicateDisagreement(exportsObj[name], val);
|
|
1255
|
+
if (!disagreement) continue;
|
|
1256
|
+
issues.push({
|
|
1257
|
+
code: "DRZL_ANL_DUP_EXPORT",
|
|
1258
|
+
level: "warn",
|
|
1259
|
+
message: `Export "${name}" is ${disagreement}: defined by both ${exportOrigin.get(name)} and ${inputs[i]}; keeping the one in ${exportOrigin.get(name)}.`,
|
|
1260
|
+
path: name
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
973
1264
|
const tables = [];
|
|
974
1265
|
const relations = [];
|
|
975
1266
|
const enums = [];
|
|
@@ -989,9 +1280,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
989
1280
|
}
|
|
990
1281
|
}
|
|
991
1282
|
if (opts.includeRelations) {
|
|
1283
|
+
const self = qualifiedTableName(table);
|
|
992
1284
|
for (const fk of table.foreignKeys ?? []) {
|
|
993
|
-
|
|
994
|
-
relations.push({ kind: "
|
|
1285
|
+
const target = qualifiedForeignTable(fk);
|
|
1286
|
+
relations.push({ kind: "one", from: self, to: target });
|
|
1287
|
+
relations.push({ kind: "many", from: target, to: self });
|
|
995
1288
|
}
|
|
996
1289
|
}
|
|
997
1290
|
} else if (this.isRelationsObject(val)) {
|
|
@@ -1019,7 +1312,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1019
1312
|
issues.push({
|
|
1020
1313
|
code: "DRZL_ANL_TABLE",
|
|
1021
1314
|
level: "warn",
|
|
1022
|
-
message: `Failed to analyze export ${name}: ${String(e)}
|
|
1315
|
+
message: `Failed to analyze export ${name}: ${String(e)}`,
|
|
1316
|
+
path: name
|
|
1023
1317
|
});
|
|
1024
1318
|
}
|
|
1025
1319
|
}
|
|
@@ -1064,11 +1358,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1064
1358
|
relations.push(...this.inferManyToMany(tables));
|
|
1065
1359
|
}
|
|
1066
1360
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1361
|
+
const byBareName = /* @__PURE__ */ new Map();
|
|
1362
|
+
for (const t of tables) {
|
|
1363
|
+
const list = byBareName.get(t.name);
|
|
1364
|
+
if (list) list.push(t);
|
|
1365
|
+
else byBareName.set(t.name, [t]);
|
|
1366
|
+
}
|
|
1367
|
+
const findTarget = (base, from) => {
|
|
1368
|
+
for (const candidate of [base, base + "s", base + "es"]) {
|
|
1369
|
+
const hits = byBareName.get(candidate);
|
|
1370
|
+
if (!hits?.length) continue;
|
|
1371
|
+
const sameSchema = hits.filter((t) => t.schema === from.schema);
|
|
1372
|
+
if (sameSchema.length === 1) return qualifiedTableName(sameSchema[0]);
|
|
1373
|
+
if (hits.length === 1) return qualifiedTableName(hits[0]);
|
|
1374
|
+
return void 0;
|
|
1375
|
+
}
|
|
1072
1376
|
return void 0;
|
|
1073
1377
|
};
|
|
1074
1378
|
for (const t of tables) {
|
|
@@ -1076,8 +1380,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1076
1380
|
if (c.references) continue;
|
|
1077
1381
|
if (c.name.endsWith("Id")) {
|
|
1078
1382
|
const base = c.name.slice(0, -2);
|
|
1079
|
-
const target = findTarget(base);
|
|
1080
|
-
if (target) relations.push({ kind: "one", from: t
|
|
1383
|
+
const target = findTarget(base, t);
|
|
1384
|
+
if (target) relations.push({ kind: "one", from: qualifiedTableName(t), to: target });
|
|
1081
1385
|
}
|
|
1082
1386
|
}
|
|
1083
1387
|
}
|
|
@@ -1109,6 +1413,12 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
|
|
|
1109
1413
|
_SchemaAnalyzer.INT_RANGES = {
|
|
1110
1414
|
// 8 bit
|
|
1111
1415
|
MySqlTinyInt: ["-128", "127"],
|
|
1416
|
+
// Absent until the unsigned fix swept the family: v1 states `number int8` for the same
|
|
1417
|
+
// column, so the majors disagreed about every SingleStore tinyint. That is the shape the
|
|
1418
|
+
// cross-major diff in `scripts/verify-packed.sh` exists to catch, and its fixture carries no
|
|
1419
|
+
// SingleStore table, so unsigned-int-ranges.spec.ts holds these two classes across both
|
|
1420
|
+
// majors instead. The width is the type's, the one `MySqlTinyInt` beside it already carries.
|
|
1421
|
+
SingleStoreTinyInt: ["-128", "127"],
|
|
1112
1422
|
SQLiteInteger: ["-9223372036854775808", "9223372036854775807"],
|
|
1113
1423
|
// 16 bit
|
|
1114
1424
|
PgSmallInt: ["-32768", "32767"],
|
|
@@ -1121,6 +1431,8 @@ _SchemaAnalyzer.INT_RANGES = {
|
|
|
1121
1431
|
SingleStoreSmallInt: ["-32768", "32767"],
|
|
1122
1432
|
// 24 bit
|
|
1123
1433
|
MySqlMediumInt: ["-8388608", "8388607"],
|
|
1434
|
+
// As SingleStoreTinyInt above: v1 states `number int24` and this table said nothing.
|
|
1435
|
+
SingleStoreMediumInt: ["-8388608", "8388607"],
|
|
1124
1436
|
// 32 bit
|
|
1125
1437
|
PgInteger: ["-2147483648", "2147483647"],
|
|
1126
1438
|
PgSerial: ["-2147483648", "2147483647"],
|
|
@@ -1135,7 +1447,61 @@ _SchemaAnalyzer.INT_RANGES = {
|
|
|
1135
1447
|
PgBigInt64: ["-9223372036854775808", "9223372036854775807"],
|
|
1136
1448
|
PgBigSerial64: ["-9223372036854775808", "9223372036854775807"],
|
|
1137
1449
|
MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
|
|
1138
|
-
SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
|
|
1450
|
+
SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"],
|
|
1451
|
+
// MySQL and SingleStore `serial`, which is `bigint unsigned auto_increment`: unsigned by the
|
|
1452
|
+
// builder's own definition, with no `config.unsigned` stating it, so the flag-keyed table
|
|
1453
|
+
// below cannot answer and the range lives here. The mode is number, so the safe-integer
|
|
1454
|
+
// ceiling rather than the column's, exactly as the 53 bit block above. The Postgres serials
|
|
1455
|
+
// stay signed on purpose: a Postgres serial is a plain integer defaulting from a sequence,
|
|
1456
|
+
// and the negative backfill note above applies to them and not to these. Before this entry
|
|
1457
|
+
// the class was in no table at all, so an auto-increment column accepted -1 and the majors
|
|
1458
|
+
// disagreed: v1 states `number uint53` for the same column and was already bounded.
|
|
1459
|
+
MySqlSerial: ["0", "9007199254740991"],
|
|
1460
|
+
SingleStoreSerial: ["0", "9007199254740991"]
|
|
1461
|
+
};
|
|
1462
|
+
/**
|
|
1463
|
+
* The same widths with `{ unsigned: true }` set, which is the half the table above cannot see.
|
|
1464
|
+
*
|
|
1465
|
+
* On 0.4x the flag moves no class name: `int('x', { unsigned: true })` still builds a
|
|
1466
|
+
* `MySqlInt`, and only `config.unsigned` and the ` unsigned` suffix on `getSQLType()` record
|
|
1467
|
+
* the difference, measured off real 0.45.2 columns. So the table above answered every unsigned
|
|
1468
|
+
* width with its signed range, and the emitted select schema refused every stored value in the
|
|
1469
|
+
* upper half of the column: an `int unsigned` holding 4294967295 failed validation on a row the
|
|
1470
|
+
* database returned, and the same one width up meant `bigint unsigned` refused
|
|
1471
|
+
* 18446744073709551615n.
|
|
1472
|
+
*
|
|
1473
|
+
* The ceilings are the type's, verified against a live MySQL 8.4.11: 255, 65535, 16777215 and
|
|
1474
|
+
* 4294967295 store and return, -1 and each ceiling plus one are refused with
|
|
1475
|
+
* ER_WARN_DATA_OUT_OF_RANGE. The bigint pair keeps the two modes apart for the reason the
|
|
1476
|
+
* signed pair above does: number mode tops out at the safe-integer bound the wire imposes,
|
|
1477
|
+
* bigint mode at the column's own 2^64-1, which a bigint can spell. SingleStore is MySQL wire
|
|
1478
|
+
* compatible, ships the same builders with the same `config.unsigned`, and v1 states the same
|
|
1479
|
+
* `uintN` semantics for it, measured off real rc.4 columns; the entries keep the majors in
|
|
1480
|
+
* agreement, which is what the cross-major diff in `scripts/verify-packed.sh` holds together.
|
|
1481
|
+
*
|
|
1482
|
+
* Keyed by class exactly like `INT_RANGES`, and consulted only when `config.unsigned` is
|
|
1483
|
+
* `true`, so no Postgres or SQLite column can ever reach it: neither dialect has an unsigned
|
|
1484
|
+
* spelling, neither builder accepts the flag, and no class of theirs is named here.
|
|
1485
|
+
*/
|
|
1486
|
+
_SchemaAnalyzer.UNSIGNED_INT_RANGES = {
|
|
1487
|
+
// 8 bit
|
|
1488
|
+
MySqlTinyInt: ["0", "255"],
|
|
1489
|
+
SingleStoreTinyInt: ["0", "255"],
|
|
1490
|
+
// 16 bit
|
|
1491
|
+
MySqlSmallInt: ["0", "65535"],
|
|
1492
|
+
SingleStoreSmallInt: ["0", "65535"],
|
|
1493
|
+
// 24 bit
|
|
1494
|
+
MySqlMediumInt: ["0", "16777215"],
|
|
1495
|
+
SingleStoreMediumInt: ["0", "16777215"],
|
|
1496
|
+
// 32 bit
|
|
1497
|
+
MySqlInt: ["0", "4294967295"],
|
|
1498
|
+
SingleStoreInt: ["0", "4294967295"],
|
|
1499
|
+
// 53 bit, the JS safe-integer ceiling rather than the column's
|
|
1500
|
+
MySqlBigInt53: ["0", "9007199254740991"],
|
|
1501
|
+
SingleStoreBigInt53: ["0", "9007199254740991"],
|
|
1502
|
+
// 64 bit, representable because the value is a bigint
|
|
1503
|
+
MySqlBigInt64: ["0", "18446744073709551615"],
|
|
1504
|
+
SingleStoreBigInt64: ["0", "18446744073709551615"]
|
|
1139
1505
|
};
|
|
1140
1506
|
/**
|
|
1141
1507
|
* The numeric column classes that are not exact, and the magnitude each one can really hold.
|
|
@@ -1186,10 +1552,85 @@ _SchemaAnalyzer.INEXACT_RANGES = {
|
|
|
1186
1552
|
SQLiteReal: null,
|
|
1187
1553
|
SingleStoreDouble: null,
|
|
1188
1554
|
SingleStoreReal: null,
|
|
1189
|
-
// `
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1555
|
+
// Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
|
|
1556
|
+
// used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
|
|
1557
|
+
// else at all, so a `real` column accepted 1e300 and the server refused it.
|
|
1558
|
+
//
|
|
1559
|
+
// Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
|
|
1560
|
+
// through the `gel` client, casting each literal so the server parses it, and again through a
|
|
1561
|
+
// stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
|
|
1562
|
+
//
|
|
1563
|
+
// 3.4028234663852886e38 accepted, returned unchanged
|
|
1564
|
+
// 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
|
|
1565
|
+
// 3.402823567797337e38 refused, "is out of range for type std::float32"
|
|
1566
|
+
// 1e300 refused, the same way
|
|
1567
|
+
//
|
|
1568
|
+
// The same value accepted, the same next double up refused, and the same rounding down of the
|
|
1569
|
+
// midpoint, so it takes the constant already here rather than a second name for one number.
|
|
1570
|
+
// float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
|
|
1571
|
+
// truthful finite bound.
|
|
1572
|
+
GelReal: PG_FLOAT4_RANGE,
|
|
1573
|
+
GelDoublePrecision: null
|
|
1574
|
+
};
|
|
1575
|
+
// `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
|
|
1576
|
+
// Its bound is not a fixed magnitude per class but the precision each column declares for itself,
|
|
1577
|
+
// which no table keyed on a class name can hold; see `declaredDecimalRange`.
|
|
1578
|
+
/**
|
|
1579
|
+
* The number columns whose server has an answer about a non-finite double, and what it is.
|
|
1580
|
+
*
|
|
1581
|
+
* Three states rather than two, and the third is the reason this table has a `false` half at all.
|
|
1582
|
+
* A column present here with `true` stores the value and hands it back, so a schema refusing it
|
|
1583
|
+
* refuses rows the column returns. A column present with `false` is one the server was asked
|
|
1584
|
+
* about and refused, so a schema accepting it promises what the server will not take. A column
|
|
1585
|
+
* *absent* is one nobody has measured, and the generators leave whatever their library does alone
|
|
1586
|
+
* rather than guessing; `nonFiniteAccepted` and `nonFiniteRefused` in `@drzl/validation-core` are
|
|
1587
|
+
* the two readings of that.
|
|
1588
|
+
*
|
|
1589
|
+
* The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
|
|
1590
|
+
* fact stated on one path and not the other is a schema that changes when the user upgrades
|
|
1591
|
+
* drizzle, which the cross-major diff in `verify-packed.sh` fails on. Every class name here is the
|
|
1592
|
+
* same on both majors, read off real columns on 0.45.2 and on 1.0.0-rc.4, so this table also
|
|
1593
|
+
* answers for a v1 column and the two answers are identical rather than merely compatible.
|
|
1594
|
+
* non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
|
|
1595
|
+
*
|
|
1596
|
+
* Postgres and Gel store all three. Gel joined on a measurement of its own rather than on being
|
|
1597
|
+
* Postgres-backed: a live Gel 7.1 stored `nan`, `inf` and `-inf` in both `std::float32` and
|
|
1598
|
+
* `std::float64` and handed all three back, through a cast and again through a stored property.
|
|
1599
|
+
* Without them every row of such a column failed validation.
|
|
1600
|
+
*
|
|
1601
|
+
* MySQL and SingleStore refuse all three, and that used to be left unstated on the reasoning that
|
|
1602
|
+
* a column stating nothing costs nothing. It cost two libraries: `v.number()` and ArkType's
|
|
1603
|
+
* `number` take both infinities where `z.number()` and `Type.Number()` refuse them, so an
|
|
1604
|
+
* unbounded `double` or `real` accepted a value the server answers `ER_WARN_DATA_OUT_OF_RANGE`
|
|
1605
|
+
* for. Measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`, on the binary prepared path, which is
|
|
1606
|
+
* the one that puts the real IEEE double on the wire: `float`, `double` and `real` refuse
|
|
1607
|
+
* `Infinity`, `-Infinity` and `NaN` alike, while `double` and `real` store 1e300 and
|
|
1608
|
+
* 3.4028235e38 unchanged. SingleStore is MySQL wire-compatible and unmeasured, and takes MySQL's
|
|
1609
|
+
* answer here exactly as it already takes MySQL's float32 bound in `INEXACT_RANGES`.
|
|
1610
|
+
*
|
|
1611
|
+
* No SQLite class belongs here in either direction. A real SQLite 3.53.4 stores both infinities in
|
|
1612
|
+
* a `real` and hands them back, and silently turns `NaN` into NULL, so it is neither the Postgres
|
|
1613
|
+
* answer nor the MySQL one; it is filed on its own and a column needs both halves of it or none.
|
|
1614
|
+
*
|
|
1615
|
+
* The decimal families are absent too. `PgNumeric` is a string whose pattern already accepts `NaN`
|
|
1616
|
+
* and `Infinity`. `PgNumericNumber` is a per-column question this table cannot ask: it takes `NaN`
|
|
1617
|
+
* at any width and an infinity only where no precision is declared, and `columnConstraints`
|
|
1618
|
+
* answers it beside the bound that decides it. MySQL's `decimal` is absent because the two client
|
|
1619
|
+
* paths disagree: on the binary prepared path MySQL 8.4.11 silently stored `0.00` for all three,
|
|
1620
|
+
* where the text path answers `Incorrect decimal value`, and "refuses" is only half true of a
|
|
1621
|
+
* column that accepted the row.
|
|
1622
|
+
*/
|
|
1623
|
+
_SchemaAnalyzer.NON_FINITE_BY_CLASS = {
|
|
1624
|
+
PgReal: { nan: true, infinity: true },
|
|
1625
|
+
PgDoublePrecision: { nan: true, infinity: true },
|
|
1626
|
+
GelReal: { nan: true, infinity: true },
|
|
1627
|
+
GelDoublePrecision: { nan: true, infinity: true },
|
|
1628
|
+
MySqlFloat: { nan: false, infinity: false },
|
|
1629
|
+
MySqlDouble: { nan: false, infinity: false },
|
|
1630
|
+
MySqlReal: { nan: false, infinity: false },
|
|
1631
|
+
SingleStoreFloat: { nan: false, infinity: false },
|
|
1632
|
+
SingleStoreDouble: { nan: false, infinity: false },
|
|
1633
|
+
SingleStoreReal: { nan: false, infinity: false }
|
|
1193
1634
|
};
|
|
1194
1635
|
var SchemaAnalyzer = _SchemaAnalyzer;
|
|
1195
1636
|
var index_default = SchemaAnalyzer;
|
|
@@ -1200,5 +1641,7 @@ var index_default = SchemaAnalyzer;
|
|
|
1200
1641
|
isDrizzleView,
|
|
1201
1642
|
isReadOnlyRelation,
|
|
1202
1643
|
isRelationsV2,
|
|
1644
|
+
qualifiedForeignTable,
|
|
1645
|
+
qualifiedTableName,
|
|
1203
1646
|
readRelationsV2
|
|
1204
1647
|
});
|