@drzl/analyzer 1.17.4 → 1.17.6

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
@@ -87,6 +87,7 @@ var BYTE_STRING_CLASSES = /* @__PURE__ */ new Set([
87
87
  "SingleStoreBinary",
88
88
  "SingleStoreVarBinary"
89
89
  ]);
90
+ var BUFFER_CLASSES = /* @__PURE__ */ new Set(["SQLiteBlobBuffer"]);
90
91
  function describeV1Column(column) {
91
92
  const codec = column?.codec;
92
93
  const dataType = column?.dataType;
@@ -111,6 +112,14 @@ function describeV1Column(column) {
111
112
  case "int53":
112
113
  case "uint53":
113
114
  case "int64": {
115
+ if (DECIMAL_BIGINT_MODE.test(entityKind)) {
116
+ out.tsType = "bigint";
117
+ out.dbType = "NUMERIC";
118
+ out.integer = true;
119
+ const range2 = decimalModeRange(column, entityKind, "bigint");
120
+ if (range2) [out.min, out.max] = range2;
121
+ break;
122
+ }
114
123
  const range = {
115
124
  int8: ["-128", "127"],
116
125
  int16: ["-32768", "32767"],
@@ -253,10 +262,11 @@ function describeV1Column(column) {
253
262
  out.tsType = "number";
254
263
  out.dbType = "NUMERIC";
255
264
  out.integer = false;
256
- [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
265
+ const range = decimalModeRange(column, entityKind, "number");
266
+ if (range) [out.min, out.max] = range;
257
267
  if (codec === "numeric:number") {
258
268
  out.allowsNaN = true;
259
- out.allowsInfinity = false;
269
+ out.allowsInfinity = !declaredDecimalRange(column);
260
270
  }
261
271
  } else if (js === "string") {
262
272
  out.tsType = "string";
@@ -285,6 +295,28 @@ function declaredLength(column) {
285
295
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
286
296
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
287
297
  }
298
+ function declaredDecimalRange(column) {
299
+ const cfg = column?.config ?? {};
300
+ const precision = column?.precision ?? cfg.precision;
301
+ const scale = column?.scale ?? cfg.scale ?? 0;
302
+ if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
303
+ return void 0;
304
+ if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
305
+ const nines = "9".repeat(precision);
306
+ const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
307
+ return [`-${max}`, max];
308
+ }
309
+ var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
310
+ var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
311
+ var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
312
+ function decimalModeRange(column, kind, mode) {
313
+ const declared = declaredDecimalRange(column);
314
+ if (declared) return declared;
315
+ if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
316
+ return MYSQL_IMPLICIT_DECIMAL_RANGE;
317
+ if (kind.startsWith("SQLite")) return void 0;
318
+ return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
319
+ }
288
320
  var VIEW_CONFIG_FIELDS = {
289
321
  "drizzle:Columns": "selectedFields",
290
322
  "drizzle:Name": "name",
@@ -582,6 +614,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
582
614
  out.allowsNaN = nonFinite.nan;
583
615
  out.allowsInfinity = nonFinite.infinity;
584
616
  }
617
+ if (DECIMAL_NUMBER_MODE.test(ctor)) {
618
+ const range2 = decimalModeRange(column, ctor, "number");
619
+ if (range2) [out.min, out.max] = range2;
620
+ out.integer = false;
621
+ if (ctor === "PgNumericNumber") {
622
+ out.allowsNaN = true;
623
+ out.allowsInfinity = !declaredDecimalRange(column);
624
+ }
625
+ } else if (DECIMAL_BIGINT_MODE.test(ctor)) {
626
+ const range2 = decimalModeRange(column, ctor, "bigint");
627
+ if (range2) [out.min, out.max] = range2;
628
+ out.integer = true;
629
+ }
585
630
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
586
631
  return out;
587
632
  }
@@ -593,12 +638,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
593
638
  tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
594
639
  dbType: "INTEGER"
595
640
  };
641
+ // Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
642
+ // `timestamp_ms` differ in the scale of the number on the wire, seconds against
643
+ // milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
644
+ // either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
645
+ // the class covers both, where the mode check that used to answer this fell through the
646
+ // switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
647
+ // The second came back `unknown`, and every generator emitted a schema accepting anything.
648
+ //
649
+ // `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
650
+ // column identically. `dbType` is read in exactly one place outside this file,
651
+ // `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
652
+ // relabel reaches no output. Measured rather than argued: emitting a `Date` column under
653
+ // both labels, nullable and not, through all five generators gives ten byte-identical pairs.
654
+ case "SQLiteTimestamp":
655
+ return { tsType: "Date", dbType: "DATE" };
596
656
  case "SQLiteText":
597
657
  return { tsType: "string", dbType: "TEXT" };
598
658
  case "SQLiteReal":
599
659
  return { tsType: "number", dbType: "REAL" };
660
+ // No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
661
+ // `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
662
+ // all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
663
+ // produces, which is why a real `blob()` reached neither it nor anything else.
600
664
  case "SQLiteBlob":
601
665
  return { tsType: "Uint8Array", dbType: "BLOB" };
666
+ // The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
667
+ // for the measurement; the answers here are v1's own for the same column, so this is the
668
+ // two majors agreeing rather than a new opinion.
669
+ case "SQLiteBlobBuffer":
670
+ return { tsType: "Buffer", dbType: "BYTEA" };
602
671
  // SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
603
672
  // is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
604
673
  // wider than the `any` a json column at least used to get.
@@ -825,7 +894,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
825
894
  if (/^Gel/i.test(ctor)) {
826
895
  if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
827
896
  if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
828
- if (/Real|DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "NUMERIC" };
897
+ if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
898
+ if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
829
899
  if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
830
900
  if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
831
901
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
@@ -876,11 +946,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
876
946
  const v1 = describeV1Column(col);
877
947
  const constraints = this.columnConstraints(col);
878
948
  if (v1?.shape) delete constraints.maxLength;
879
- const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
949
+ const sqlKind = String(
950
+ outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
951
+ );
880
952
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
881
953
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
882
954
  const ctorName = String(col?.constructor?.name ?? "");
883
- const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
955
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : BUFFER_CLASSES.has(ctorName) ? { kind: "buffer" } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
884
956
  if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
885
957
  const shape = (v1?.shape ?? fallbackShape)?.kind;
886
958
  const finalTs = v1?.tsType ?? tsType;
@@ -1243,11 +1315,29 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1243
1315
  SQLiteReal: null,
1244
1316
  SingleStoreDouble: null,
1245
1317
  SingleStoreReal: null,
1246
- // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1247
- // `describeV1Column`. This one is about what a JS number can carry rather than about the
1248
- // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1249
- PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1318
+ // Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
1319
+ // used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
1320
+ // else at all, so a `real` column accepted 1e300 and the server refused it.
1321
+ //
1322
+ // Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
1323
+ // through the `gel` client, casting each literal so the server parses it, and again through a
1324
+ // stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
1325
+ //
1326
+ // 3.4028234663852886e38 accepted, returned unchanged
1327
+ // 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
1328
+ // 3.402823567797337e38 refused, "is out of range for type std::float32"
1329
+ // 1e300 refused, the same way
1330
+ //
1331
+ // The same value accepted, the same next double up refused, and the same rounding down of the
1332
+ // midpoint, so it takes the constant already here rather than a second name for one number.
1333
+ // float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
1334
+ // truthful finite bound.
1335
+ GelReal: PG_FLOAT4_RANGE,
1336
+ GelDoublePrecision: null
1250
1337
  };
1338
+ // `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
1339
+ // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1340
+ // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1251
1341
  /**
1252
1342
  * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1253
1343
  *
@@ -1258,17 +1348,25 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1258
1348
  * table also answers for a v1 column and the two answers are identical rather than merely
1259
1349
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1260
1350
  *
1261
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
1262
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
1263
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
1351
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
1352
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
1353
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
1354
+ *
1355
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
1356
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
1357
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
1358
+ * and again through a stored property. Without them every row of such a column failed validation.
1264
1359
  *
1265
1360
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
1266
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
1361
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
1362
+ * any width and an infinity only where no precision is declared, which is a per-column question
1363
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
1267
1364
  */
1268
1365
  _SchemaAnalyzer.PG_NON_FINITE = {
1269
1366
  PgReal: { nan: true, infinity: true },
1270
1367
  PgDoublePrecision: { nan: true, infinity: true },
1271
- PgNumericNumber: { nan: true, infinity: false }
1368
+ GelReal: { nan: true, infinity: true },
1369
+ GelDoublePrecision: { nan: true, infinity: true }
1272
1370
  };
1273
1371
  var SchemaAnalyzer = _SchemaAnalyzer;
1274
1372
  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
@@ -46,6 +46,7 @@ var BYTE_STRING_CLASSES = /* @__PURE__ */ new Set([
46
46
  "SingleStoreBinary",
47
47
  "SingleStoreVarBinary"
48
48
  ]);
49
+ var BUFFER_CLASSES = /* @__PURE__ */ new Set(["SQLiteBlobBuffer"]);
49
50
  function describeV1Column(column) {
50
51
  const codec = column?.codec;
51
52
  const dataType = column?.dataType;
@@ -70,6 +71,14 @@ function describeV1Column(column) {
70
71
  case "int53":
71
72
  case "uint53":
72
73
  case "int64": {
74
+ if (DECIMAL_BIGINT_MODE.test(entityKind)) {
75
+ out.tsType = "bigint";
76
+ out.dbType = "NUMERIC";
77
+ out.integer = true;
78
+ const range2 = decimalModeRange(column, entityKind, "bigint");
79
+ if (range2) [out.min, out.max] = range2;
80
+ break;
81
+ }
73
82
  const range = {
74
83
  int8: ["-128", "127"],
75
84
  int16: ["-32768", "32767"],
@@ -212,10 +221,11 @@ function describeV1Column(column) {
212
221
  out.tsType = "number";
213
222
  out.dbType = "NUMERIC";
214
223
  out.integer = false;
215
- [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
224
+ const range = decimalModeRange(column, entityKind, "number");
225
+ if (range) [out.min, out.max] = range;
216
226
  if (codec === "numeric:number") {
217
227
  out.allowsNaN = true;
218
- out.allowsInfinity = false;
228
+ out.allowsInfinity = !declaredDecimalRange(column);
219
229
  }
220
230
  } else if (js === "string") {
221
231
  out.tsType = "string";
@@ -244,6 +254,28 @@ function declaredLength(column) {
244
254
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
245
255
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
246
256
  }
257
+ function declaredDecimalRange(column) {
258
+ const cfg = column?.config ?? {};
259
+ const precision = column?.precision ?? cfg.precision;
260
+ const scale = column?.scale ?? cfg.scale ?? 0;
261
+ if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
262
+ return void 0;
263
+ if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
264
+ const nines = "9".repeat(precision);
265
+ const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
266
+ return [`-${max}`, max];
267
+ }
268
+ var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
269
+ var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
270
+ var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
271
+ function decimalModeRange(column, kind, mode) {
272
+ const declared = declaredDecimalRange(column);
273
+ if (declared) return declared;
274
+ if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
275
+ return MYSQL_IMPLICIT_DECIMAL_RANGE;
276
+ if (kind.startsWith("SQLite")) return void 0;
277
+ return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
278
+ }
247
279
  var VIEW_CONFIG_FIELDS = {
248
280
  "drizzle:Columns": "selectedFields",
249
281
  "drizzle:Name": "name",
@@ -541,6 +573,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
541
573
  out.allowsNaN = nonFinite.nan;
542
574
  out.allowsInfinity = nonFinite.infinity;
543
575
  }
576
+ if (DECIMAL_NUMBER_MODE.test(ctor)) {
577
+ const range2 = decimalModeRange(column, ctor, "number");
578
+ if (range2) [out.min, out.max] = range2;
579
+ out.integer = false;
580
+ if (ctor === "PgNumericNumber") {
581
+ out.allowsNaN = true;
582
+ out.allowsInfinity = !declaredDecimalRange(column);
583
+ }
584
+ } else if (DECIMAL_BIGINT_MODE.test(ctor)) {
585
+ const range2 = decimalModeRange(column, ctor, "bigint");
586
+ if (range2) [out.min, out.max] = range2;
587
+ out.integer = true;
588
+ }
544
589
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
545
590
  return out;
546
591
  }
@@ -552,12 +597,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
552
597
  tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
553
598
  dbType: "INTEGER"
554
599
  };
600
+ // Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
601
+ // `timestamp_ms` differ in the scale of the number on the wire, seconds against
602
+ // milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
603
+ // either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
604
+ // the class covers both, where the mode check that used to answer this fell through the
605
+ // switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
606
+ // The second came back `unknown`, and every generator emitted a schema accepting anything.
607
+ //
608
+ // `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
609
+ // column identically. `dbType` is read in exactly one place outside this file,
610
+ // `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
611
+ // relabel reaches no output. Measured rather than argued: emitting a `Date` column under
612
+ // both labels, nullable and not, through all five generators gives ten byte-identical pairs.
613
+ case "SQLiteTimestamp":
614
+ return { tsType: "Date", dbType: "DATE" };
555
615
  case "SQLiteText":
556
616
  return { tsType: "string", dbType: "TEXT" };
557
617
  case "SQLiteReal":
558
618
  return { tsType: "number", dbType: "REAL" };
619
+ // No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
620
+ // `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
621
+ // all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
622
+ // produces, which is why a real `blob()` reached neither it nor anything else.
559
623
  case "SQLiteBlob":
560
624
  return { tsType: "Uint8Array", dbType: "BLOB" };
625
+ // The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
626
+ // for the measurement; the answers here are v1's own for the same column, so this is the
627
+ // two majors agreeing rather than a new opinion.
628
+ case "SQLiteBlobBuffer":
629
+ return { tsType: "Buffer", dbType: "BYTEA" };
561
630
  // SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
562
631
  // is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
563
632
  // wider than the `any` a json column at least used to get.
@@ -784,7 +853,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
784
853
  if (/^Gel/i.test(ctor)) {
785
854
  if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
786
855
  if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
787
- if (/Real|DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "NUMERIC" };
856
+ if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
857
+ if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
788
858
  if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
789
859
  if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
790
860
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
@@ -835,11 +905,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
835
905
  const v1 = describeV1Column(col);
836
906
  const constraints = this.columnConstraints(col);
837
907
  if (v1?.shape) delete constraints.maxLength;
838
- const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
908
+ const sqlKind = String(
909
+ outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
910
+ );
839
911
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
840
912
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
841
913
  const ctorName = String(col?.constructor?.name ?? "");
842
- const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
914
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : BUFFER_CLASSES.has(ctorName) ? { kind: "buffer" } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
843
915
  if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
844
916
  const shape = (v1?.shape ?? fallbackShape)?.kind;
845
917
  const finalTs = v1?.tsType ?? tsType;
@@ -1202,11 +1274,29 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1202
1274
  SQLiteReal: null,
1203
1275
  SingleStoreDouble: null,
1204
1276
  SingleStoreReal: null,
1205
- // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1206
- // `describeV1Column`. This one is about what a JS number can carry rather than about the
1207
- // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1208
- PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1277
+ // Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
1278
+ // used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
1279
+ // else at all, so a `real` column accepted 1e300 and the server refused it.
1280
+ //
1281
+ // Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
1282
+ // through the `gel` client, casting each literal so the server parses it, and again through a
1283
+ // stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
1284
+ //
1285
+ // 3.4028234663852886e38 accepted, returned unchanged
1286
+ // 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
1287
+ // 3.402823567797337e38 refused, "is out of range for type std::float32"
1288
+ // 1e300 refused, the same way
1289
+ //
1290
+ // The same value accepted, the same next double up refused, and the same rounding down of the
1291
+ // midpoint, so it takes the constant already here rather than a second name for one number.
1292
+ // float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
1293
+ // truthful finite bound.
1294
+ GelReal: PG_FLOAT4_RANGE,
1295
+ GelDoublePrecision: null
1209
1296
  };
1297
+ // `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
1298
+ // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1299
+ // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1210
1300
  /**
1211
1301
  * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1212
1302
  *
@@ -1217,17 +1307,25 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1217
1307
  * table also answers for a v1 column and the two answers are identical rather than merely
1218
1308
  * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1219
1309
  *
1220
- * Postgres only. No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a
1221
- * `float`/`double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while
1222
- * silently turning `NaN` into NULL, which is a different answer that has to arrive whole.
1310
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
1311
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
1312
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
1313
+ *
1314
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
1315
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
1316
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
1317
+ * and again through a stored property. Without them every row of such a column failed validation.
1223
1318
  *
1224
1319
  * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
1225
- * `Infinity`. `PgNumericNumber` states `infinity: false` on purpose; see `Column.allowsNaN`.
1320
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
1321
+ * any width and an infinity only where no precision is declared, which is a per-column question
1322
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
1226
1323
  */
1227
1324
  _SchemaAnalyzer.PG_NON_FINITE = {
1228
1325
  PgReal: { nan: true, infinity: true },
1229
1326
  PgDoublePrecision: { nan: true, infinity: true },
1230
- PgNumericNumber: { nan: true, infinity: false }
1327
+ GelReal: { nan: true, infinity: true },
1328
+ GelDoublePrecision: { nan: true, infinity: true }
1231
1329
  };
1232
1330
  var SchemaAnalyzer = _SchemaAnalyzer;
1233
1331
  var index_default = SchemaAnalyzer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.17.4",
3
+ "version": "1.17.6",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",