@drzl/analyzer 1.17.5 → 1.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -106,14 +106,24 @@ function describeV1Column(column) {
106
106
  const out = {};
107
107
  switch (semantic) {
108
108
  case "int8":
109
+ case "uint8":
109
110
  case "int16":
110
111
  case "int24":
111
112
  case "int32":
112
113
  case "int53":
113
114
  case "uint53":
114
115
  case "int64": {
116
+ if (DECIMAL_BIGINT_MODE.test(entityKind)) {
117
+ out.tsType = "bigint";
118
+ out.dbType = "NUMERIC";
119
+ out.integer = true;
120
+ const range2 = decimalModeRange(column, entityKind, "bigint");
121
+ if (range2) [out.min, out.max] = range2;
122
+ break;
123
+ }
115
124
  const range = {
116
125
  int8: ["-128", "127"],
126
+ uint8: ["0", "255"],
117
127
  int16: ["-32768", "32767"],
118
128
  int24: ["-8388608", "8388607"],
119
129
  int32: ["-2147483648", "2147483647"],
@@ -126,7 +136,7 @@ function describeV1Column(column) {
126
136
  [out.min, out.max] = range;
127
137
  out.integer = true;
128
138
  out.tsType = js === "bigint" ? "bigint" : "number";
129
- out.dbType = semantic === "int8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
139
+ out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
130
140
  break;
131
141
  }
132
142
  case "year":
@@ -175,6 +185,16 @@ function describeV1Column(column) {
175
185
  out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
176
186
  break;
177
187
  case "timestamp":
188
+ // `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
189
+ // stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
190
+ // are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
191
+ // `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
192
+ // `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
193
+ // A label only, since `dbType` is read outside this file in exactly one place,
194
+ // `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
195
+ // because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
196
+ // two majors disagreeing about a column is what the cross-major diff exists to catch.
197
+ case "datetime":
178
198
  out.tsType = js === "string" ? "string" : "Date";
179
199
  out.dbType = "TIMESTAMP";
180
200
  break;
@@ -196,13 +216,25 @@ function describeV1Column(column) {
196
216
  const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
197
217
  const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
198
218
  out.tsType = "string";
199
- out.dbType = codec === "bit" ? "BIT" : "BINARY";
219
+ out.dbType = bytes ? "BINARY" : "BIT";
200
220
  out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
201
221
  kind: "bitstring",
202
222
  length: declaredLength(column),
203
223
  // A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
204
224
  // most that many, which is why `''` is valid there and not here.
205
- exact: codec === "bit"
225
+ //
226
+ // `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
227
+ // states no codec, so both of its builders came back `exact: false` and a `bit(3)`
228
+ // was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
229
+ // `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
230
+ // type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
231
+ // refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
232
+ // 1.0.0-rc.4 answers the same for both columns.
233
+ //
234
+ // The class rather than a prefix, because `CockroachVarbit` starts with neither
235
+ // `CockroachBit` nor anything else this could key on without catching the varying
236
+ // half too.
237
+ exact: codec === "bit" || entity === "CockroachBit"
206
238
  };
207
239
  break;
208
240
  }
@@ -254,10 +286,11 @@ function describeV1Column(column) {
254
286
  out.tsType = "number";
255
287
  out.dbType = "NUMERIC";
256
288
  out.integer = false;
257
- [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
289
+ const range = decimalModeRange(column, entityKind, "number");
290
+ if (range) [out.min, out.max] = range;
258
291
  if (codec === "numeric:number") {
259
292
  out.allowsNaN = true;
260
- out.allowsInfinity = false;
293
+ out.allowsInfinity = !declaredDecimalRange(column);
261
294
  }
262
295
  } else if (js === "string") {
263
296
  out.tsType = "string";
@@ -286,6 +319,28 @@ function declaredLength(column) {
286
319
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
287
320
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
288
321
  }
322
+ function declaredDecimalRange(column) {
323
+ const cfg = column?.config ?? {};
324
+ const precision = column?.precision ?? cfg.precision;
325
+ const scale = column?.scale ?? cfg.scale ?? 0;
326
+ if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
327
+ return void 0;
328
+ if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
329
+ const nines = "9".repeat(precision);
330
+ const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
331
+ return [`-${max}`, max];
332
+ }
333
+ var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
334
+ var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
335
+ var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
336
+ function decimalModeRange(column, kind, mode) {
337
+ const declared = declaredDecimalRange(column);
338
+ if (declared) return declared;
339
+ if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
340
+ return MYSQL_IMPLICIT_DECIMAL_RANGE;
341
+ if (kind.startsWith("SQLite")) return void 0;
342
+ return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
343
+ }
289
344
  var VIEW_CONFIG_FIELDS = {
290
345
  "drizzle:Columns": "selectedFields",
291
346
  "drizzle:Name": "name",
@@ -583,6 +638,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
583
638
  out.allowsNaN = nonFinite.nan;
584
639
  out.allowsInfinity = nonFinite.infinity;
585
640
  }
641
+ if (DECIMAL_NUMBER_MODE.test(ctor)) {
642
+ const range2 = decimalModeRange(column, ctor, "number");
643
+ if (range2) [out.min, out.max] = range2;
644
+ out.integer = false;
645
+ if (ctor === "PgNumericNumber") {
646
+ out.allowsNaN = true;
647
+ out.allowsInfinity = !declaredDecimalRange(column);
648
+ }
649
+ } else if (DECIMAL_BIGINT_MODE.test(ctor)) {
650
+ const range2 = decimalModeRange(column, ctor, "bigint");
651
+ if (range2) [out.min, out.max] = range2;
652
+ out.integer = true;
653
+ }
586
654
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
587
655
  return out;
588
656
  }
@@ -850,7 +918,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
850
918
  if (/^Gel/i.test(ctor)) {
851
919
  if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
852
920
  if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
853
- if (/Real|DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "NUMERIC" };
921
+ if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
922
+ if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
854
923
  if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
855
924
  if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
856
925
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
@@ -901,7 +970,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
901
970
  const v1 = describeV1Column(col);
902
971
  const constraints = this.columnConstraints(col);
903
972
  if (v1?.shape) delete constraints.maxLength;
904
- const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
973
+ const sqlKind = String(
974
+ outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
975
+ );
905
976
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
906
977
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
907
978
  const ctorName = String(col?.constructor?.name ?? "");
@@ -1268,11 +1339,29 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1268
1339
  SQLiteReal: null,
1269
1340
  SingleStoreDouble: null,
1270
1341
  SingleStoreReal: null,
1271
- // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1272
- // `describeV1Column`. This one is about what a JS number can carry rather than about the
1273
- // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1274
- PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1342
+ // Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
1343
+ // used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
1344
+ // else at all, so a `real` column accepted 1e300 and the server refused it.
1345
+ //
1346
+ // Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
1347
+ // through the `gel` client, casting each literal so the server parses it, and again through a
1348
+ // stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
1349
+ //
1350
+ // 3.4028234663852886e38 accepted, returned unchanged
1351
+ // 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
1352
+ // 3.402823567797337e38 refused, "is out of range for type std::float32"
1353
+ // 1e300 refused, the same way
1354
+ //
1355
+ // The same value accepted, the same next double up refused, and the same rounding down of the
1356
+ // midpoint, so it takes the constant already here rather than a second name for one number.
1357
+ // float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
1358
+ // truthful finite bound.
1359
+ GelReal: PG_FLOAT4_RANGE,
1360
+ GelDoublePrecision: null
1275
1361
  };
1362
+ // `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
1363
+ // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1364
+ // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1276
1365
  /**
1277
1366
  * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1278
1367
  *
@@ -1283,17 +1372,25 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1283
1372
  * table also answers for a v1 column and the two answers are identical rather than merely
1284
1373
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1285
1374
  *
1286
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
1287
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
1288
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
1375
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
1376
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
1377
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
1378
+ *
1379
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
1380
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
1381
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
1382
+ * and again through a stored property. Without them every row of such a column failed validation.
1289
1383
  *
1290
1384
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
1291
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
1385
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
1386
+ * any width and an infinity only where no precision is declared, which is a per-column question
1387
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
1292
1388
  */
1293
1389
  _SchemaAnalyzer.PG_NON_FINITE = {
1294
1390
  PgReal: { nan: true, infinity: true },
1295
1391
  PgDoublePrecision: { nan: true, infinity: true },
1296
- PgNumericNumber: { nan: true, infinity: false }
1392
+ GelReal: { nan: true, infinity: true },
1393
+ GelDoublePrecision: { nan: true, infinity: true }
1297
1394
  };
1298
1395
  var SchemaAnalyzer = _SchemaAnalyzer;
1299
1396
  var index_default = SchemaAnalyzer;
package/dist/index.d.cts CHANGED
@@ -48,8 +48,16 @@ interface Column {
48
48
  *
49
49
  * Strings rather than numbers because a 64 bit bound is not representable as a JS number:
50
50
  * `9223372036854775807` rounds to `9223372036854775808` the moment it becomes one, so a
51
- * numeric field here would silently emit a wrong bound. Absent for floats and for `numeric`,
52
- * which have no integer range.
51
+ * numeric field here would silently emit a wrong bound. A 20 digit `numeric(20,0)` bound is
52
+ * further past that again.
53
+ *
54
+ * Not an integer range, despite the name this field used to be described by. An inexact column
55
+ * carries one too: a `real` is bounded by the magnitude the database refuses past, and a
56
+ * `numeric(10,2)` by the width its own declaration states. `integer` says which kind it is, and
57
+ * saying it is what stops a bounded float schema refusing 1.5.
58
+ *
59
+ * Absent where nothing declares a bound: an 8 byte float holds every finite JS number, and a
60
+ * `numeric` with no precision holds arbitrary precision.
53
61
  */
54
62
  min?: string;
55
63
  max?: string;
@@ -76,14 +84,22 @@ interface Column {
76
84
  * does too, but a `numeric(10,2)` refuses either infinity with `22003 numeric field overflow`
77
85
  * while still taking `NaN`, and `integer`/`bigint` refuse all three.
78
86
  *
79
- * So `numeric` in `{ mode: 'number' }` states `allowsInfinity: false` deliberately, and it is a
80
- * narrowing rather than an oversight: nothing here reads a column's precision or scale, so the
81
- * two `numeric` declarations are indistinguishable, and admitting the infinities would make the
82
- * schema promise what the server refuses for the commoner of the two.
87
+ * So `numeric` in `{ mode: 'number' }` answers each of the two separately: `allowsNaN` at any
88
+ * width, and `allowsInfinity` only where the declaration carries no precision. That used to be a
89
+ * flat `false`, and the recorded reason was that nothing here read a column's precision or scale,
90
+ * so the two declarations were indistinguishable and the narrower answer was the safer one.
91
+ * `declaredDecimalRange` reads both numbers now, the two are distinguishable, and each says what
92
+ * its own server does.
83
93
  *
84
- * Postgres only, today. MySQL refuses all three on a `float`/`double` and silently stores `0.00`
85
- * for a `decimal`. SQLite returns both infinities and silently turns `NaN` into NULL, which is
86
- * real and is filed on its own: a column needs both halves of that answer or none.
94
+ * Postgres and Gel. MySQL refuses all three on a `float`/`double`, and on a `decimal` refuses
95
+ * them outright rather than storing `0.00`: measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`,
96
+ * all three answer `Incorrect decimal value`. SQLite returns both infinities and silently turns
97
+ * `NaN` into NULL, which is real and is filed on its own: a column needs both halves of that
98
+ * answer or none.
99
+ *
100
+ * Gel joined on a measurement of its own rather than on being Postgres-backed: a live Gel 7.1
101
+ * stored `nan`, `inf` and `-inf` in both `std::float32` and `std::float64` and handed all three
102
+ * back unchanged, through a cast and through a stored property.
87
103
  *
88
104
  * Absent on every other column, including the string mode of `numeric`, which already carries the
89
105
  * same fact as a pattern; see `COLUMN_FORMATS.numeric` in `@drzl/validation-core`.
@@ -489,12 +505,19 @@ declare class SchemaAnalyzer {
489
505
  * table also answers for a v1 column and the two answers are identical rather than merely
490
506
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
491
507
  *
492
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
493
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
494
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
508
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
509
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
510
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
511
+ *
512
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
513
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
514
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
515
+ * and again through a stored property. Without them every row of such a column failed validation.
495
516
  *
496
517
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
497
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
518
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
519
+ * any width and an infinity only where no precision is declared, which is a per-column question
520
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
498
521
  */
499
522
  private static readonly PG_NON_FINITE;
500
523
  /**
package/dist/index.d.ts CHANGED
@@ -48,8 +48,16 @@ interface Column {
48
48
  *
49
49
  * Strings rather than numbers because a 64 bit bound is not representable as a JS number:
50
50
  * `9223372036854775807` rounds to `9223372036854775808` the moment it becomes one, so a
51
- * numeric field here would silently emit a wrong bound. Absent for floats and for `numeric`,
52
- * which have no integer range.
51
+ * numeric field here would silently emit a wrong bound. A 20 digit `numeric(20,0)` bound is
52
+ * further past that again.
53
+ *
54
+ * Not an integer range, despite the name this field used to be described by. An inexact column
55
+ * carries one too: a `real` is bounded by the magnitude the database refuses past, and a
56
+ * `numeric(10,2)` by the width its own declaration states. `integer` says which kind it is, and
57
+ * saying it is what stops a bounded float schema refusing 1.5.
58
+ *
59
+ * Absent where nothing declares a bound: an 8 byte float holds every finite JS number, and a
60
+ * `numeric` with no precision holds arbitrary precision.
53
61
  */
54
62
  min?: string;
55
63
  max?: string;
@@ -76,14 +84,22 @@ interface Column {
76
84
  * does too, but a `numeric(10,2)` refuses either infinity with `22003 numeric field overflow`
77
85
  * while still taking `NaN`, and `integer`/`bigint` refuse all three.
78
86
  *
79
- * So `numeric` in `{ mode: 'number' }` states `allowsInfinity: false` deliberately, and it is a
80
- * narrowing rather than an oversight: nothing here reads a column's precision or scale, so the
81
- * two `numeric` declarations are indistinguishable, and admitting the infinities would make the
82
- * schema promise what the server refuses for the commoner of the two.
87
+ * So `numeric` in `{ mode: 'number' }` answers each of the two separately: `allowsNaN` at any
88
+ * width, and `allowsInfinity` only where the declaration carries no precision. That used to be a
89
+ * flat `false`, and the recorded reason was that nothing here read a column's precision or scale,
90
+ * so the two declarations were indistinguishable and the narrower answer was the safer one.
91
+ * `declaredDecimalRange` reads both numbers now, the two are distinguishable, and each says what
92
+ * its own server does.
83
93
  *
84
- * Postgres only, today. MySQL refuses all three on a `float`/`double` and silently stores `0.00`
85
- * for a `decimal`. SQLite returns both infinities and silently turns `NaN` into NULL, which is
86
- * real and is filed on its own: a column needs both halves of that answer or none.
94
+ * Postgres and Gel. MySQL refuses all three on a `float`/`double`, and on a `decimal` refuses
95
+ * them outright rather than storing `0.00`: measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`,
96
+ * all three answer `Incorrect decimal value`. SQLite returns both infinities and silently turns
97
+ * `NaN` into NULL, which is real and is filed on its own: a column needs both halves of that
98
+ * answer or none.
99
+ *
100
+ * Gel joined on a measurement of its own rather than on being Postgres-backed: a live Gel 7.1
101
+ * stored `nan`, `inf` and `-inf` in both `std::float32` and `std::float64` and handed all three
102
+ * back unchanged, through a cast and through a stored property.
87
103
  *
88
104
  * Absent on every other column, including the string mode of `numeric`, which already carries the
89
105
  * same fact as a pattern; see `COLUMN_FORMATS.numeric` in `@drzl/validation-core`.
@@ -489,12 +505,19 @@ declare class SchemaAnalyzer {
489
505
  * table also answers for a v1 column and the two answers are identical rather than merely
490
506
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
491
507
  *
492
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
493
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
494
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
508
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
509
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
510
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
511
+ *
512
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
513
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
514
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
515
+ * and again through a stored property. Without them every row of such a column failed validation.
495
516
  *
496
517
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
497
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
518
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
519
+ * any width and an infinity only where no precision is declared, which is a per-column question
520
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
498
521
  */
499
522
  private static readonly PG_NON_FINITE;
500
523
  /**
package/dist/index.js CHANGED
@@ -65,14 +65,24 @@ function describeV1Column(column) {
65
65
  const out = {};
66
66
  switch (semantic) {
67
67
  case "int8":
68
+ case "uint8":
68
69
  case "int16":
69
70
  case "int24":
70
71
  case "int32":
71
72
  case "int53":
72
73
  case "uint53":
73
74
  case "int64": {
75
+ if (DECIMAL_BIGINT_MODE.test(entityKind)) {
76
+ out.tsType = "bigint";
77
+ out.dbType = "NUMERIC";
78
+ out.integer = true;
79
+ const range2 = decimalModeRange(column, entityKind, "bigint");
80
+ if (range2) [out.min, out.max] = range2;
81
+ break;
82
+ }
74
83
  const range = {
75
84
  int8: ["-128", "127"],
85
+ uint8: ["0", "255"],
76
86
  int16: ["-32768", "32767"],
77
87
  int24: ["-8388608", "8388607"],
78
88
  int32: ["-2147483648", "2147483647"],
@@ -85,7 +95,7 @@ function describeV1Column(column) {
85
95
  [out.min, out.max] = range;
86
96
  out.integer = true;
87
97
  out.tsType = js === "bigint" ? "bigint" : "number";
88
- out.dbType = semantic === "int8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
98
+ out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
89
99
  break;
90
100
  }
91
101
  case "year":
@@ -134,6 +144,16 @@ function describeV1Column(column) {
134
144
  out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
135
145
  break;
136
146
  case "timestamp":
147
+ // `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
148
+ // stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
149
+ // are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
150
+ // `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
151
+ // `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
152
+ // A label only, since `dbType` is read outside this file in exactly one place,
153
+ // `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
154
+ // because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
155
+ // two majors disagreeing about a column is what the cross-major diff exists to catch.
156
+ case "datetime":
137
157
  out.tsType = js === "string" ? "string" : "Date";
138
158
  out.dbType = "TIMESTAMP";
139
159
  break;
@@ -155,13 +175,25 @@ function describeV1Column(column) {
155
175
  const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
156
176
  const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
157
177
  out.tsType = "string";
158
- out.dbType = codec === "bit" ? "BIT" : "BINARY";
178
+ out.dbType = bytes ? "BINARY" : "BIT";
159
179
  out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
160
180
  kind: "bitstring",
161
181
  length: declaredLength(column),
162
182
  // A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
163
183
  // most that many, which is why `''` is valid there and not here.
164
- exact: codec === "bit"
184
+ //
185
+ // `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
186
+ // states no codec, so both of its builders came back `exact: false` and a `bit(3)`
187
+ // was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
188
+ // `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
189
+ // type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
190
+ // refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
191
+ // 1.0.0-rc.4 answers the same for both columns.
192
+ //
193
+ // The class rather than a prefix, because `CockroachVarbit` starts with neither
194
+ // `CockroachBit` nor anything else this could key on without catching the varying
195
+ // half too.
196
+ exact: codec === "bit" || entity === "CockroachBit"
165
197
  };
166
198
  break;
167
199
  }
@@ -213,10 +245,11 @@ function describeV1Column(column) {
213
245
  out.tsType = "number";
214
246
  out.dbType = "NUMERIC";
215
247
  out.integer = false;
216
- [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
248
+ const range = decimalModeRange(column, entityKind, "number");
249
+ if (range) [out.min, out.max] = range;
217
250
  if (codec === "numeric:number") {
218
251
  out.allowsNaN = true;
219
- out.allowsInfinity = false;
252
+ out.allowsInfinity = !declaredDecimalRange(column);
220
253
  }
221
254
  } else if (js === "string") {
222
255
  out.tsType = "string";
@@ -245,6 +278,28 @@ function declaredLength(column) {
245
278
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
246
279
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
247
280
  }
281
+ function declaredDecimalRange(column) {
282
+ const cfg = column?.config ?? {};
283
+ const precision = column?.precision ?? cfg.precision;
284
+ const scale = column?.scale ?? cfg.scale ?? 0;
285
+ if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
286
+ return void 0;
287
+ if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
288
+ const nines = "9".repeat(precision);
289
+ const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
290
+ return [`-${max}`, max];
291
+ }
292
+ var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
293
+ var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
294
+ var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
295
+ function decimalModeRange(column, kind, mode) {
296
+ const declared = declaredDecimalRange(column);
297
+ if (declared) return declared;
298
+ if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
299
+ return MYSQL_IMPLICIT_DECIMAL_RANGE;
300
+ if (kind.startsWith("SQLite")) return void 0;
301
+ return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
302
+ }
248
303
  var VIEW_CONFIG_FIELDS = {
249
304
  "drizzle:Columns": "selectedFields",
250
305
  "drizzle:Name": "name",
@@ -542,6 +597,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
542
597
  out.allowsNaN = nonFinite.nan;
543
598
  out.allowsInfinity = nonFinite.infinity;
544
599
  }
600
+ if (DECIMAL_NUMBER_MODE.test(ctor)) {
601
+ const range2 = decimalModeRange(column, ctor, "number");
602
+ if (range2) [out.min, out.max] = range2;
603
+ out.integer = false;
604
+ if (ctor === "PgNumericNumber") {
605
+ out.allowsNaN = true;
606
+ out.allowsInfinity = !declaredDecimalRange(column);
607
+ }
608
+ } else if (DECIMAL_BIGINT_MODE.test(ctor)) {
609
+ const range2 = decimalModeRange(column, ctor, "bigint");
610
+ if (range2) [out.min, out.max] = range2;
611
+ out.integer = true;
612
+ }
545
613
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
546
614
  return out;
547
615
  }
@@ -809,7 +877,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
809
877
  if (/^Gel/i.test(ctor)) {
810
878
  if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
811
879
  if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
812
- if (/Real|DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "NUMERIC" };
880
+ if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
881
+ if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
813
882
  if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
814
883
  if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
815
884
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
@@ -860,7 +929,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
860
929
  const v1 = describeV1Column(col);
861
930
  const constraints = this.columnConstraints(col);
862
931
  if (v1?.shape) delete constraints.maxLength;
863
- const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
932
+ const sqlKind = String(
933
+ outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
934
+ );
864
935
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
865
936
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
866
937
  const ctorName = String(col?.constructor?.name ?? "");
@@ -1227,11 +1298,29 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1227
1298
  SQLiteReal: null,
1228
1299
  SingleStoreDouble: null,
1229
1300
  SingleStoreReal: null,
1230
- // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1231
- // `describeV1Column`. This one is about what a JS number can carry rather than about the
1232
- // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1233
- PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1301
+ // Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
1302
+ // used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
1303
+ // else at all, so a `real` column accepted 1e300 and the server refused it.
1304
+ //
1305
+ // Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
1306
+ // through the `gel` client, casting each literal so the server parses it, and again through a
1307
+ // stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
1308
+ //
1309
+ // 3.4028234663852886e38 accepted, returned unchanged
1310
+ // 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
1311
+ // 3.402823567797337e38 refused, "is out of range for type std::float32"
1312
+ // 1e300 refused, the same way
1313
+ //
1314
+ // The same value accepted, the same next double up refused, and the same rounding down of the
1315
+ // midpoint, so it takes the constant already here rather than a second name for one number.
1316
+ // float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
1317
+ // truthful finite bound.
1318
+ GelReal: PG_FLOAT4_RANGE,
1319
+ GelDoublePrecision: null
1234
1320
  };
1321
+ // `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
1322
+ // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1323
+ // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1235
1324
  /**
1236
1325
  * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1237
1326
  *
@@ -1242,17 +1331,25 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1242
1331
  * table also answers for a v1 column and the two answers are identical rather than merely
1243
1332
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1244
1333
  *
1245
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
1246
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
1247
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
1334
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
1335
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
1336
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
1337
+ *
1338
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
1339
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
1340
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
1341
+ * and again through a stored property. Without them every row of such a column failed validation.
1248
1342
  *
1249
1343
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
1250
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
1344
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
1345
+ * any width and an infinity only where no precision is declared, which is a per-column question
1346
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
1251
1347
  */
1252
1348
  _SchemaAnalyzer.PG_NON_FINITE = {
1253
1349
  PgReal: { nan: true, infinity: true },
1254
1350
  PgDoublePrecision: { nan: true, infinity: true },
1255
- PgNumericNumber: { nan: true, infinity: false }
1351
+ GelReal: { nan: true, infinity: true },
1352
+ GelDoublePrecision: { nan: true, infinity: true }
1256
1353
  };
1257
1354
  var SchemaAnalyzer = _SchemaAnalyzer;
1258
1355
  var index_default = SchemaAnalyzer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.17.5",
3
+ "version": "1.17.7",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",