@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.
- package/dist/emit/postgres.js +1 -0
- package/dist/emit/postgres.js.map +1 -1
- package/dist/emit/sqlite.js +1 -0
- package/dist/emit/sqlite.js.map +1 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +104 -39
- package/dist/expected-schema.js.map +1 -1
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/postgres.js +3 -0
- package/dist/introspect/postgres.js.map +1 -1
- package/dist/referential-actions.d.ts.map +1 -1
- package/dist/referential-actions.js +5 -3
- package/dist/referential-actions.js.map +1 -1
- package/dist/sql-type.d.ts +2 -0
- package/dist/sql-type.d.ts.map +1 -1
- package/dist/sql-type.js +2 -0
- package/dist/sql-type.js.map +1 -1
- package/package.json +2 -2
- package/src/emit/postgres.ts +1 -0
- package/src/emit/sqlite.ts +1 -0
- package/src/expected-schema.ts +108 -43
- package/src/introspect/postgres.ts +3 -0
- package/src/referential-actions.ts +5 -3
- package/src/sql-type.ts +3 -0
package/src/expected-schema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
535
|
-
const
|
|
536
|
-
const
|
|
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
|
-
|
|
543
|
-
const
|
|
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.
|
|
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
|
-
|
|
554
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
719
|
-
case DB_COLUMN_TYPE_JSONB:
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|