@metaobjectsdev/migrate-ts 0.14.1 → 0.15.0-rc.1

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.
@@ -40,6 +40,8 @@ import {
40
40
  FIELD_SUBTYPE_OBJECT,
41
41
  FIELD_SUBTYPE_MAP,
42
42
  FIELD_SUBTYPE_UUID,
43
+ FIELD_SUBTYPE_URI,
44
+ FIELD_SUBTYPE_INET,
43
45
  FIELD_SUBTYPE_ENUM,
44
46
  FIELD_ATTR_VALUES,
45
47
  FIELD_ATTR_OBJECT_REF,
@@ -47,9 +49,7 @@ import {
47
49
  FIELD_ATTR_DB_COLUMN_TYPE,
48
50
  DB_COLUMN_TYPE_UUID,
49
51
  DB_COLUMN_TYPE_JSONB,
50
- DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ,
51
- DB_COLUMN_TYPE_UUID_ARRAY,
52
- DB_COLUMN_TYPE_TEXT_ARRAY,
52
+ FIELD_ATTR_LOCAL_TIME,
53
53
  STORAGE_FLATTENED,
54
54
  DOC_ATTR_DESCRIPTION,
55
55
  applyColumnNamingStrategy, DEFAULT_COLUMN_NAMING_STRATEGY,
@@ -110,17 +110,19 @@ export function buildExpectedSchema(
110
110
  // - projections (read-only @kind source with no writable peer — handled by
111
111
  // the view-diff pipeline, not the table diff)
112
112
  const entities: { entity: MetaObject; tableName: string }[] = [];
113
- for (const child of root.ownChildren()) {
113
+ // ADR-0039: effective children — resolve rather than rely on root being unextended.
114
+ for (const child of root.children()) {
114
115
  if (child.type !== TYPE_OBJECT) continue;
115
116
  if (child.isAbstract) continue;
116
117
  if (child.subType === "value") continue;
117
118
  // FR-017 TPH: a subtype shares its discriminator base's single table, so it
118
119
  // emits no table of its own. Its own columns are folded into the base below.
119
120
  if (isTphSubtype(child)) continue;
120
- const hasReadOnlySource = child.ownChildren().some(
121
+ // ADR-0039: effective children — an entity may inherit its source.rdb via extends.
122
+ const hasReadOnlySource = child.children().some(
121
123
  (c) => c instanceof MetaSource && c.isReadOnly(),
122
124
  );
123
- const hasWritableSource = child.ownChildren().some(
125
+ const hasWritableSource = child.children().some(
124
126
  (c) => c instanceof MetaSource && c.isWritable(),
125
127
  );
126
128
  // Projection: read-only and not write-through.
@@ -223,6 +225,8 @@ function normalizeForSqlite(sqlType: SqlType): SqlType {
223
225
  function discriminatorBaseOf(entity: MetaData): MetaData | undefined {
224
226
  let a = entity.superResolved;
225
227
  while (a !== undefined) {
228
+ // ADR-0039 category 3: super-resolution walk — read each ancestor's OWN
229
+ // @discriminator as we ascend (this manual walk IS the resolution).
226
230
  if (a.ownAttr(OBJECT_ATTR_DISCRIMINATOR) !== undefined) return a;
227
231
  a = a.superResolved;
228
232
  }
@@ -233,19 +237,23 @@ function discriminatorBaseOf(entity: MetaData): MetaData | undefined {
233
237
  * discriminator-bearing ancestor). Such an entity emits no table of its own. */
234
238
  function isTphSubtype(entity: MetaData): boolean {
235
239
  return (
236
- entity.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) !== undefined &&
240
+ // ADR-0039: effective attr — @discriminatorValue may be inherited (sub-subtype).
241
+ entity.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) !== undefined &&
237
242
  discriminatorBaseOf(entity) !== undefined
238
243
  );
239
244
  }
240
245
 
241
246
  /** Concrete TPH subtypes whose discriminator base is `base` (root-level scan). */
242
247
  function tphConcreteSubtypes(base: MetaObject, root: MetaData): MetaObject[] {
243
- if (base.ownAttr(OBJECT_ATTR_DISCRIMINATOR) === undefined) return [];
244
- return root.ownChildren().filter(
248
+ // ADR-0039: effective attr @discriminator may be inherited.
249
+ if (base.attr(OBJECT_ATTR_DISCRIMINATOR) === undefined) return [];
250
+ // ADR-0039: effective children — resolve rather than rely on root being unextended.
251
+ return root.children().filter(
245
252
  (c): c is MetaObject =>
246
253
  c.type === TYPE_OBJECT &&
247
254
  !c.isAbstract &&
248
- c.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) !== undefined &&
255
+ // ADR-0039: effective attr — @discriminatorValue may be inherited (sub-subtype).
256
+ c.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) !== undefined &&
249
257
  discriminatorBaseOf(c) === base,
250
258
  );
251
259
  }
@@ -264,7 +272,8 @@ function buildTable(
264
272
 
265
273
  const pkJsNames = pkIdentity ? readIdentityFields(pkIdentity) : [];
266
274
  const pkGeneration = pkIdentity
267
- ? (pkIdentity.ownAttr(IDENTITY_ATTR_GENERATION) as string | undefined)
275
+ // ADR-0039: effective attr @generation may be inherited via the identity's extends.
276
+ ? (pkIdentity.attr(IDENTITY_ATTR_GENERATION) as string | undefined)
268
277
  : undefined;
269
278
 
270
279
  const primaryKey = pkJsNames.map((jsName) => {
@@ -277,7 +286,8 @@ function buildTable(
277
286
  const isPk = pkJsNames.includes(field.name);
278
287
  if (
279
288
  field.subType === FIELD_SUBTYPE_OBJECT &&
280
- field.ownAttr(FIELD_ATTR_STORAGE) === STORAGE_FLATTENED
289
+ // ADR-0039: resolving — @storage may be inherited via extends.
290
+ field.attr(FIELD_ATTR_STORAGE) === STORAGE_FLATTENED
281
291
  ) {
282
292
  // Flattened storage: expand nested value-object fields as prefixed columns.
283
293
  // The parent field.object itself does NOT produce its own column.
@@ -291,9 +301,12 @@ function buildTable(
291
301
  // OWN fields into the single table as NULLABLE columns (a row of any other
292
302
  // subtype stores NULL there), even when the field is @required. Dedupe by
293
303
  // column name so an inherited/overridden base column is not re-emitted.
294
- if (entity.ownAttr(OBJECT_ATTR_DISCRIMINATOR) !== undefined) {
304
+ // ADR-0039: effective attr — @discriminator may be inherited (deep TPH base).
305
+ if (entity.attr(OBJECT_ATTR_DISCRIMINATOR) !== undefined) {
295
306
  const existing = new Set(columns.map((c) => c.name));
296
307
  for (const sub of tphConcreteSubtypes(entity, root)) {
308
+ // ADR-0039 category 1: emit-declared-here — fold ONLY the subtype's OWN
309
+ // fields (inherited base fields are already emitted on the base table).
297
310
  for (const field of sub.ownChildren()) {
298
311
  if (field.type !== TYPE_FIELD) continue;
299
312
  const col = buildColumn(field, false, undefined, strategy);
@@ -339,7 +352,8 @@ function buildSecondaryIndexes(
339
352
  // column has `.unique()`. We mirror them in the expected schema so the diff
340
353
  // doesn't see them as drop-only on the actual side.
341
354
  for (const field of entity.fields()) {
342
- if (field.ownAttr(FIELD_ATTR_UNIQUE) !== true) continue;
355
+ // ADR-0039: resolving @unique may be inherited via extends.
356
+ if (field.attr(FIELD_ATTR_UNIQUE) !== true) continue;
343
357
  const colName = resolveColumnName(field, strategy);
344
358
  indexes.push({
345
359
  name: `${tableName}_${colName}_unique`,
@@ -352,7 +366,8 @@ function buildSecondaryIndexes(
352
366
  // Drizzle emits the index using the identity's @name attr directly (no table
353
367
  // prefix), so the expected name must match.
354
368
  for (const identity of entity.secondaryIdentities()) {
355
- const exprRaw = identity.ownAttr(IDENTITY_ATTR_EXPR);
369
+ // ADR-0039: effective attrs — a secondary identity's attrs may be inherited via extends.
370
+ const exprRaw = identity.attr(IDENTITY_ATTR_EXPR);
356
371
  const expr = typeof exprRaw === "string" && exprRaw.trim().length > 0 ? exprRaw.trim() : undefined;
357
372
  const fieldNames = readIdentityFields(identity);
358
373
  // An expression index keys off @expr (not @fields); a plain index needs @fields.
@@ -361,21 +376,21 @@ function buildSecondaryIndexes(
361
376
  const field = findField(entity, jsName);
362
377
  return field ? resolveColumnName(field, strategy) : applyColumnNamingStrategy(jsName, strategy);
363
378
  });
364
- const uniqueAttr = identity.ownAttr(IDENTITY_ATTR_UNIQUE);
379
+ const uniqueAttr = identity.attr(IDENTITY_ATTR_UNIQUE);
365
380
  const index: IndexDescriptor = {
366
381
  name: identity.name,
367
382
  columns: expr ? [] : cols,
368
383
  unique: uniqueAttr !== false,
369
384
  };
370
385
  if (expr) index.expr = expr;
371
- const usingRaw = identity.ownAttr(IDENTITY_ATTR_USING);
386
+ const usingRaw = identity.attr(IDENTITY_ATTR_USING);
372
387
  if (typeof usingRaw === "string" && usingRaw.trim().length > 0 && usingRaw.trim() !== "btree") {
373
388
  index.using = usingRaw.trim();
374
389
  }
375
390
  // @orders — per-field sort direction (positional to @fields). Only attach when
376
391
  // at least one field is descending (an all-ascending array is the default and
377
392
  // must serialize identically to "no orders" for diff stability).
378
- const ordersRaw = identity.ownAttr(IDENTITY_ATTR_ORDERS);
393
+ const ordersRaw = identity.attr(IDENTITY_ATTR_ORDERS);
379
394
  if (Array.isArray(ordersRaw)) {
380
395
  const orders = cols.map((_, i) => (ordersRaw[i] === "desc" ? "desc" : "asc")) as (
381
396
  "asc" | "desc"
@@ -383,7 +398,7 @@ function buildSecondaryIndexes(
383
398
  if (orders.some((o) => o === "desc")) index.orders = orders;
384
399
  }
385
400
  // @where — partial-index predicate.
386
- const whereRaw = identity.ownAttr(IDENTITY_ATTR_WHERE);
401
+ const whereRaw = identity.attr(IDENTITY_ATTR_WHERE);
387
402
  if (typeof whereRaw === "string" && whereRaw.trim().length > 0) {
388
403
  index.where = whereRaw.trim();
389
404
  }
@@ -430,7 +445,8 @@ function validatorCheck(
430
445
  case VALIDATOR_SUBTYPE_REGEX: {
431
446
  // Postgres-only: SQLite has no native regex operator.
432
447
  if (dialect === "sqlite" || dialect === "d1") return null;
433
- const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
448
+ // ADR-0039: effective attr — @pattern may be inherited via the validator's extends.
449
+ const pattern = v.attr(VALIDATOR_ATTR_PATTERN);
434
450
  if (typeof pattern !== "string" || pattern.length === 0) return null;
435
451
  return {
436
452
  name: `${tableName}_${col}_regex_chk`,
@@ -531,18 +547,20 @@ function crossFieldCheck(
531
547
  ): CheckDescriptor | null {
532
548
  switch (v.subType) {
533
549
  case VALIDATOR_SUBTYPE_COMPARISON: {
534
- const left = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_LEFT), strategy);
535
- const right = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_RIGHT), strategy);
536
- const op = COMPARISON_SQL_OP[String(v.ownAttr(VALIDATOR_ATTR_OP))];
550
+ // ADR-0039: effective attrs — cross-field validator attrs may be inherited via extends.
551
+ const left = resolveRef(entity, v.attr(VALIDATOR_ATTR_LEFT), strategy);
552
+ const right = resolveRef(entity, v.attr(VALIDATOR_ATTR_RIGHT), strategy);
553
+ const op = COMPARISON_SQL_OP[String(v.attr(VALIDATOR_ATTR_OP))];
537
554
  if (!left || !right || !op) return null;
538
555
  const lc = resolveColumnName(left.field, strategy);
539
556
  return { name: `${tableName}_${lc}_cmp_chk`, expression: `${left.qcol} ${op} ${right.qcol}` };
540
557
  }
541
558
  case VALIDATOR_SUBTYPE_REQUIRED_WHEN: {
542
- const target = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_FIELD), strategy);
543
- const when = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_WHEN), strategy);
559
+ // ADR-0039: effective attrs — cross-field validator attrs may be inherited via extends.
560
+ const target = resolveRef(entity, v.attr(VALIDATOR_ATTR_FIELD), strategy);
561
+ const when = resolveRef(entity, v.attr(VALIDATOR_ATTR_WHEN), strategy);
544
562
  if (!target || !when) return null;
545
- const lit = renderEquals(v.ownAttr(VALIDATOR_ATTR_EQUALS), when.field, dialect);
563
+ const lit = renderEquals(v.attr(VALIDATOR_ATTR_EQUALS), when.field, dialect);
546
564
  const fc = resolveColumnName(target.field, strategy);
547
565
  return {
548
566
  name: `${tableName}_${fc}_reqwhen_chk`,
@@ -550,10 +568,11 @@ function crossFieldCheck(
550
568
  };
551
569
  }
552
570
  case VALIDATOR_SUBTYPE_PRESENT_IFF: {
553
- const target = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_FIELD), strategy);
554
- const when = resolveRef(entity, v.ownAttr(VALIDATOR_ATTR_WHEN), strategy);
571
+ // ADR-0039: effective attrs — cross-field validator attrs may be inherited via extends.
572
+ const target = resolveRef(entity, v.attr(VALIDATOR_ATTR_FIELD), strategy);
573
+ const when = resolveRef(entity, v.attr(VALIDATOR_ATTR_WHEN), strategy);
555
574
  if (!target || !when) return null;
556
- const lit = renderEquals(v.ownAttr(VALIDATOR_ATTR_EQUALS), when.field, dialect);
575
+ const lit = renderEquals(v.attr(VALIDATOR_ATTR_EQUALS), when.field, dialect);
557
576
  const fc = resolveColumnName(target.field, strategy);
558
577
  return {
559
578
  name: `${tableName}_${fc}_presentiff_chk`,
@@ -561,7 +580,8 @@ function crossFieldCheck(
561
580
  };
562
581
  }
563
582
  case VALIDATOR_SUBTYPE_AT_LEAST_ONE: {
564
- const raw = v.ownAttr(VALIDATOR_ATTR_FIELDS);
583
+ // ADR-0039: effective attr — @fields may be inherited via the validator's extends.
584
+ const raw = v.attr(VALIDATOR_ATTR_FIELDS);
565
585
  const names = Array.isArray(raw) ? raw : (typeof raw === "string" ? [raw] : []);
566
586
  const refs = names.map((n) => resolveRef(entity, n, strategy));
567
587
  if (refs.length === 0 || refs.some((r) => r === null)) return null;
@@ -611,7 +631,8 @@ function buildForeignKeys(
611
631
  const { onDelete, onUpdate } = resolveReferentialActions(entity, refChild);
612
632
  // An explicit @constraintName adopts an existing FK name (e.g. a database
613
633
  // created by another toolchain); absent → the auto-derived default.
614
- const constraintNameOverride = refChild.ownAttr(IDENTITY_ATTR_CONSTRAINT_NAME);
634
+ // ADR-0039: effective attr — @constraintName may be inherited via the identity's extends.
635
+ const constraintNameOverride = refChild.attr(IDENTITY_ATTR_CONSTRAINT_NAME);
615
636
  const constraintName =
616
637
  typeof constraintNameOverride === "string" && constraintNameOverride.length > 0
617
638
  ? constraintNameOverride
@@ -644,7 +665,8 @@ function buildForeignKeys(
644
665
  function flattenObjectField(
645
666
  field: MetaData, root: MetaRoot, strategy: ColumnNamingStrategy,
646
667
  ): ColumnDescriptor[] {
647
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
668
+ // ADR-0039: resolving — @objectRef may be inherited via extends.
669
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
648
670
  if (typeof ref !== "string" || ref.length === 0) return [];
649
671
  const targetObject = root.findObject(ref);
650
672
  if (targetObject === undefined) return [];
@@ -673,7 +695,8 @@ function buildColumn(
673
695
  ): ColumnDescriptor {
674
696
  // Both the @required attr and the validator.required child signal NOT NULL.
675
697
  const fieldIsRequired = isRequired(field);
676
- const defaultRaw = field.ownAttr(FIELD_ATTR_DEFAULT);
698
+ // ADR-0039: resolving — @default may be inherited via extends.
699
+ const defaultRaw = field.attr(FIELD_ATTR_DEFAULT);
677
700
 
678
701
  const col: ColumnDescriptor = {
679
702
  name: resolveColumnName(field, strategy),
@@ -692,7 +715,8 @@ function buildColumn(
692
715
  // NOT NULL timestamp populates without an explicit value. (The per-update
693
716
  // refresh for "onUpdate" is the ORM/trigger's job, not the column default.)
694
717
  // An explicit @default always wins, so this only applies when none was set.
695
- const autoSet = field.ownAttr(FIELD_ATTR_AUTO_SET);
718
+ // ADR-0039: resolving — @autoSet may be inherited via extends.
719
+ const autoSet = field.attr(FIELD_ATTR_AUTO_SET);
696
720
  if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
697
721
  col.default = { kind: "expr", value: "now()" };
698
722
  }
@@ -708,26 +732,60 @@ function buildColumn(
708
732
  return col;
709
733
  }
710
734
 
735
+ /**
736
+ * The native Postgres array ELEMENT SqlType for an `isArray` scalar field, or
737
+ * undefined when the subtype has no native-array form (object/map → single jsonb
738
+ * column; everything else falls through to the scalar subtype default).
739
+ *
740
+ * dbColumnType slim-and-derive Phase 1 wires the two derived cases the design calls
741
+ * out: `field.string` → `text[]`, `field.uuid` → `uuid[]`. This mirrors codegen-ts's
742
+ * column-mapper, which emits native `.array()` for the same scalar subtypes.
743
+ */
744
+ function arrayElementSqlType(field: MetaData): SqlType | undefined {
745
+ switch (field.subType) {
746
+ case FIELD_SUBTYPE_STRING: return { kind: "text" };
747
+ case FIELD_SUBTYPE_UUID: return { kind: "uuid" };
748
+ default: return undefined;
749
+ }
750
+ }
751
+
711
752
  function subtypeToSqlType(field: MetaData): SqlType {
712
753
  // R6 Plan 2b: a physical @dbColumnType override selects the DB column type
713
754
  // instead of the subtype default (the loader has already validated the
714
755
  // (subtype × value) pairing, so an unrecognized value never reaches here).
756
+ // dbColumnType slim-and-derive Phase 1: the array overrides (uuid_array /
757
+ // text_array) are RETIRED — native text[]/uuid[] are derived from `isArray`
758
+ // below, not declared here. ADR-0036 Wave 2: `timestamp_with_tz` is RETIRED
759
+ // too — field.timestamp is tz-aware by default + @localTime opts into naive
760
+ // (handled in the subtype switch below), not via this physical escape hatch.
761
+ // ADR-0039: @dbColumnType is the ONE deliberately own-only attr — a physical
762
+ // column-type override is never inherited (a logical field extending a base
763
+ // must not silently pick up the base's physical DB type). Keep ownAttr.
715
764
  const dbColumnType = field.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
716
765
  if (typeof dbColumnType === "string") {
717
766
  switch (dbColumnType) {
718
- case DB_COLUMN_TYPE_UUID: return { kind: "uuid" };
719
- case DB_COLUMN_TYPE_JSONB: return { kind: "json" };
720
- case DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ: return { kind: "timestamp", withTimezone: true };
721
- case DB_COLUMN_TYPE_UUID_ARRAY: return { kind: "array", element: { kind: "uuid" } };
722
- case DB_COLUMN_TYPE_TEXT_ARRAY: return { kind: "array", element: { kind: "text" } };
767
+ case DB_COLUMN_TYPE_UUID: return { kind: "uuid" };
768
+ case DB_COLUMN_TYPE_JSONB: return { kind: "json" };
723
769
  }
724
770
  }
725
771
 
772
+ // Native array columns are DERIVED from `isArray` (dbColumnType slim-and-derive
773
+ // Phase 1). Only scalar subtypes with a stable element SqlType get a native
774
+ // Postgres array (e.g. field.string → text[], field.uuid → uuid[]). field.object
775
+ // / field.map carry their array-ness inside a single jsonb column (no native
776
+ // array — handled by the subtype switch returning { kind: "json" }).
777
+ // ADR-0039: resolving — array-ness may be inherited via extends.
778
+ if (field.resolvedIsArray()) {
779
+ const element = arrayElementSqlType(field);
780
+ if (element !== undefined) return { kind: "array", element };
781
+ }
782
+
726
783
  const subType = field.subType;
727
784
  switch (subType) {
728
785
  case FIELD_SUBTYPE_STRING: {
729
786
  // @maxLength is declared as ATTR_SUBTYPE_INT so the loader coerces it to a number.
730
- const m = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
787
+ // ADR-0039: resolving — @maxLength may be inherited via extends.
788
+ const m = field.attr(FIELD_ATTR_MAX_LENGTH);
731
789
  return typeof m === "number" ? { kind: "text", maxLength: m } : { kind: "text" };
732
790
  }
733
791
  case FIELD_SUBTYPE_INT: return { kind: "integer", bits: 32 };
@@ -738,8 +796,9 @@ function subtypeToSqlType(field: MetaData): SqlType {
738
796
  case FIELD_SUBTYPE_DECIMAL: {
739
797
  // @precision/@scale are declared as ATTR_SUBTYPE_INT so the loader coerces them
740
798
  // to numbers. Both present → NUMERIC(p,s); absent → bare NUMERIC (back-compat).
741
- const precision = field.ownAttr(FIELD_ATTR_PRECISION);
742
- const scale = field.ownAttr(FIELD_ATTR_SCALE);
799
+ // ADR-0039: resolving — @precision / @scale may be inherited via extends.
800
+ const precision = field.attr(FIELD_ATTR_PRECISION);
801
+ const scale = field.attr(FIELD_ATTR_SCALE);
743
802
  if (typeof precision === "number" && typeof scale === "number") {
744
803
  return { kind: "numeric", precision, scale };
745
804
  }
@@ -751,10 +810,16 @@ function subtypeToSqlType(field: MetaData): SqlType {
751
810
  case FIELD_SUBTYPE_BOOLEAN: return { kind: "boolean" };
752
811
  case FIELD_SUBTYPE_DATE: return { kind: "date" };
753
812
  case FIELD_SUBTYPE_TIME: return { kind: "time" }; // Postgres native TIME (whole-second wire form)
754
- case FIELD_SUBTYPE_TIMESTAMP: return { kind: "timestamp", withTimezone: false };
813
+ case FIELD_SUBTYPE_TIMESTAMP:
814
+ // ADR-0036 Wave 2: instant / tz-aware BY DEFAULT (→ timestamptz). A naive
815
+ // wall-clock value opts out with @localTime:true (→ TIMESTAMP, no tz).
816
+ // ADR-0039: resolving — @localTime may be inherited via extends.
817
+ return { kind: "timestamp", withTimezone: field.attr(FIELD_ATTR_LOCAL_TIME) !== true };
755
818
  case FIELD_SUBTYPE_OBJECT:
756
819
  case FIELD_SUBTYPE_MAP: return { kind: "json" }; // field.map → single jsonb (pg) / text-json (sqlite) column
757
820
  case FIELD_SUBTYPE_UUID: return { kind: "uuid" }; // R6 Plan 2a — Postgres native uuid
821
+ case FIELD_SUBTYPE_URI: return { kind: "text" }; // ADR-0036/0037 Wave 3 — no Postgres uri type → text
822
+ case FIELD_SUBTYPE_INET: return { kind: "inet" }; // ADR-0036/0037 Wave 3 — Postgres-native inet
758
823
  default: return { kind: "text" }; // unknown → text fallback
759
824
  }
760
825
  }
@@ -148,6 +148,9 @@ export function pgTypeToSqlType(dataType: string, maxLength?: number | null, udt
148
148
  // UUID
149
149
  if (dt === "uuid") return { kind: "uuid" };
150
150
 
151
+ // INET (ADR-0036/0037 Wave 3 — field.inet)
152
+ if (dt === "inet") return { kind: "inet" };
153
+
151
154
  // Unknown types (user-defined enums, citext, ltree, etc.) fall back to text
152
155
  // so we don't blow up on unrecognised types.
153
156
  return { kind: "text" };
@@ -17,7 +17,8 @@ import { SetNullNotNullableError } from "./errors.js";
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  export function readIdentityFields(identity: MetaData): string[] {
20
- const raw = identity.ownAttr(IDENTITY_ATTR_FIELDS);
20
+ // ADR-0039: effective attr — @fields may be inherited via the identity's extends.
21
+ const raw = identity.attr(IDENTITY_ATTR_FIELDS);
21
22
  if (Array.isArray(raw)) return raw.map(String).filter((s) => s.length > 0);
22
23
  // Fallback: comma-separated string form (defensive; canonical form is array)
23
24
  if (typeof raw === "string") return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -32,9 +33,10 @@ export function findField(entity: MetaObject, name: string): MetaData | undefine
32
33
  }
33
34
 
34
35
  export function isRequired(field: MetaData): boolean {
35
- const attr = field.ownAttr(FIELD_ATTR_REQUIRED);
36
+ // ADR-0039: resolving — @required and validator.required may be inherited via extends.
37
+ const attr = field.attr(FIELD_ATTR_REQUIRED);
36
38
  if (attr === true || attr === "true") return true;
37
- return field.ownChildren().some(
39
+ return field.children().some(
38
40
  (c) => c.type === TYPE_VALIDATOR && c.subType === VALIDATOR_SUBTYPE_REQUIRED,
39
41
  );
40
42
  }
package/src/sql-type.ts CHANGED
@@ -18,6 +18,7 @@ export type SqlType =
18
18
  | { kind: "json" }
19
19
  | { kind: "blob" }
20
20
  | { kind: "uuid" }
21
+ | { kind: "inet" } // Postgres-native inet (IPv4/IPv6) — field.inet (ADR-0036/0037 Wave 3)
21
22
  | { kind: "array"; element: SqlType }; // native SQL array (e.g. uuid[], text[])
22
23
 
23
24
  /** Structural equality on SqlType. */
@@ -44,6 +45,7 @@ export function sqlTypeEquals(a: SqlType, b: SqlType): boolean {
44
45
  case "json":
45
46
  case "blob":
46
47
  case "uuid":
48
+ case "inet":
47
49
  return true;
48
50
  }
49
51
  }
@@ -94,6 +96,7 @@ export function isWidening(from: SqlType, to: SqlType): boolean {
94
96
  case "json":
95
97
  case "blob":
96
98
  case "uuid":
99
+ case "inet":
97
100
  case "timestamp":
98
101
  case "array":
99
102
  // Array element-type changes are not auto-widened — require explicit allow.