@drzl/analyzer 1.20.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 CHANGED
@@ -48,10 +48,22 @@ The CLI consumes this analysis to generate validation, services, and routers.
48
48
  - issues (warnings/errors) for constraints and shape
49
49
 
50
50
  Per column, beyond the type: `nullable`, `hasDefault`, `defaultValue` (literal defaults only),
51
- `isGenerated`, `enumValues`, `maxLength` (characters), `maxBytes` (MySQL's TEXT family is a byte
52
- budget, which is a different measurement on the same kind of column), `min`/`max`,
53
- `arrayDimensions`, `format`, and `shape` for values that are not scalars (json, buffer, tuple,
54
- numberObject, vector, bitstring, customType).
51
+ `isGenerated`, `enumValues`, `maxLength` (characters), `maxBytes` (bytes, which is a different
52
+ measurement on the same kind of column), `min`/`max`, `arrayDimensions`, `format`, and `shape` for
53
+ values that are not scalars (json, buffer, tuple, numberObject, vector, bitstring, customType).
54
+
55
+ `maxBytes` is set by MySQL and nothing else. Its TEXT and BLOB families carry their limit in the
56
+ type rather than in a declared length, and count it in bytes: 255 for `tinytext` and `tinyblob`,
57
+ 65535 for `text` and `blob`, 16777215 for the medium pair and 4294967295 for the long pair. A
58
+ `varchar(n)` is genuinely n characters and keeps `maxLength`. See
59
+ [the analyzer page](https://use-drzl.github.io/drzl/packages/analyzer) for how the generators
60
+ encode it.
61
+
62
+ `format` names a pattern in `COLUMN_FORMATS`, and two of its values name a dialect: a
63
+ `bigint({ mode: 'string' })` column is `pgBigint` on Postgres and `mysqlBigint` on MySQL and
64
+ SingleStore, because the two servers really do parse that text differently (`'0x1f'` is 31 on one
65
+ and an error on the other, `'12.5'` is 13 on one and an error on the other). SQL Server carries no
66
+ format for it, since none was measured.
55
67
 
56
68
  `DRZL_ANL_UNKNOWN_COLUMN` is reported for any column whose validator would accept anything, which
57
69
  is the shape a missing type mapping takes: nothing throws, and every row passes.
package/dist/index.cjs CHANGED
@@ -116,11 +116,15 @@ function describeV1Column(column) {
116
116
  case "int8":
117
117
  case "uint8":
118
118
  case "int16":
119
+ case "uint16":
119
120
  case "int24":
121
+ case "uint24":
120
122
  case "int32":
123
+ case "uint32":
121
124
  case "int53":
122
125
  case "uint53":
123
- case "int64": {
126
+ case "int64":
127
+ case "uint64": {
124
128
  if (DECIMAL_BIGINT_MODE.test(entityKind)) {
125
129
  out.tsType = "bigint";
126
130
  out.dbType = "NUMERIC";
@@ -129,22 +133,44 @@ function describeV1Column(column) {
129
133
  if (range2) [out.min, out.max] = range2;
130
134
  break;
131
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
+ }
132
144
  const range = {
133
145
  int8: ["-128", "127"],
134
146
  uint8: ["0", "255"],
135
147
  int16: ["-32768", "32767"],
148
+ uint16: ["0", "65535"],
136
149
  int24: ["-8388608", "8388607"],
150
+ uint24: ["0", "16777215"],
137
151
  int32: ["-2147483648", "2147483647"],
152
+ uint32: ["0", "4294967295"],
138
153
  int53: ["-9007199254740991", "9007199254740991"],
139
154
  // MySQL `serial` is `bigint unsigned auto_increment`, so it starts at 0 rather than
140
- // 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.
141
157
  uint53: ["0", "9007199254740991"],
142
- int64: ["-9223372036854775808", "9223372036854775807"]
158
+ int64: ["-9223372036854775808", "9223372036854775807"],
159
+ uint64: ["0", "18446744073709551615"]
143
160
  }[semantic];
144
161
  [out.min, out.max] = range;
145
162
  out.integer = true;
146
163
  out.tsType = js === "bigint" ? "bigint" : "number";
147
- out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
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";
148
174
  break;
149
175
  }
150
176
  case "year":
@@ -164,6 +190,10 @@ function describeV1Column(column) {
164
190
  out.allowsNaN = true;
165
191
  out.allowsInfinity = true;
166
192
  }
193
+ if (codec === "float" || codec === "double" || codec === "real" || entityKind.startsWith("SingleStore")) {
194
+ out.allowsNaN = false;
195
+ out.allowsInfinity = false;
196
+ }
167
197
  break;
168
198
  }
169
199
  case "uuid":
@@ -431,6 +461,13 @@ function unknownColumnHint(reason) {
431
461
  return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
432
462
  }
433
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
+ */
434
471
  constructor(schemaPath) {
435
472
  this.schemaPath = schemaPath;
436
473
  }
@@ -652,7 +689,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
652
689
  if (typeof length === "number" && Number.isFinite(length) && length > 0) {
653
690
  out.maxLength = length;
654
691
  }
655
- const range = _SchemaAnalyzer.INT_RANGES[ctor];
692
+ const unsignedRange = column?.config?.unsigned === true ? _SchemaAnalyzer.UNSIGNED_INT_RANGES[ctor] : void 0;
693
+ const range = unsignedRange ?? _SchemaAnalyzer.INT_RANGES[ctor];
656
694
  if (range) {
657
695
  [out.min, out.max] = range;
658
696
  out.integer = true;
@@ -662,7 +700,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
662
700
  if (inexact) [out.min, out.max] = inexact;
663
701
  out.integer = false;
664
702
  }
665
- const nonFinite = _SchemaAnalyzer.PG_NON_FINITE[ctor];
703
+ const nonFinite = _SchemaAnalyzer.NON_FINITE_BY_CLASS[ctor];
666
704
  if (nonFinite) {
667
705
  out.allowsNaN = nonFinite.nan;
668
706
  out.allowsInfinity = nonFinite.infinity;
@@ -907,7 +945,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
907
945
  }
908
946
  if (/Numeric|Float|Double|Real/i.test(ctor))
909
947
  return { tsType: "number", dbType: "NUMERIC" };
910
- if (/Int|Serial|TinyInt|SmallInt|MediumInt/i.test(ctor))
948
+ if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
949
+ if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
911
950
  return { tsType: "number", dbType: "INTEGER" };
912
951
  if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
913
952
  if (/TimestampString|DateTimeString|DateString/i.test(ctor))
@@ -931,7 +970,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
931
970
  }
932
971
  if (/Numeric|Float|Double|Real/i.test(ctor))
933
972
  return { tsType: "number", dbType: "NUMERIC" };
934
- if (/Int|Serial|TinyInt|SmallInt|MediumInt/i.test(ctor))
973
+ if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
974
+ if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
935
975
  return { tsType: "number", dbType: "INTEGER" };
936
976
  if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
937
977
  if (/TimestampString|DateTimeString|DateString/i.test(ctor))
@@ -1142,31 +1182,85 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1142
1182
  const fs = await import("fs/promises");
1143
1183
  const path = await import("path");
1144
1184
  const issues = [];
1145
- const full = path.resolve(process.cwd(), this.schemaPath);
1146
- try {
1147
- await fs.access(full);
1148
- } catch (_e) {
1149
- issues.push({
1150
- code: "DRZL_ANL_NOFILE",
1151
- level: "error",
1152
- message: `Schema file not found: ${this.schemaPath}`
1153
- });
1154
- return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
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
+ }
1155
1200
  }
1156
- let mod;
1157
- try {
1158
- const { default: jiti } = await import("jiti");
1159
- const jit = jiti(import_meta.url, { moduleCache: false });
1160
- mod = jit(full);
1161
- } catch (e) {
1162
- issues.push({
1163
- code: "DRZL_ANL_IMPORT",
1164
- level: "error",
1165
- message: `Failed to import schema: ${String(e)}`
1166
- });
1201
+ if (missing) {
1167
1202
  return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
1168
1203
  }
1169
- const exportsObj = mod?.default && typeof mod.default === "object" ? mod.default : mod;
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
+ }
1170
1264
  const tables = [];
1171
1265
  const relations = [];
1172
1266
  const enums = [];
@@ -1319,6 +1413,12 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1319
1413
  _SchemaAnalyzer.INT_RANGES = {
1320
1414
  // 8 bit
1321
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"],
1322
1422
  SQLiteInteger: ["-9223372036854775808", "9223372036854775807"],
1323
1423
  // 16 bit
1324
1424
  PgSmallInt: ["-32768", "32767"],
@@ -1331,6 +1431,8 @@ _SchemaAnalyzer.INT_RANGES = {
1331
1431
  SingleStoreSmallInt: ["-32768", "32767"],
1332
1432
  // 24 bit
1333
1433
  MySqlMediumInt: ["-8388608", "8388607"],
1434
+ // As SingleStoreTinyInt above: v1 states `number int24` and this table said nothing.
1435
+ SingleStoreMediumInt: ["-8388608", "8388607"],
1334
1436
  // 32 bit
1335
1437
  PgInteger: ["-2147483648", "2147483647"],
1336
1438
  PgSerial: ["-2147483648", "2147483647"],
@@ -1345,7 +1447,61 @@ _SchemaAnalyzer.INT_RANGES = {
1345
1447
  PgBigInt64: ["-9223372036854775808", "9223372036854775807"],
1346
1448
  PgBigSerial64: ["-9223372036854775808", "9223372036854775807"],
1347
1449
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
1348
- 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"]
1349
1505
  };
1350
1506
  /**
1351
1507
  * The numeric column classes that are not exact, and the magnitude each one can really hold.
@@ -1420,34 +1576,61 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1420
1576
  // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1421
1577
  // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1422
1578
  /**
1423
- * The Postgres number columns that hold a non-finite double, and which of the three each holds.
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.
1424
1588
  *
1425
1589
  * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
1426
1590
  * 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.
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.
1431
1600
  *
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.
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`.
1435
1610
  *
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.
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.
1440
1614
  *
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.
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.
1445
1622
  */
1446
- _SchemaAnalyzer.PG_NON_FINITE = {
1623
+ _SchemaAnalyzer.NON_FINITE_BY_CLASS = {
1447
1624
  PgReal: { nan: true, infinity: true },
1448
1625
  PgDoublePrecision: { nan: true, infinity: true },
1449
1626
  GelReal: { nan: true, infinity: true },
1450
- GelDoublePrecision: { 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 }
1451
1634
  };
1452
1635
  var SchemaAnalyzer = _SchemaAnalyzer;
1453
1636
  var index_default = SchemaAnalyzer;
package/dist/index.d.cts CHANGED
@@ -59,9 +59,11 @@ interface Column {
59
59
  /**
60
60
  * A coarse label for the column's kind, not its type.
61
61
  *
62
- * `varchar`, `char` and `text` are all `TEXT` here, deliberately: exactly one consumer reads
63
- * this, `isIntegerColumn`, and it only asks whether a number is whole. Use `sqlType` for the
64
- * question this name suggests it answers.
62
+ * `varchar`, `char` and `text` are all `TEXT` here, deliberately: two consumers read this and
63
+ * both ask coarse questions. `isIntegerColumn` asks whether a number is whole, and
64
+ * `comparisonWire` in `@drzl/validation-core` asks whether a string wire carries decimal text
65
+ * the database compares numerically, which is `NUMERIC` (every decimal family) and `BIGINT`
66
+ * (the v1 string mode). Use `sqlType` for the question this name suggests it answers.
65
67
  */
66
68
  dbType: string;
67
69
  /**
@@ -172,13 +174,18 @@ interface Column {
172
174
  /**
173
175
  * A string column whose contents have a shape the database enforces.
174
176
  *
175
- * Only formats checked against Postgres itself appear here, and the list is short because most
177
+ * Only formats checked against a real server appear here, and the list is short because most
176
178
  * candidates failed: Postgres reads `'today'` and `'January 8, 1999'` as dates, pads
177
179
  * `'2020-01-01'` into a macaddr, and accepts `'10.1/16'` as an inet. A check for any of those
178
180
  * would reject input the database accepts, and turning away valid data is worse than not
179
181
  * checking at all. See `COLUMN_FORMATS` in `@drzl/validation-core`.
182
+ *
183
+ * Two of the keys name a dialect, because the same column has two different answers: a
184
+ * `bigint({ mode: 'string' })` is parsed by Postgres as an integer literal, `'0x1f'` and
185
+ * `'1_000'` included, and by MySQL as a decimal number it then rounds, so `'12.5'` is a row on
186
+ * one server and an error on the other.
180
187
  */
181
- format?: 'uuid' | 'numeric';
188
+ format?: 'uuid' | 'numeric' | 'pgBigint' | 'mysqlBigint';
182
189
  /**
183
190
  * The column's default, when it is a literal a schema can reproduce.
184
191
  *
@@ -474,7 +481,14 @@ declare function isRelationsV2(val: any): boolean;
474
481
  declare function readRelationsV2(val: any, issues?: Issue[]): Relation[];
475
482
  declare class SchemaAnalyzer {
476
483
  private readonly schemaPath;
477
- constructor(schemaPath: string);
484
+ /**
485
+ * One path or several. The plural exists for drizzle-kit interop: kit's `schema` key names
486
+ * files in the plural (arrays, globs), and the commonest multi-file layout is a directory of
487
+ * one file per table with no barrel, so there is no single module to point at. Entries are
488
+ * concrete files, never globs; expansion is the caller's job, so this class's contract stays
489
+ * "load exactly these modules and read their exports as one schema".
490
+ */
491
+ constructor(schemaPath: string | readonly string[]);
478
492
  private getSymbol;
479
493
  /**
480
494
  * Drizzle keys the Columns object by TypeScript property name, but every other piece of
@@ -564,6 +578,31 @@ declare class SchemaAnalyzer {
564
578
  * promise a precision that cannot survive the round trip.
565
579
  */
566
580
  private static readonly INT_RANGES;
581
+ /**
582
+ * The same widths with `{ unsigned: true }` set, which is the half the table above cannot see.
583
+ *
584
+ * On 0.4x the flag moves no class name: `int('x', { unsigned: true })` still builds a
585
+ * `MySqlInt`, and only `config.unsigned` and the ` unsigned` suffix on `getSQLType()` record
586
+ * the difference, measured off real 0.45.2 columns. So the table above answered every unsigned
587
+ * width with its signed range, and the emitted select schema refused every stored value in the
588
+ * upper half of the column: an `int unsigned` holding 4294967295 failed validation on a row the
589
+ * database returned, and the same one width up meant `bigint unsigned` refused
590
+ * 18446744073709551615n.
591
+ *
592
+ * The ceilings are the type's, verified against a live MySQL 8.4.11: 255, 65535, 16777215 and
593
+ * 4294967295 store and return, -1 and each ceiling plus one are refused with
594
+ * ER_WARN_DATA_OUT_OF_RANGE. The bigint pair keeps the two modes apart for the reason the
595
+ * signed pair above does: number mode tops out at the safe-integer bound the wire imposes,
596
+ * bigint mode at the column's own 2^64-1, which a bigint can spell. SingleStore is MySQL wire
597
+ * compatible, ships the same builders with the same `config.unsigned`, and v1 states the same
598
+ * `uintN` semantics for it, measured off real rc.4 columns; the entries keep the majors in
599
+ * agreement, which is what the cross-major diff in `scripts/verify-packed.sh` holds together.
600
+ *
601
+ * Keyed by class exactly like `INT_RANGES`, and consulted only when `config.unsigned` is
602
+ * `true`, so no Postgres or SQLite column can ever reach it: neither dialect has an unsigned
603
+ * spelling, neither builder accepts the flag, and no class of theirs is named here.
604
+ */
605
+ private static readonly UNSIGNED_INT_RANGES;
567
606
  /**
568
607
  * The numeric column classes that are not exact, and the magnitude each one can really hold.
569
608
  *
@@ -601,30 +640,51 @@ declare class SchemaAnalyzer {
601
640
  */
602
641
  private static readonly INEXACT_RANGES;
603
642
  /**
604
- * The Postgres number columns that hold a non-finite double, and which of the three each holds.
643
+ * The number columns whose server has an answer about a non-finite double, and what it is.
644
+ *
645
+ * Three states rather than two, and the third is the reason this table has a `false` half at all.
646
+ * A column present here with `true` stores the value and hands it back, so a schema refusing it
647
+ * refuses rows the column returns. A column present with `false` is one the server was asked
648
+ * about and refused, so a schema accepting it promises what the server will not take. A column
649
+ * *absent* is one nobody has measured, and the generators leave whatever their library does alone
650
+ * rather than guessing; `nonFiniteAccepted` and `nonFiniteRefused` in `@drzl/validation-core` are
651
+ * the two readings of that.
605
652
  *
606
653
  * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
607
654
  * fact stated on one path and not the other is a schema that changes when the user upgrades
608
- * drizzle, which the cross-major diff in `verify-packed.sh` fails on. These three class names are
609
- * the same on both majors, read off real `pgTable` columns on 0.45.2 and on 1.0.0-rc.4, so this
610
- * table also answers for a v1 column and the two answers are identical rather than merely
611
- * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
612
- *
613
- * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
614
- * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
615
- * turning `NaN` into NULL, which is a different answer that has to arrive whole.
616
- *
617
- * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
618
- * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
619
- * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
620
- * and again through a stored property. Without them every row of such a column failed validation.
621
- *
622
- * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
623
- * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
624
- * any width and an infinity only where no precision is declared, which is a per-column question
625
- * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
655
+ * drizzle, which the cross-major diff in `verify-packed.sh` fails on. Every class name here is the
656
+ * same on both majors, read off real columns on 0.45.2 and on 1.0.0-rc.4, so this table also
657
+ * answers for a v1 column and the two answers are identical rather than merely compatible.
658
+ * non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
659
+ *
660
+ * Postgres and Gel store all three. Gel joined on a measurement of its own rather than on being
661
+ * Postgres-backed: a live Gel 7.1 stored `nan`, `inf` and `-inf` in both `std::float32` and
662
+ * `std::float64` and handed all three back, through a cast and again through a stored property.
663
+ * Without them every row of such a column failed validation.
664
+ *
665
+ * MySQL and SingleStore refuse all three, and that used to be left unstated on the reasoning that
666
+ * a column stating nothing costs nothing. It cost two libraries: `v.number()` and ArkType's
667
+ * `number` take both infinities where `z.number()` and `Type.Number()` refuse them, so an
668
+ * unbounded `double` or `real` accepted a value the server answers `ER_WARN_DATA_OUT_OF_RANGE`
669
+ * for. Measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`, on the binary prepared path, which is
670
+ * the one that puts the real IEEE double on the wire: `float`, `double` and `real` refuse
671
+ * `Infinity`, `-Infinity` and `NaN` alike, while `double` and `real` store 1e300 and
672
+ * 3.4028235e38 unchanged. SingleStore is MySQL wire-compatible and unmeasured, and takes MySQL's
673
+ * answer here exactly as it already takes MySQL's float32 bound in `INEXACT_RANGES`.
674
+ *
675
+ * No SQLite class belongs here in either direction. A real SQLite 3.53.4 stores both infinities in
676
+ * a `real` and hands them back, and silently turns `NaN` into NULL, so it is neither the Postgres
677
+ * answer nor the MySQL one; it is filed on its own and a column needs both halves of it or none.
678
+ *
679
+ * The decimal families are absent too. `PgNumeric` is a string whose pattern already accepts `NaN`
680
+ * and `Infinity`. `PgNumericNumber` is a per-column question this table cannot ask: it takes `NaN`
681
+ * at any width and an infinity only where no precision is declared, and `columnConstraints`
682
+ * answers it beside the bound that decides it. MySQL's `decimal` is absent because the two client
683
+ * paths disagree: on the binary prepared path MySQL 8.4.11 silently stored `0.00` for all three,
684
+ * where the text path answers `Incorrect decimal value`, and "refuses" is only half true of a
685
+ * column that accepted the row.
626
686
  */
627
- private static readonly PG_NON_FINITE;
687
+ private static readonly NON_FINITE_BY_CLASS;
628
688
  /**
629
689
  * Constraints the column definition already carries, which the analysis used to throw away.
630
690
  *
package/dist/index.d.ts CHANGED
@@ -59,9 +59,11 @@ interface Column {
59
59
  /**
60
60
  * A coarse label for the column's kind, not its type.
61
61
  *
62
- * `varchar`, `char` and `text` are all `TEXT` here, deliberately: exactly one consumer reads
63
- * this, `isIntegerColumn`, and it only asks whether a number is whole. Use `sqlType` for the
64
- * question this name suggests it answers.
62
+ * `varchar`, `char` and `text` are all `TEXT` here, deliberately: two consumers read this and
63
+ * both ask coarse questions. `isIntegerColumn` asks whether a number is whole, and
64
+ * `comparisonWire` in `@drzl/validation-core` asks whether a string wire carries decimal text
65
+ * the database compares numerically, which is `NUMERIC` (every decimal family) and `BIGINT`
66
+ * (the v1 string mode). Use `sqlType` for the question this name suggests it answers.
65
67
  */
66
68
  dbType: string;
67
69
  /**
@@ -172,13 +174,18 @@ interface Column {
172
174
  /**
173
175
  * A string column whose contents have a shape the database enforces.
174
176
  *
175
- * Only formats checked against Postgres itself appear here, and the list is short because most
177
+ * Only formats checked against a real server appear here, and the list is short because most
176
178
  * candidates failed: Postgres reads `'today'` and `'January 8, 1999'` as dates, pads
177
179
  * `'2020-01-01'` into a macaddr, and accepts `'10.1/16'` as an inet. A check for any of those
178
180
  * would reject input the database accepts, and turning away valid data is worse than not
179
181
  * checking at all. See `COLUMN_FORMATS` in `@drzl/validation-core`.
182
+ *
183
+ * Two of the keys name a dialect, because the same column has two different answers: a
184
+ * `bigint({ mode: 'string' })` is parsed by Postgres as an integer literal, `'0x1f'` and
185
+ * `'1_000'` included, and by MySQL as a decimal number it then rounds, so `'12.5'` is a row on
186
+ * one server and an error on the other.
180
187
  */
181
- format?: 'uuid' | 'numeric';
188
+ format?: 'uuid' | 'numeric' | 'pgBigint' | 'mysqlBigint';
182
189
  /**
183
190
  * The column's default, when it is a literal a schema can reproduce.
184
191
  *
@@ -474,7 +481,14 @@ declare function isRelationsV2(val: any): boolean;
474
481
  declare function readRelationsV2(val: any, issues?: Issue[]): Relation[];
475
482
  declare class SchemaAnalyzer {
476
483
  private readonly schemaPath;
477
- constructor(schemaPath: string);
484
+ /**
485
+ * One path or several. The plural exists for drizzle-kit interop: kit's `schema` key names
486
+ * files in the plural (arrays, globs), and the commonest multi-file layout is a directory of
487
+ * one file per table with no barrel, so there is no single module to point at. Entries are
488
+ * concrete files, never globs; expansion is the caller's job, so this class's contract stays
489
+ * "load exactly these modules and read their exports as one schema".
490
+ */
491
+ constructor(schemaPath: string | readonly string[]);
478
492
  private getSymbol;
479
493
  /**
480
494
  * Drizzle keys the Columns object by TypeScript property name, but every other piece of
@@ -564,6 +578,31 @@ declare class SchemaAnalyzer {
564
578
  * promise a precision that cannot survive the round trip.
565
579
  */
566
580
  private static readonly INT_RANGES;
581
+ /**
582
+ * The same widths with `{ unsigned: true }` set, which is the half the table above cannot see.
583
+ *
584
+ * On 0.4x the flag moves no class name: `int('x', { unsigned: true })` still builds a
585
+ * `MySqlInt`, and only `config.unsigned` and the ` unsigned` suffix on `getSQLType()` record
586
+ * the difference, measured off real 0.45.2 columns. So the table above answered every unsigned
587
+ * width with its signed range, and the emitted select schema refused every stored value in the
588
+ * upper half of the column: an `int unsigned` holding 4294967295 failed validation on a row the
589
+ * database returned, and the same one width up meant `bigint unsigned` refused
590
+ * 18446744073709551615n.
591
+ *
592
+ * The ceilings are the type's, verified against a live MySQL 8.4.11: 255, 65535, 16777215 and
593
+ * 4294967295 store and return, -1 and each ceiling plus one are refused with
594
+ * ER_WARN_DATA_OUT_OF_RANGE. The bigint pair keeps the two modes apart for the reason the
595
+ * signed pair above does: number mode tops out at the safe-integer bound the wire imposes,
596
+ * bigint mode at the column's own 2^64-1, which a bigint can spell. SingleStore is MySQL wire
597
+ * compatible, ships the same builders with the same `config.unsigned`, and v1 states the same
598
+ * `uintN` semantics for it, measured off real rc.4 columns; the entries keep the majors in
599
+ * agreement, which is what the cross-major diff in `scripts/verify-packed.sh` holds together.
600
+ *
601
+ * Keyed by class exactly like `INT_RANGES`, and consulted only when `config.unsigned` is
602
+ * `true`, so no Postgres or SQLite column can ever reach it: neither dialect has an unsigned
603
+ * spelling, neither builder accepts the flag, and no class of theirs is named here.
604
+ */
605
+ private static readonly UNSIGNED_INT_RANGES;
567
606
  /**
568
607
  * The numeric column classes that are not exact, and the magnitude each one can really hold.
569
608
  *
@@ -601,30 +640,51 @@ declare class SchemaAnalyzer {
601
640
  */
602
641
  private static readonly INEXACT_RANGES;
603
642
  /**
604
- * The Postgres number columns that hold a non-finite double, and which of the three each holds.
643
+ * The number columns whose server has an answer about a non-finite double, and what it is.
644
+ *
645
+ * Three states rather than two, and the third is the reason this table has a `false` half at all.
646
+ * A column present here with `true` stores the value and hands it back, so a schema refusing it
647
+ * refuses rows the column returns. A column present with `false` is one the server was asked
648
+ * about and refused, so a schema accepting it promises what the server will not take. A column
649
+ * *absent* is one nobody has measured, and the generators leave whatever their library does alone
650
+ * rather than guessing; `nonFiniteAccepted` and `nonFiniteRefused` in `@drzl/validation-core` are
651
+ * the two readings of that.
605
652
  *
606
653
  * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
607
654
  * fact stated on one path and not the other is a schema that changes when the user upgrades
608
- * drizzle, which the cross-major diff in `verify-packed.sh` fails on. These three class names are
609
- * the same on both majors, read off real `pgTable` columns on 0.45.2 and on 1.0.0-rc.4, so this
610
- * table also answers for a v1 column and the two answers are identical rather than merely
611
- * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
612
- *
613
- * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
614
- * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
615
- * turning `NaN` into NULL, which is a different answer that has to arrive whole.
616
- *
617
- * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
618
- * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
619
- * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
620
- * and again through a stored property. Without them every row of such a column failed validation.
621
- *
622
- * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
623
- * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
624
- * any width and an infinity only where no precision is declared, which is a per-column question
625
- * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
655
+ * drizzle, which the cross-major diff in `verify-packed.sh` fails on. Every class name here is the
656
+ * same on both majors, read off real columns on 0.45.2 and on 1.0.0-rc.4, so this table also
657
+ * answers for a v1 column and the two answers are identical rather than merely compatible.
658
+ * non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
659
+ *
660
+ * Postgres and Gel store all three. Gel joined on a measurement of its own rather than on being
661
+ * Postgres-backed: a live Gel 7.1 stored `nan`, `inf` and `-inf` in both `std::float32` and
662
+ * `std::float64` and handed all three back, through a cast and again through a stored property.
663
+ * Without them every row of such a column failed validation.
664
+ *
665
+ * MySQL and SingleStore refuse all three, and that used to be left unstated on the reasoning that
666
+ * a column stating nothing costs nothing. It cost two libraries: `v.number()` and ArkType's
667
+ * `number` take both infinities where `z.number()` and `Type.Number()` refuse them, so an
668
+ * unbounded `double` or `real` accepted a value the server answers `ER_WARN_DATA_OUT_OF_RANGE`
669
+ * for. Measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`, on the binary prepared path, which is
670
+ * the one that puts the real IEEE double on the wire: `float`, `double` and `real` refuse
671
+ * `Infinity`, `-Infinity` and `NaN` alike, while `double` and `real` store 1e300 and
672
+ * 3.4028235e38 unchanged. SingleStore is MySQL wire-compatible and unmeasured, and takes MySQL's
673
+ * answer here exactly as it already takes MySQL's float32 bound in `INEXACT_RANGES`.
674
+ *
675
+ * No SQLite class belongs here in either direction. A real SQLite 3.53.4 stores both infinities in
676
+ * a `real` and hands them back, and silently turns `NaN` into NULL, so it is neither the Postgres
677
+ * answer nor the MySQL one; it is filed on its own and a column needs both halves of it or none.
678
+ *
679
+ * The decimal families are absent too. `PgNumeric` is a string whose pattern already accepts `NaN`
680
+ * and `Infinity`. `PgNumericNumber` is a per-column question this table cannot ask: it takes `NaN`
681
+ * at any width and an infinity only where no precision is declared, and `columnConstraints`
682
+ * answers it beside the bound that decides it. MySQL's `decimal` is absent because the two client
683
+ * paths disagree: on the binary prepared path MySQL 8.4.11 silently stored `0.00` for all three,
684
+ * where the text path answers `Incorrect decimal value`, and "refuses" is only half true of a
685
+ * column that accepted the row.
626
686
  */
627
- private static readonly PG_NON_FINITE;
687
+ private static readonly NON_FINITE_BY_CLASS;
628
688
  /**
629
689
  * Constraints the column definition already carries, which the analysis used to throw away.
630
690
  *
package/dist/index.js CHANGED
@@ -73,11 +73,15 @@ function describeV1Column(column) {
73
73
  case "int8":
74
74
  case "uint8":
75
75
  case "int16":
76
+ case "uint16":
76
77
  case "int24":
78
+ case "uint24":
77
79
  case "int32":
80
+ case "uint32":
78
81
  case "int53":
79
82
  case "uint53":
80
- case "int64": {
83
+ case "int64":
84
+ case "uint64": {
81
85
  if (DECIMAL_BIGINT_MODE.test(entityKind)) {
82
86
  out.tsType = "bigint";
83
87
  out.dbType = "NUMERIC";
@@ -86,22 +90,44 @@ function describeV1Column(column) {
86
90
  if (range2) [out.min, out.max] = range2;
87
91
  break;
88
92
  }
93
+ if ((semantic === "int64" || semantic === "uint64") && js === "string") {
94
+ out.tsType = "string";
95
+ out.dbType = "BIGINT";
96
+ if (entityKind.startsWith("Pg")) out.format = "pgBigint";
97
+ else if (entityKind.startsWith("MySql") || entityKind.startsWith("SingleStore"))
98
+ out.format = "mysqlBigint";
99
+ break;
100
+ }
89
101
  const range = {
90
102
  int8: ["-128", "127"],
91
103
  uint8: ["0", "255"],
92
104
  int16: ["-32768", "32767"],
105
+ uint16: ["0", "65535"],
93
106
  int24: ["-8388608", "8388607"],
107
+ uint24: ["0", "16777215"],
94
108
  int32: ["-2147483648", "2147483647"],
109
+ uint32: ["0", "4294967295"],
95
110
  int53: ["-9007199254740991", "9007199254740991"],
96
111
  // MySQL `serial` is `bigint unsigned auto_increment`, so it starts at 0 rather than
97
- // spanning the signed range.
112
+ // spanning the signed range. An explicit `bigint({ mode: 'number', unsigned: true })`
113
+ // states the same semantic and takes the same answer.
98
114
  uint53: ["0", "9007199254740991"],
99
- int64: ["-9223372036854775808", "9223372036854775807"]
115
+ int64: ["-9223372036854775808", "9223372036854775807"],
116
+ uint64: ["0", "18446744073709551615"]
100
117
  }[semantic];
101
118
  [out.min, out.max] = range;
102
119
  out.integer = true;
103
120
  out.tsType = js === "bigint" ? "bigint" : "number";
104
- out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
121
+ out.dbType = {
122
+ int8: "TINYINT",
123
+ uint8: "TINYINT",
124
+ int16: "SMALLINT",
125
+ uint16: "SMALLINT",
126
+ int24: "MEDIUMINT",
127
+ uint24: "MEDIUMINT",
128
+ int32: "INTEGER",
129
+ uint32: "INTEGER"
130
+ }[semantic] ?? "BIGINT";
105
131
  break;
106
132
  }
107
133
  case "year":
@@ -121,6 +147,10 @@ function describeV1Column(column) {
121
147
  out.allowsNaN = true;
122
148
  out.allowsInfinity = true;
123
149
  }
150
+ if (codec === "float" || codec === "double" || codec === "real" || entityKind.startsWith("SingleStore")) {
151
+ out.allowsNaN = false;
152
+ out.allowsInfinity = false;
153
+ }
124
154
  break;
125
155
  }
126
156
  case "uuid":
@@ -388,6 +418,13 @@ function unknownColumnHint(reason) {
388
418
  return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
389
419
  }
390
420
  var _SchemaAnalyzer = class _SchemaAnalyzer {
421
+ /**
422
+ * One path or several. The plural exists for drizzle-kit interop: kit's `schema` key names
423
+ * files in the plural (arrays, globs), and the commonest multi-file layout is a directory of
424
+ * one file per table with no barrel, so there is no single module to point at. Entries are
425
+ * concrete files, never globs; expansion is the caller's job, so this class's contract stays
426
+ * "load exactly these modules and read their exports as one schema".
427
+ */
391
428
  constructor(schemaPath) {
392
429
  this.schemaPath = schemaPath;
393
430
  }
@@ -609,7 +646,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
609
646
  if (typeof length === "number" && Number.isFinite(length) && length > 0) {
610
647
  out.maxLength = length;
611
648
  }
612
- const range = _SchemaAnalyzer.INT_RANGES[ctor];
649
+ const unsignedRange = column?.config?.unsigned === true ? _SchemaAnalyzer.UNSIGNED_INT_RANGES[ctor] : void 0;
650
+ const range = unsignedRange ?? _SchemaAnalyzer.INT_RANGES[ctor];
613
651
  if (range) {
614
652
  [out.min, out.max] = range;
615
653
  out.integer = true;
@@ -619,7 +657,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
619
657
  if (inexact) [out.min, out.max] = inexact;
620
658
  out.integer = false;
621
659
  }
622
- const nonFinite = _SchemaAnalyzer.PG_NON_FINITE[ctor];
660
+ const nonFinite = _SchemaAnalyzer.NON_FINITE_BY_CLASS[ctor];
623
661
  if (nonFinite) {
624
662
  out.allowsNaN = nonFinite.nan;
625
663
  out.allowsInfinity = nonFinite.infinity;
@@ -864,7 +902,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
864
902
  }
865
903
  if (/Numeric|Float|Double|Real/i.test(ctor))
866
904
  return { tsType: "number", dbType: "NUMERIC" };
867
- if (/Int|Serial|TinyInt|SmallInt|MediumInt/i.test(ctor))
905
+ if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
906
+ if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
868
907
  return { tsType: "number", dbType: "INTEGER" };
869
908
  if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
870
909
  if (/TimestampString|DateTimeString|DateString/i.test(ctor))
@@ -888,7 +927,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
888
927
  }
889
928
  if (/Numeric|Float|Double|Real/i.test(ctor))
890
929
  return { tsType: "number", dbType: "NUMERIC" };
891
- if (/Int|Serial|TinyInt|SmallInt|MediumInt/i.test(ctor))
930
+ if (/Serial/i.test(ctor)) return { tsType: "number", dbType: "BIGINT" };
931
+ if (/Int|TinyInt|SmallInt|MediumInt/i.test(ctor))
892
932
  return { tsType: "number", dbType: "INTEGER" };
893
933
  if (/Bool|Boolean/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
894
934
  if (/TimestampString|DateTimeString|DateString/i.test(ctor))
@@ -1099,31 +1139,85 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1099
1139
  const fs = await import("fs/promises");
1100
1140
  const path = await import("path");
1101
1141
  const issues = [];
1102
- const full = path.resolve(process.cwd(), this.schemaPath);
1103
- try {
1104
- await fs.access(full);
1105
- } catch (_e) {
1106
- issues.push({
1107
- code: "DRZL_ANL_NOFILE",
1108
- level: "error",
1109
- message: `Schema file not found: ${this.schemaPath}`
1110
- });
1111
- return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
1142
+ const listed = Array.isArray(this.schemaPath);
1143
+ const inputs = listed ? this.schemaPath : [this.schemaPath];
1144
+ const fulls = inputs.map((p) => path.resolve(process.cwd(), p));
1145
+ let missing = false;
1146
+ for (let i = 0; i < fulls.length; i++) {
1147
+ try {
1148
+ await fs.access(fulls[i]);
1149
+ } catch (_e) {
1150
+ missing = true;
1151
+ issues.push({
1152
+ code: "DRZL_ANL_NOFILE",
1153
+ level: "error",
1154
+ message: `Schema file not found: ${inputs[i]}`
1155
+ });
1156
+ }
1112
1157
  }
1113
- let mod;
1114
- try {
1115
- const { default: jiti } = await import("jiti");
1116
- const jit = jiti(import.meta.url, { moduleCache: false });
1117
- mod = jit(full);
1118
- } catch (e) {
1119
- issues.push({
1120
- code: "DRZL_ANL_IMPORT",
1121
- level: "error",
1122
- message: `Failed to import schema: ${String(e)}`
1123
- });
1158
+ if (missing) {
1124
1159
  return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
1125
1160
  }
1126
- const exportsObj = mod?.default && typeof mod.default === "object" ? mod.default : mod;
1161
+ const { default: jiti } = await import("jiti");
1162
+ const jit = jiti(import.meta.url, { moduleCache: false });
1163
+ const exportsObj = {};
1164
+ const exportOrigin = /* @__PURE__ */ new Map();
1165
+ const duplicateDisagreement = (a, b) => {
1166
+ if (Object.is(a, b)) return null;
1167
+ const aCols = this.getSymbol(a, "drizzle:Columns");
1168
+ const bCols = this.getSymbol(b, "drizzle:Columns");
1169
+ if (aCols && bCols) {
1170
+ const aName = this.getSymbol(a, "drizzle:Name");
1171
+ const bName = this.getSymbol(b, "drizzle:Name");
1172
+ const aSchema = this.getSymbol(a, "drizzle:Schema");
1173
+ const bSchema = this.getSymbol(b, "drizzle:Schema");
1174
+ if (aName !== bName || aSchema !== bSchema) {
1175
+ return `two different tables ("${String(aName)}" and "${String(bName)}")`;
1176
+ }
1177
+ if (Object.keys(aCols).join(",") !== Object.keys(bCols).join(",")) {
1178
+ return `two declarations of table "${String(aName)}" with different columns`;
1179
+ }
1180
+ return null;
1181
+ }
1182
+ if (!!aCols !== !!bCols) return "a table and a non-table";
1183
+ const aEnum = a?.enumValues;
1184
+ const bEnum = b?.enumValues;
1185
+ if (Array.isArray(aEnum) && Array.isArray(bEnum)) {
1186
+ return JSON.stringify(aEnum) === JSON.stringify(bEnum) ? null : "two enums with different values";
1187
+ }
1188
+ return null;
1189
+ };
1190
+ for (let i = 0; i < fulls.length; i++) {
1191
+ let mod;
1192
+ try {
1193
+ mod = jit(fulls[i]);
1194
+ } catch (e) {
1195
+ issues.push({
1196
+ code: "DRZL_ANL_IMPORT",
1197
+ level: "error",
1198
+ // The single-path message keeps its historical bytes; a list names the file, since
1199
+ // "the schema" no longer identifies one.
1200
+ message: listed ? `Failed to import schema ${inputs[i]}: ${String(e)}` : `Failed to import schema: ${String(e)}`
1201
+ });
1202
+ return { dialect: "unknown", tables: [], enums: [], relations: [], issues };
1203
+ }
1204
+ const one = mod?.default && typeof mod.default === "object" ? mod.default : mod;
1205
+ for (const [name, val] of Object.entries(one)) {
1206
+ if (!(name in exportsObj)) {
1207
+ exportsObj[name] = val;
1208
+ exportOrigin.set(name, inputs[i]);
1209
+ continue;
1210
+ }
1211
+ const disagreement = duplicateDisagreement(exportsObj[name], val);
1212
+ if (!disagreement) continue;
1213
+ issues.push({
1214
+ code: "DRZL_ANL_DUP_EXPORT",
1215
+ level: "warn",
1216
+ message: `Export "${name}" is ${disagreement}: defined by both ${exportOrigin.get(name)} and ${inputs[i]}; keeping the one in ${exportOrigin.get(name)}.`,
1217
+ path: name
1218
+ });
1219
+ }
1220
+ }
1127
1221
  const tables = [];
1128
1222
  const relations = [];
1129
1223
  const enums = [];
@@ -1276,6 +1370,12 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1276
1370
  _SchemaAnalyzer.INT_RANGES = {
1277
1371
  // 8 bit
1278
1372
  MySqlTinyInt: ["-128", "127"],
1373
+ // Absent until the unsigned fix swept the family: v1 states `number int8` for the same
1374
+ // column, so the majors disagreed about every SingleStore tinyint. That is the shape the
1375
+ // cross-major diff in `scripts/verify-packed.sh` exists to catch, and its fixture carries no
1376
+ // SingleStore table, so unsigned-int-ranges.spec.ts holds these two classes across both
1377
+ // majors instead. The width is the type's, the one `MySqlTinyInt` beside it already carries.
1378
+ SingleStoreTinyInt: ["-128", "127"],
1279
1379
  SQLiteInteger: ["-9223372036854775808", "9223372036854775807"],
1280
1380
  // 16 bit
1281
1381
  PgSmallInt: ["-32768", "32767"],
@@ -1288,6 +1388,8 @@ _SchemaAnalyzer.INT_RANGES = {
1288
1388
  SingleStoreSmallInt: ["-32768", "32767"],
1289
1389
  // 24 bit
1290
1390
  MySqlMediumInt: ["-8388608", "8388607"],
1391
+ // As SingleStoreTinyInt above: v1 states `number int24` and this table said nothing.
1392
+ SingleStoreMediumInt: ["-8388608", "8388607"],
1291
1393
  // 32 bit
1292
1394
  PgInteger: ["-2147483648", "2147483647"],
1293
1395
  PgSerial: ["-2147483648", "2147483647"],
@@ -1302,7 +1404,61 @@ _SchemaAnalyzer.INT_RANGES = {
1302
1404
  PgBigInt64: ["-9223372036854775808", "9223372036854775807"],
1303
1405
  PgBigSerial64: ["-9223372036854775808", "9223372036854775807"],
1304
1406
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
1305
- SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
1407
+ SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"],
1408
+ // MySQL and SingleStore `serial`, which is `bigint unsigned auto_increment`: unsigned by the
1409
+ // builder's own definition, with no `config.unsigned` stating it, so the flag-keyed table
1410
+ // below cannot answer and the range lives here. The mode is number, so the safe-integer
1411
+ // ceiling rather than the column's, exactly as the 53 bit block above. The Postgres serials
1412
+ // stay signed on purpose: a Postgres serial is a plain integer defaulting from a sequence,
1413
+ // and the negative backfill note above applies to them and not to these. Before this entry
1414
+ // the class was in no table at all, so an auto-increment column accepted -1 and the majors
1415
+ // disagreed: v1 states `number uint53` for the same column and was already bounded.
1416
+ MySqlSerial: ["0", "9007199254740991"],
1417
+ SingleStoreSerial: ["0", "9007199254740991"]
1418
+ };
1419
+ /**
1420
+ * The same widths with `{ unsigned: true }` set, which is the half the table above cannot see.
1421
+ *
1422
+ * On 0.4x the flag moves no class name: `int('x', { unsigned: true })` still builds a
1423
+ * `MySqlInt`, and only `config.unsigned` and the ` unsigned` suffix on `getSQLType()` record
1424
+ * the difference, measured off real 0.45.2 columns. So the table above answered every unsigned
1425
+ * width with its signed range, and the emitted select schema refused every stored value in the
1426
+ * upper half of the column: an `int unsigned` holding 4294967295 failed validation on a row the
1427
+ * database returned, and the same one width up meant `bigint unsigned` refused
1428
+ * 18446744073709551615n.
1429
+ *
1430
+ * The ceilings are the type's, verified against a live MySQL 8.4.11: 255, 65535, 16777215 and
1431
+ * 4294967295 store and return, -1 and each ceiling plus one are refused with
1432
+ * ER_WARN_DATA_OUT_OF_RANGE. The bigint pair keeps the two modes apart for the reason the
1433
+ * signed pair above does: number mode tops out at the safe-integer bound the wire imposes,
1434
+ * bigint mode at the column's own 2^64-1, which a bigint can spell. SingleStore is MySQL wire
1435
+ * compatible, ships the same builders with the same `config.unsigned`, and v1 states the same
1436
+ * `uintN` semantics for it, measured off real rc.4 columns; the entries keep the majors in
1437
+ * agreement, which is what the cross-major diff in `scripts/verify-packed.sh` holds together.
1438
+ *
1439
+ * Keyed by class exactly like `INT_RANGES`, and consulted only when `config.unsigned` is
1440
+ * `true`, so no Postgres or SQLite column can ever reach it: neither dialect has an unsigned
1441
+ * spelling, neither builder accepts the flag, and no class of theirs is named here.
1442
+ */
1443
+ _SchemaAnalyzer.UNSIGNED_INT_RANGES = {
1444
+ // 8 bit
1445
+ MySqlTinyInt: ["0", "255"],
1446
+ SingleStoreTinyInt: ["0", "255"],
1447
+ // 16 bit
1448
+ MySqlSmallInt: ["0", "65535"],
1449
+ SingleStoreSmallInt: ["0", "65535"],
1450
+ // 24 bit
1451
+ MySqlMediumInt: ["0", "16777215"],
1452
+ SingleStoreMediumInt: ["0", "16777215"],
1453
+ // 32 bit
1454
+ MySqlInt: ["0", "4294967295"],
1455
+ SingleStoreInt: ["0", "4294967295"],
1456
+ // 53 bit, the JS safe-integer ceiling rather than the column's
1457
+ MySqlBigInt53: ["0", "9007199254740991"],
1458
+ SingleStoreBigInt53: ["0", "9007199254740991"],
1459
+ // 64 bit, representable because the value is a bigint
1460
+ MySqlBigInt64: ["0", "18446744073709551615"],
1461
+ SingleStoreBigInt64: ["0", "18446744073709551615"]
1306
1462
  };
1307
1463
  /**
1308
1464
  * The numeric column classes that are not exact, and the magnitude each one can really hold.
@@ -1377,34 +1533,61 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1377
1533
  // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1378
1534
  // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1379
1535
  /**
1380
- * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1536
+ * The number columns whose server has an answer about a non-finite double, and what it is.
1537
+ *
1538
+ * Three states rather than two, and the third is the reason this table has a `false` half at all.
1539
+ * A column present here with `true` stores the value and hands it back, so a schema refusing it
1540
+ * refuses rows the column returns. A column present with `false` is one the server was asked
1541
+ * about and refused, so a schema accepting it promises what the server will not take. A column
1542
+ * *absent* is one nobody has measured, and the generators leave whatever their library does alone
1543
+ * rather than guessing; `nonFiniteAccepted` and `nonFiniteRefused` in `@drzl/validation-core` are
1544
+ * the two readings of that.
1381
1545
  *
1382
1546
  * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
1383
1547
  * 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.
1548
+ * drizzle, which the cross-major diff in `verify-packed.sh` fails on. Every class name here is the
1549
+ * same on both majors, read off real columns on 0.45.2 and on 1.0.0-rc.4, so this table also
1550
+ * answers for a v1 column and the two answers are identical rather than merely compatible.
1551
+ * non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1552
+ *
1553
+ * Postgres and Gel store all three. Gel joined on a measurement of its own rather than on being
1554
+ * Postgres-backed: a live Gel 7.1 stored `nan`, `inf` and `-inf` in both `std::float32` and
1555
+ * `std::float64` and handed all three back, through a cast and again through a stored property.
1556
+ * Without them every row of such a column failed validation.
1388
1557
  *
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.
1558
+ * MySQL and SingleStore refuse all three, and that used to be left unstated on the reasoning that
1559
+ * a column stating nothing costs nothing. It cost two libraries: `v.number()` and ArkType's
1560
+ * `number` take both infinities where `z.number()` and `Type.Number()` refuse them, so an
1561
+ * unbounded `double` or `real` accepted a value the server answers `ER_WARN_DATA_OUT_OF_RANGE`
1562
+ * for. Measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`, on the binary prepared path, which is
1563
+ * the one that puts the real IEEE double on the wire: `float`, `double` and `real` refuse
1564
+ * `Infinity`, `-Infinity` and `NaN` alike, while `double` and `real` store 1e300 and
1565
+ * 3.4028235e38 unchanged. SingleStore is MySQL wire-compatible and unmeasured, and takes MySQL's
1566
+ * answer here exactly as it already takes MySQL's float32 bound in `INEXACT_RANGES`.
1392
1567
  *
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.
1568
+ * No SQLite class belongs here in either direction. A real SQLite 3.53.4 stores both infinities in
1569
+ * a `real` and hands them back, and silently turns `NaN` into NULL, so it is neither the Postgres
1570
+ * answer nor the MySQL one; it is filed on its own and a column needs both halves of it or none.
1397
1571
  *
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.
1572
+ * The decimal families are absent too. `PgNumeric` is a string whose pattern already accepts `NaN`
1573
+ * and `Infinity`. `PgNumericNumber` is a per-column question this table cannot ask: it takes `NaN`
1574
+ * at any width and an infinity only where no precision is declared, and `columnConstraints`
1575
+ * answers it beside the bound that decides it. MySQL's `decimal` is absent because the two client
1576
+ * paths disagree: on the binary prepared path MySQL 8.4.11 silently stored `0.00` for all three,
1577
+ * where the text path answers `Incorrect decimal value`, and "refuses" is only half true of a
1578
+ * column that accepted the row.
1402
1579
  */
1403
- _SchemaAnalyzer.PG_NON_FINITE = {
1580
+ _SchemaAnalyzer.NON_FINITE_BY_CLASS = {
1404
1581
  PgReal: { nan: true, infinity: true },
1405
1582
  PgDoublePrecision: { nan: true, infinity: true },
1406
1583
  GelReal: { nan: true, infinity: true },
1407
- GelDoublePrecision: { nan: true, infinity: true }
1584
+ GelDoublePrecision: { nan: true, infinity: true },
1585
+ MySqlFloat: { nan: false, infinity: false },
1586
+ MySqlDouble: { nan: false, infinity: false },
1587
+ MySqlReal: { nan: false, infinity: false },
1588
+ SingleStoreFloat: { nan: false, infinity: false },
1589
+ SingleStoreDouble: { nan: false, infinity: false },
1590
+ SingleStoreReal: { nan: false, infinity: false }
1408
1591
  };
1409
1592
  var SchemaAnalyzer = _SchemaAnalyzer;
1410
1593
  var index_default = SchemaAnalyzer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",