@drzl/analyzer 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  SchemaAnalyzer: () => SchemaAnalyzer,
34
34
  default: () => index_default,
35
35
  describeV1Column: () => describeV1Column,
36
+ isReadOnlyRelation: () => isReadOnlyRelation,
36
37
  isRelationsV2: () => isRelationsV2,
37
38
  readRelationsV2: () => readRelationsV2
38
39
  });
@@ -214,6 +215,15 @@ function describeV1Column(column) {
214
215
  if (typeof dims === "number" && dims >= 1) out.arrayDimensions = dims;
215
216
  return out;
216
217
  }
218
+ function unwrapArrayColumn(column) {
219
+ let element = column;
220
+ let dimensions = 0;
221
+ while (element?.baseColumn && String(element?.constructor?.name ?? "").endsWith("Array")) {
222
+ element = element.baseColumn;
223
+ dimensions++;
224
+ }
225
+ return { element, dimensions };
226
+ }
217
227
  function declaredLength(column) {
218
228
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
219
229
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
@@ -228,6 +238,12 @@ function getSymbolOf(target, key) {
228
238
  }
229
239
  return target[Symbol.for(key)];
230
240
  }
241
+ function isReadOnlyRelation(val) {
242
+ if (!val || typeof val !== "object") return false;
243
+ return Object.getOwnPropertySymbols(val).some(
244
+ (sym) => String(sym.description).includes("MaterializedViewConfig")
245
+ );
246
+ }
231
247
  function isRelationsV2(val) {
232
248
  if (!val || typeof val !== "object" || Array.isArray(val)) return false;
233
249
  const entries = Object.values(val);
@@ -508,10 +524,23 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
508
524
  return { tsType: "number", dbType: "REAL" };
509
525
  case "SQLiteBlob":
510
526
  return { tsType: "Uint8Array", dbType: "BLOB" };
527
+ // SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
528
+ // is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
529
+ // wider than the `any` a json column at least used to get.
530
+ case "SQLiteTextJson":
531
+ case "SQLiteBlobJson":
532
+ return { tsType: "any", dbType: "JSON" };
533
+ case "SQLiteBigInt":
534
+ return { tsType: "bigint", dbType: "BIGINT" };
511
535
  case "SQLiteNumeric":
512
536
  return { tsType: "string", dbType: "NUMERIC" };
513
537
  case "SQLiteBoolean":
514
538
  return { tsType: "boolean", dbType: "INTEGER" };
539
+ // 0.4x gives an enum its own class, which had no arm here at all, so an enum column came
540
+ // back `unknown` and every generator emitted a schema that accepted anything. The values
541
+ // were on the column the whole time, in `enumValues`, waiting for a type to attach to.
542
+ case "PgEnumColumn":
543
+ return { tsType: "string", dbType: "TEXT" };
515
544
  case "PgInteger":
516
545
  case "PgSmallInt":
517
546
  return { tsType: "number", dbType: "INTEGER" };
@@ -643,25 +672,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
643
672
  const foreignKeys = [];
644
673
  const pkCols = [];
645
674
  const uniqueGroups = /* @__PURE__ */ new Map();
646
- for (const [colName, col] of Object.entries(columnsObj)) {
675
+ for (const [colName, outerCol] of Object.entries(columnsObj)) {
676
+ const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
647
677
  let { tsType, dbType } = this.mapColumnType(col);
648
678
  if (tsType === "unknown" && /At$/.test(colName)) {
649
679
  tsType = "Date";
650
680
  dbType = "INTEGER";
651
681
  }
652
682
  const ev = col?.enumValues;
653
- const nullable = !col?.notNull && !col?.config?.notNull;
654
- const rawDefault = col?.default;
683
+ const nullable = !outerCol?.notNull && !outerCol?.config?.notNull;
684
+ const rawDefault = outerCol?.default;
655
685
  const defaultValue = rawDefault !== void 0 && !(rawDefault && typeof rawDefault === "object" && "queryChunks" in rawDefault) ? rawDefault : void 0;
656
- const generatedIdentity = col?.generatedIdentity;
657
- const isGenerated = !!(col?.generated || generatedIdentity?.type === "always" || col?.isGenerated);
658
- const hasDefault = col?.hasDefault === true || col?.default !== void 0 || col?.config?.default !== void 0 || col?.defaultFn !== void 0 || isGenerated;
686
+ const generatedIdentity = outerCol?.generatedIdentity;
687
+ const isGenerated = !!(outerCol?.generated || generatedIdentity?.type === "always" || outerCol?.isGenerated);
688
+ const hasDefault = outerCol?.hasDefault === true || outerCol?.default !== void 0 || outerCol?.config?.default !== void 0 || outerCol?.defaultFn !== void 0 || isGenerated;
659
689
  const references = void 0;
660
- const isUnique = !!(col?.isUnique || col?.config?.isUnique);
661
- const isPk = !!(col?.primary || col?.config?.primaryKey);
690
+ const isUnique = !!(outerCol?.isUnique || outerCol?.config?.isUnique);
691
+ const isPk = !!(outerCol?.primary || outerCol?.config?.primaryKey);
662
692
  if (isPk) pkCols.push(colName);
663
693
  if (isUnique) unique.push({ columns: [colName] });
664
- const uName = col?.uniqueName || col?.config?.uniqueName;
694
+ const uName = outerCol?.uniqueName || outerCol?.config?.uniqueName;
665
695
  if (uName) {
666
696
  const arr = uniqueGroups.get(uName) ?? [];
667
697
  arr.push(colName);
@@ -670,6 +700,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
670
700
  const v1 = describeV1Column(col);
671
701
  const constraints = this.columnConstraints(col);
672
702
  if (v1?.shape) delete constraints.maxLength;
703
+ const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
704
+ const shape = (v1?.shape ?? jsonShape)?.kind;
705
+ const finalTs = v1?.tsType ?? tsType;
706
+ const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
707
+ if (wide) {
708
+ const sqlType = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
709
+ issues.push({
710
+ code: "DRZL_ANL_UNKNOWN_COLUMN",
711
+ level: "warn",
712
+ message: `Column "${colName}" on table "${tsName}" has no known type${sqlType ? ` (SQL type ${sqlType})` : ""}, so its validator will accept any value.`,
713
+ hint: shape === "custom" ? "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type." : "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns."
714
+ });
715
+ }
673
716
  columns.push({
674
717
  name: colName,
675
718
  tsType,
@@ -682,7 +725,12 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
682
725
  enumValues: Array.isArray(ev) ? ev : void 0,
683
726
  ...defaultValue !== void 0 ? { defaultValue } : {},
684
727
  ...constraints,
685
- ...v1 ?? {}
728
+ ...v1 ?? {},
729
+ // After the v1 spread, which sets its own `arrayDimensions` from `dimensions`. On 0.4x
730
+ // that spread has nothing to say and this is the only source.
731
+ ...arrayDims ? { arrayDimensions: arrayDims } : {},
732
+ // Only where v1 did not already describe the value, so a shaped column keeps its shape.
733
+ ...jsonShape && !v1?.shape ? { shape: jsonShape } : {}
686
734
  });
687
735
  }
688
736
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
@@ -757,6 +805,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
757
805
  indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
758
806
  checks,
759
807
  foreignKeys,
808
+ // A materialized view refuses every write, so the generators skip its insert and update
809
+ // schemas rather than describe an operation the database will always reject.
810
+ ...isReadOnlyRelation(tbl) ? { readOnly: true } : {},
760
811
  meta: {}
761
812
  };
762
813
  }
@@ -957,6 +1008,7 @@ var index_default = SchemaAnalyzer;
957
1008
  0 && (module.exports = {
958
1009
  SchemaAnalyzer,
959
1010
  describeV1Column,
1011
+ isReadOnlyRelation,
960
1012
  isRelationsV2,
961
1013
  readRelationsV2
962
1014
  });
package/dist/index.d.cts CHANGED
@@ -182,6 +182,12 @@ interface Table {
182
182
  indexes: Index[];
183
183
  checks?: Check[];
184
184
  foreignKeys?: ForeignKey[];
185
+ /**
186
+ * Set when the relation refuses writes, which today means a materialized view. Insert and
187
+ * update schemas are not emitted for one, because the database will always refuse the
188
+ * operation they describe.
189
+ */
190
+ readOnly?: boolean;
185
191
  meta?: Record<string, unknown>;
186
192
  }
187
193
  interface Enum {
@@ -223,6 +229,18 @@ declare function describeV1Column(column: any): Partial<Column> | null;
223
229
  * `relations` record whose entries carry a `relationType`. That is specific enough not to
224
230
  * collide with a table or an enum, both of which are checked before this.
225
231
  */
232
+ /**
233
+ * Whether a relation refuses writes outright.
234
+ *
235
+ * A materialized view does: `INSERT INTO mv ...` fails with `cannot change materialized view`,
236
+ * verified against Postgres. An insert or update schema for one describes an operation the
237
+ * database will always refuse.
238
+ *
239
+ * An ordinary view is deliberately not included. Postgres accepts an INSERT into a simple
240
+ * auto-updatable view, and whether a given view qualifies depends on its query rather than on
241
+ * anything the schema file states, so refusing them all would take away something that works.
242
+ */
243
+ declare function isReadOnlyRelation(val: any): boolean;
226
244
  declare function isRelationsV2(val: any): boolean;
227
245
  /**
228
246
  * Read the relations declared by `defineRelations`.
@@ -338,4 +356,4 @@ declare class SchemaAnalyzer {
338
356
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
339
357
  }
340
358
 
341
- export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default, describeV1Column, isRelationsV2, readRelationsV2 };
359
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default, describeV1Column, isReadOnlyRelation, isRelationsV2, readRelationsV2 };
package/dist/index.d.ts CHANGED
@@ -182,6 +182,12 @@ interface Table {
182
182
  indexes: Index[];
183
183
  checks?: Check[];
184
184
  foreignKeys?: ForeignKey[];
185
+ /**
186
+ * Set when the relation refuses writes, which today means a materialized view. Insert and
187
+ * update schemas are not emitted for one, because the database will always refuse the
188
+ * operation they describe.
189
+ */
190
+ readOnly?: boolean;
185
191
  meta?: Record<string, unknown>;
186
192
  }
187
193
  interface Enum {
@@ -223,6 +229,18 @@ declare function describeV1Column(column: any): Partial<Column> | null;
223
229
  * `relations` record whose entries carry a `relationType`. That is specific enough not to
224
230
  * collide with a table or an enum, both of which are checked before this.
225
231
  */
232
+ /**
233
+ * Whether a relation refuses writes outright.
234
+ *
235
+ * A materialized view does: `INSERT INTO mv ...` fails with `cannot change materialized view`,
236
+ * verified against Postgres. An insert or update schema for one describes an operation the
237
+ * database will always refuse.
238
+ *
239
+ * An ordinary view is deliberately not included. Postgres accepts an INSERT into a simple
240
+ * auto-updatable view, and whether a given view qualifies depends on its query rather than on
241
+ * anything the schema file states, so refusing them all would take away something that works.
242
+ */
243
+ declare function isReadOnlyRelation(val: any): boolean;
226
244
  declare function isRelationsV2(val: any): boolean;
227
245
  /**
228
246
  * Read the relations declared by `defineRelations`.
@@ -338,4 +356,4 @@ declare class SchemaAnalyzer {
338
356
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
339
357
  }
340
358
 
341
- export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default, describeV1Column, isRelationsV2, readRelationsV2 };
359
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default, describeV1Column, isReadOnlyRelation, isRelationsV2, readRelationsV2 };
package/dist/index.js CHANGED
@@ -175,6 +175,15 @@ function describeV1Column(column) {
175
175
  if (typeof dims === "number" && dims >= 1) out.arrayDimensions = dims;
176
176
  return out;
177
177
  }
178
+ function unwrapArrayColumn(column) {
179
+ let element = column;
180
+ let dimensions = 0;
181
+ while (element?.baseColumn && String(element?.constructor?.name ?? "").endsWith("Array")) {
182
+ element = element.baseColumn;
183
+ dimensions++;
184
+ }
185
+ return { element, dimensions };
186
+ }
178
187
  function declaredLength(column) {
179
188
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
180
189
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
@@ -189,6 +198,12 @@ function getSymbolOf(target, key) {
189
198
  }
190
199
  return target[Symbol.for(key)];
191
200
  }
201
+ function isReadOnlyRelation(val) {
202
+ if (!val || typeof val !== "object") return false;
203
+ return Object.getOwnPropertySymbols(val).some(
204
+ (sym) => String(sym.description).includes("MaterializedViewConfig")
205
+ );
206
+ }
192
207
  function isRelationsV2(val) {
193
208
  if (!val || typeof val !== "object" || Array.isArray(val)) return false;
194
209
  const entries = Object.values(val);
@@ -469,10 +484,23 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
469
484
  return { tsType: "number", dbType: "REAL" };
470
485
  case "SQLiteBlob":
471
486
  return { tsType: "Uint8Array", dbType: "BLOB" };
487
+ // SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
488
+ // is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
489
+ // wider than the `any` a json column at least used to get.
490
+ case "SQLiteTextJson":
491
+ case "SQLiteBlobJson":
492
+ return { tsType: "any", dbType: "JSON" };
493
+ case "SQLiteBigInt":
494
+ return { tsType: "bigint", dbType: "BIGINT" };
472
495
  case "SQLiteNumeric":
473
496
  return { tsType: "string", dbType: "NUMERIC" };
474
497
  case "SQLiteBoolean":
475
498
  return { tsType: "boolean", dbType: "INTEGER" };
499
+ // 0.4x gives an enum its own class, which had no arm here at all, so an enum column came
500
+ // back `unknown` and every generator emitted a schema that accepted anything. The values
501
+ // were on the column the whole time, in `enumValues`, waiting for a type to attach to.
502
+ case "PgEnumColumn":
503
+ return { tsType: "string", dbType: "TEXT" };
476
504
  case "PgInteger":
477
505
  case "PgSmallInt":
478
506
  return { tsType: "number", dbType: "INTEGER" };
@@ -604,25 +632,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
604
632
  const foreignKeys = [];
605
633
  const pkCols = [];
606
634
  const uniqueGroups = /* @__PURE__ */ new Map();
607
- for (const [colName, col] of Object.entries(columnsObj)) {
635
+ for (const [colName, outerCol] of Object.entries(columnsObj)) {
636
+ const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
608
637
  let { tsType, dbType } = this.mapColumnType(col);
609
638
  if (tsType === "unknown" && /At$/.test(colName)) {
610
639
  tsType = "Date";
611
640
  dbType = "INTEGER";
612
641
  }
613
642
  const ev = col?.enumValues;
614
- const nullable = !col?.notNull && !col?.config?.notNull;
615
- const rawDefault = col?.default;
643
+ const nullable = !outerCol?.notNull && !outerCol?.config?.notNull;
644
+ const rawDefault = outerCol?.default;
616
645
  const defaultValue = rawDefault !== void 0 && !(rawDefault && typeof rawDefault === "object" && "queryChunks" in rawDefault) ? rawDefault : void 0;
617
- const generatedIdentity = col?.generatedIdentity;
618
- const isGenerated = !!(col?.generated || generatedIdentity?.type === "always" || col?.isGenerated);
619
- const hasDefault = col?.hasDefault === true || col?.default !== void 0 || col?.config?.default !== void 0 || col?.defaultFn !== void 0 || isGenerated;
646
+ const generatedIdentity = outerCol?.generatedIdentity;
647
+ const isGenerated = !!(outerCol?.generated || generatedIdentity?.type === "always" || outerCol?.isGenerated);
648
+ const hasDefault = outerCol?.hasDefault === true || outerCol?.default !== void 0 || outerCol?.config?.default !== void 0 || outerCol?.defaultFn !== void 0 || isGenerated;
620
649
  const references = void 0;
621
- const isUnique = !!(col?.isUnique || col?.config?.isUnique);
622
- const isPk = !!(col?.primary || col?.config?.primaryKey);
650
+ const isUnique = !!(outerCol?.isUnique || outerCol?.config?.isUnique);
651
+ const isPk = !!(outerCol?.primary || outerCol?.config?.primaryKey);
623
652
  if (isPk) pkCols.push(colName);
624
653
  if (isUnique) unique.push({ columns: [colName] });
625
- const uName = col?.uniqueName || col?.config?.uniqueName;
654
+ const uName = outerCol?.uniqueName || outerCol?.config?.uniqueName;
626
655
  if (uName) {
627
656
  const arr = uniqueGroups.get(uName) ?? [];
628
657
  arr.push(colName);
@@ -631,6 +660,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
631
660
  const v1 = describeV1Column(col);
632
661
  const constraints = this.columnConstraints(col);
633
662
  if (v1?.shape) delete constraints.maxLength;
663
+ const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
664
+ const shape = (v1?.shape ?? jsonShape)?.kind;
665
+ const finalTs = v1?.tsType ?? tsType;
666
+ const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
667
+ if (wide) {
668
+ const sqlType = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
669
+ issues.push({
670
+ code: "DRZL_ANL_UNKNOWN_COLUMN",
671
+ level: "warn",
672
+ message: `Column "${colName}" on table "${tsName}" has no known type${sqlType ? ` (SQL type ${sqlType})` : ""}, so its validator will accept any value.`,
673
+ hint: shape === "custom" ? "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type." : "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns."
674
+ });
675
+ }
634
676
  columns.push({
635
677
  name: colName,
636
678
  tsType,
@@ -643,7 +685,12 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
643
685
  enumValues: Array.isArray(ev) ? ev : void 0,
644
686
  ...defaultValue !== void 0 ? { defaultValue } : {},
645
687
  ...constraints,
646
- ...v1 ?? {}
688
+ ...v1 ?? {},
689
+ // After the v1 spread, which sets its own `arrayDimensions` from `dimensions`. On 0.4x
690
+ // that spread has nothing to say and this is the only source.
691
+ ...arrayDims ? { arrayDimensions: arrayDims } : {},
692
+ // Only where v1 did not already describe the value, so a shaped column keeps its shape.
693
+ ...jsonShape && !v1?.shape ? { shape: jsonShape } : {}
647
694
  });
648
695
  }
649
696
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
@@ -718,6 +765,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
718
765
  indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
719
766
  checks,
720
767
  foreignKeys,
768
+ // A materialized view refuses every write, so the generators skip its insert and update
769
+ // schemas rather than describe an operation the database will always reject.
770
+ ...isReadOnlyRelation(tbl) ? { readOnly: true } : {},
721
771
  meta: {}
722
772
  };
723
773
  }
@@ -918,6 +968,7 @@ export {
918
968
  SchemaAnalyzer,
919
969
  index_default as default,
920
970
  describeV1Column,
971
+ isReadOnlyRelation,
921
972
  isRelationsV2,
922
973
  readRelationsV2
923
974
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -45,6 +45,6 @@
45
45
  "scripts": {
46
46
  "build": "tsup src/index.ts --dts --format esm,cjs",
47
47
  "lint": "eslint . --ext .ts",
48
- "test": "vitest run"
48
+ "test": "vitest run --testTimeout=20000"
49
49
  }
50
50
  }