diff --git a/fixtures/metamodel-docs/expected/types/field.md b/fixtures/metamodel-docs/expected/types/field.md index cd7d01cff..3d673897f 100644 --- a/fixtures/metamodel-docs/expected/types/field.md +++ b/fixtures/metamodel-docs/expected/types/field.md @@ -22,7 +22,7 @@ Abstract base field — the shared root subtype that concrete field subtypes spe | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -52,7 +52,7 @@ True/false flag. Binds to the native boolean type; DB column is BOOLEAN. | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -85,7 +85,7 @@ Stores money as integer minor units (cents). Binds to long; the client formats v | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@currency` | string | no | `USD` | | metaobjects-core-types | ISO 4217 currency code for a currency-subtype field. Storage is integer minor units; defaults to 'USD' when omitted. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -116,7 +116,7 @@ Calendar date (no time-of-day). Binds to the native date/temporal type; DB colum | `@autoSet` | string | no | | `onCreate`, `onUpdate` | metaobjects-db | Auto-set semantics for timestamp-like fields: 'onCreate' stamps on insert, 'onUpdate' stamps on every write. | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -148,7 +148,7 @@ Precision-exact decimal (use @precision/@scale). Native TS binding is string (lo | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -180,7 +180,7 @@ Double-precision (64-bit) IEEE-754 floating point. Binds to the native double/nu | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -213,7 +213,7 @@ String-backed enumeration constrained to a closed set of member symbols (@values | `@coerceDefault` | string | no | | | metaobjects-prompt | Fallback enum member used by tolerant extract when a present value cannot be coerced; must be one of the field's @values. | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@enumAlias` | properties | no | | | metaobjects-prompt | Map of alternate/off-vocabulary tokens to canonical enum members; feeds the FR-010 tolerant extract alias-fold. | | `@enumDoc` | properties | no | | | metaobjects-prompt | Map of enum member to a human-readable description; shown per-member in the FR-010 'guide'-style prompt fragment. | @@ -248,7 +248,7 @@ Single-precision floating point. Binds to the native double/number type (TS has | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -278,7 +278,7 @@ Single-precision floating point. Binds to the native double/number type (TS has | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -308,7 +308,7 @@ Single-precision floating point. Binds to the native double/number type (TS has | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -340,7 +340,7 @@ An open-keyed map (Record / dict[str,V]) stored in a single jsonb colu | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -374,7 +374,7 @@ A nested structured value (set @objectRef to the target object). Storage is gove | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -406,7 +406,7 @@ Variable-length text. Binds to the native string type; DB column is VARCHAR/TEXT | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -438,7 +438,7 @@ Time-of-day (no calendar date). Binds to the native date/temporal type; DB colum | `@autoSet` | string | no | | `onCreate`, `onUpdate` | metaobjects-db | Auto-set semantics for timestamp-like fields: 'onCreate' stamps on insert, 'onUpdate' stamps on every write. | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -469,7 +469,7 @@ Date + time-of-day instant (optionally with timezone). Binds to the native date/ | `@autoSet` | string | no | | `onCreate`, `onUpdate` | metaobjects-db | Auto-set semantics for timestamp-like fields: 'onCreate' stamps on insert, 'onUpdate' stamps on every write. | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | @@ -499,7 +499,7 @@ Logical UUID identity scalar. A bare scalar (no required attrs, no loader value- | --- | --- | --- | --- | --- | --- | --- | | `@column` | string | no | | | metaobjects-db | Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy. | | `@db.indexed` | boolean | no | | | metaobjects-db | When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means). | -| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged. | +| `@dbColumnType` | string | no | | | metaobjects-db | Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid \| jsonb \| timestamp_with_tz \| uuid_array \| text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged. | | `@default` | any | no | | | metaobjects-core-types | Default value applied to the column when no value is supplied. Its type follows the field's own subtype (string / boolean / number / ...). Converted at consumption time via MetaField.defaultValue(). | | `@example` | string | no | | | metaobjects-prompt | FR-010: an example value for this field, shown in the generated output-format prompt fragment. | | `@filterable` | boolean | no | | | metaobjects-ui | When true, the field is exposed in generated CRUD filter allowlists (Project D filter layer). | diff --git a/fixtures/registry-conformance/expected-registry.json b/fixtures/registry-conformance/expected-registry.json index 1affe1419..f93721cd5 100644 --- a/fixtures/registry-conformance/expected-registry.json +++ b/fixtures/registry-conformance/expected-registry.json @@ -87,7 +87,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -208,7 +208,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -337,7 +337,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -465,7 +465,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -587,7 +587,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -722,7 +722,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -851,7 +851,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1007,7 +1007,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1128,7 +1128,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1249,7 +1249,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1371,7 +1371,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1507,7 +1507,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1642,7 +1642,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1777,7 +1777,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -1905,7 +1905,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", @@ -2026,7 +2026,7 @@ "valueType": "string", "isArray": false, "required": false, - "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." + "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." }, { "name": "default", diff --git a/server/csharp/MetaObjects/Loader/ValidationPasses.cs b/server/csharp/MetaObjects/Loader/ValidationPasses.cs index 0279a51a7..020c0034b 100644 --- a/server/csharp/MetaObjects/Loader/ValidationPasses.cs +++ b/server/csharp/MetaObjects/Loader/ValidationPasses.cs @@ -1347,7 +1347,8 @@ private static void WalkDbColumnType(MetaData node, List errors) // Rule 2: legal (subtype × value) pairing. var requiredSubType = value switch { - DB_COLUMN_TYPE_UUID or DB_COLUMN_TYPE_JSONB => FIELD_SUBTYPE_STRING, + DB_COLUMN_TYPE_UUID or DB_COLUMN_TYPE_JSONB + or DB_COLUMN_TYPE_UUID_ARRAY or DB_COLUMN_TYPE_TEXT_ARRAY => FIELD_SUBTYPE_STRING, DB_COLUMN_TYPE_TIMESTAMP_TZ => FIELD_SUBTYPE_TIMESTAMP, _ => null, // unreachable (Rule 1) }; diff --git a/server/csharp/MetaObjects/Persistence/Db/DbConstants.cs b/server/csharp/MetaObjects/Persistence/Db/DbConstants.cs index 05e7262c0..6148bd99b 100644 --- a/server/csharp/MetaObjects/Persistence/Db/DbConstants.cs +++ b/server/csharp/MetaObjects/Persistence/Db/DbConstants.cs @@ -60,6 +60,10 @@ public static class DbConstants public const string DB_COLUMN_TYPE_JSONB = "jsonb"; /// @dbColumnType: timestamp_with_tztimestamp with time zone column (legal on field.timestamp). public const string DB_COLUMN_TYPE_TIMESTAMP_TZ = "timestamp_with_tz"; + /// @dbColumnType: uuid_array — native Postgres uuid[] array column (legal on field.string). + public const string DB_COLUMN_TYPE_UUID_ARRAY = "uuid_array"; + /// @dbColumnType: text_array — native Postgres text[] array column (legal on field.string). + public const string DB_COLUMN_TYPE_TEXT_ARRAY = "text_array"; /// The closed set of legal @dbColumnType values. public static readonly IReadOnlyList VALID_DB_COLUMN_TYPES = new[] @@ -67,5 +71,7 @@ public static class DbConstants DB_COLUMN_TYPE_UUID, DB_COLUMN_TYPE_JSONB, DB_COLUMN_TYPE_TIMESTAMP_TZ, + DB_COLUMN_TYPE_UUID_ARRAY, + DB_COLUMN_TYPE_TEXT_ARRAY, }; } diff --git a/server/csharp/MetaObjects/SpecMetamodel/db.json b/server/csharp/MetaObjects/SpecMetamodel/db.json index 20b06329e..648ea134e 100644 --- a/server/csharp/MetaObjects/SpecMetamodel/db.json +++ b/server/csharp/MetaObjects/SpecMetamodel/db.json @@ -7,7 +7,7 @@ "children": [ { "type": "attr", "subType": "string", "name": "column", "min": 0, "max": 1, "description": "Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy." }, { "type": "attr", "subType": "boolean", "name": "db.indexed", "min": 0, "max": 1, "description": "When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means)." }, - { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." } + { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." } ] }, { diff --git a/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapper.kt b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapper.kt index eae20a3c5..1765f81f6 100644 --- a/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapper.kt +++ b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapper.kt @@ -23,6 +23,7 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.LIST import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.MAP import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy @@ -108,6 +109,12 @@ object KotlinTypeMapper { /** `@dbColumnType` value on [TimestampField] that opts in to Exposed `timestampWithTimeZone("col")`. */ private val DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ = CoreDBMetaDataProvider.DB_COLUMN_TYPE_TIMESTAMP_TZ + /** `@dbColumnType` values on [StringField] that emit native Postgres array columns + * (`uuid[]` / `text[]`) via the Exposed `array("col")` extension. The Kotlin + * property becomes `List` / `List`. */ + private val DB_COLUMN_TYPE_UUID_ARRAY = CoreDBMetaDataProvider.DB_COLUMN_TYPE_UUID_ARRAY + private val DB_COLUMN_TYPE_TEXT_ARRAY = CoreDBMetaDataProvider.DB_COLUMN_TYPE_TEXT_ARRAY + /** FQN of the Exposed `jsonb` extension function (raw-string open-JSON path). */ private const val EXPOSED_JSONB_IMPORT = "org.jetbrains.exposed.sql.json.jsonb" @@ -181,7 +188,13 @@ object KotlinTypeMapper { /** Map a MetaField to its KotlinPoet data-class property TypeName. */ fun kotlinTypeName(field: MetaField<*>): TypeName = when (field) { - is StringField -> STRING + // `@dbColumnType=uuid_array/text_array` makes a field.string a native SQL array, + // so the property is a List / List. Plain strings stay String. + is StringField -> when (dbColumnType(field)) { + DB_COLUMN_TYPE_UUID_ARRAY -> LIST.parameterizedBy(ClassName("java.util", "UUID")) + DB_COLUMN_TYPE_TEXT_ARRAY -> LIST.parameterizedBy(STRING) + else -> STRING + } is IntegerField -> INT is LongField -> LONG is DoubleField -> DOUBLE @@ -298,6 +311,8 @@ object KotlinTypeMapper { // `@dbColumnType=jsonb` on a field.string emits the `jsonb(...)` extension, which // needs the exposed-json import. `@dbColumnType=uuid` maps to `uuid(...)`, a Table // member — no import. All other StringField shapes (varchar/text) are Table members. + // `uuid_array`/`text_array` emit Exposed's `array("col")` — a Table MEMBER + // (like uuid/integer), so no import is required (same as the uuid/varchar paths). is StringField -> if (dbColumnType(field) == DB_COLUMN_TYPE_JSONB) EXPOSED_JSONB_IMPORT else null // field.map emits the `jsonb(...)` extension (single JSONB column), which needs @@ -329,6 +344,12 @@ object KotlinTypeMapper { when (dbColumnType(field)) { DB_COLUMN_TYPE_UUID -> "uuid(\"$colName\")" DB_COLUMN_TYPE_JSONB -> "jsonb(\"$colName\", { it }, { it })" + // Native SQL array columns via Exposed's `array("col", columnType)` Table + // member. The element ColumnType is explicit (UUIDColumnType / TextColumnType) + // so emission never depends on Exposed's reified resolveColumnType picking + // VARCHAR vs TEXT for String. uuid[] / text[] match the migrate-ts DDL. + DB_COLUMN_TYPE_UUID_ARRAY -> "array(\"$colName\", org.jetbrains.exposed.sql.UUIDColumnType())" + DB_COLUMN_TYPE_TEXT_ARRAY -> "array(\"$colName\", org.jetbrains.exposed.sql.TextColumnType())" else -> { // Dispatch to Exposed `text(name)` when the field is declared as unbounded text: // (1) explicit `@kind: "text"` opt-in, OR diff --git a/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt index d70476d09..b1de208bf 100644 --- a/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt +++ b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt @@ -116,6 +116,20 @@ class KotlinTypeMapperTest { assertEquals("long(\"id\")", spec) } + @Test fun `string field with dbColumnType=uuid_array maps to List of UUID + Exposed array column`() { + val f = StringField("memberIds") + f.addMetaAttr(StringAttribute.create("dbColumnType", "uuid_array")) + assertEquals("kotlin.collections.List", KotlinTypeMapper.kotlinTypeName(f).toString()) + assertEquals("array(\"member_ids\", org.jetbrains.exposed.sql.UUIDColumnType())", KotlinTypeMapper.exposedColumnSpec(f)) + } + + @Test fun `string field with dbColumnType=text_array maps to List of String + Exposed array column`() { + val f = StringField("tags") + f.addMetaAttr(StringAttribute.create("dbColumnType", "text_array")) + assertEquals("kotlin.collections.List", KotlinTypeMapper.kotlinTypeName(f).toString()) + assertEquals("array(\"tags\", org.jetbrains.exposed.sql.TextColumnType())", KotlinTypeMapper.exposedColumnSpec(f)) + } + @Test fun `int field maps to integer exposed column`() { val f = IntegerField("count") assertEquals("integer(\"count\")", KotlinTypeMapper.exposedColumnSpec(f)) diff --git a/server/java/metadata/src/main/java/com/metaobjects/database/CoreDBMetaDataProvider.java b/server/java/metadata/src/main/java/com/metaobjects/database/CoreDBMetaDataProvider.java index fa185f8b2..b749fe2f0 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/database/CoreDBMetaDataProvider.java +++ b/server/java/metadata/src/main/java/com/metaobjects/database/CoreDBMetaDataProvider.java @@ -67,10 +67,15 @@ public class CoreDBMetaDataProvider implements MetaDataTypeProvider { public static final String DB_COLUMN_TYPE_JSONB = "jsonb"; /** {@code @dbColumnType} value → {@code timestamp with time zone} column (on {@code field.timestamp}). */ public static final String DB_COLUMN_TYPE_TIMESTAMP_TZ = "timestamp_with_tz"; + /** {@code @dbColumnType} value → native Postgres {@code uuid[]} array column (on {@code field.string}). */ + public static final String DB_COLUMN_TYPE_UUID_ARRAY = "uuid_array"; + /** {@code @dbColumnType} value → native Postgres {@code text[]} array column (on {@code field.string}). */ + public static final String DB_COLUMN_TYPE_TEXT_ARRAY = "text_array"; /** The closed set of legal {@code @dbColumnType} values. */ public static final java.util.List VALID_DB_COLUMN_TYPES = java.util.List.of( - DB_COLUMN_TYPE_UUID, DB_COLUMN_TYPE_JSONB, DB_COLUMN_TYPE_TIMESTAMP_TZ); + DB_COLUMN_TYPE_UUID, DB_COLUMN_TYPE_JSONB, DB_COLUMN_TYPE_TIMESTAMP_TZ, + DB_COLUMN_TYPE_UUID_ARRAY, DB_COLUMN_TYPE_TEXT_ARRAY); @Override public String getProviderId() { diff --git a/server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java b/server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java index 7ba3111e5..cb1f21819 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java +++ b/server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java @@ -710,7 +710,9 @@ private static void validateDbColumnTypeNode(MetaData node) { String subType = node.getSubType(); String requiredSubType = switch (value) { case CoreDBMetaDataProvider.DB_COLUMN_TYPE_UUID, - CoreDBMetaDataProvider.DB_COLUMN_TYPE_JSONB -> StringField.SUBTYPE_STRING; + CoreDBMetaDataProvider.DB_COLUMN_TYPE_JSONB, + CoreDBMetaDataProvider.DB_COLUMN_TYPE_UUID_ARRAY, + CoreDBMetaDataProvider.DB_COLUMN_TYPE_TEXT_ARRAY -> StringField.SUBTYPE_STRING; case CoreDBMetaDataProvider.DB_COLUMN_TYPE_TIMESTAMP_TZ -> TimestampField.SUBTYPE_TIMESTAMP; default -> null; // unreachable — Rule 1 already rejected unknown values }; @@ -721,7 +723,8 @@ private static void validateDbColumnTypeNode(MetaData node) { + "' @" + CoreDBMetaDataProvider.DB_COLUMN_TYPE + " '" + value + "' is not valid on field." + subType + " (requires field." + requiredSubType + "); allowed pairings: " - + "uuid→field.string, jsonb→field.string, timestamp_with_tz→field.timestamp", + + "uuid→field.string, jsonb→field.string, uuid_array→field.string, " + + "text_array→field.string, timestamp_with_tz→field.timestamp", ErrorCode.ERR_BAD_ATTR_VALUE, node.getSource()); } } diff --git a/server/python/src/metaobjects/loader/validation_passes.py b/server/python/src/metaobjects/loader/validation_passes.py index 052f72ec9..a99ce92f3 100644 --- a/server/python/src/metaobjects/loader/validation_passes.py +++ b/server/python/src/metaobjects/loader/validation_passes.py @@ -42,8 +42,10 @@ ) from ..meta.persistence.db.db_constants import ( DB_COLUMN_TYPE_JSONB, + DB_COLUMN_TYPE_TEXT_ARRAY, DB_COLUMN_TYPE_TIMESTAMP_TZ, DB_COLUMN_TYPE_UUID, + DB_COLUMN_TYPE_UUID_ARRAY, FIELD_ATTR_DB_COLUMN_TYPE, VALID_DB_COLUMN_TYPES, ) @@ -655,6 +657,8 @@ def _validate_field_defaults(root: MetaData, errors: list[MetaError]) -> None: DB_COLUMN_TYPE_UUID: FIELD_SUBTYPE_STRING, DB_COLUMN_TYPE_JSONB: FIELD_SUBTYPE_STRING, DB_COLUMN_TYPE_TIMESTAMP_TZ: FIELD_SUBTYPE_TIMESTAMP, + DB_COLUMN_TYPE_UUID_ARRAY: FIELD_SUBTYPE_STRING, + DB_COLUMN_TYPE_TEXT_ARRAY: FIELD_SUBTYPE_STRING, } diff --git a/server/python/src/metaobjects/meta/persistence/db/db_constants.py b/server/python/src/metaobjects/meta/persistence/db/db_constants.py index 19cbdb160..67345072b 100644 --- a/server/python/src/metaobjects/meta/persistence/db/db_constants.py +++ b/server/python/src/metaobjects/meta/persistence/db/db_constants.py @@ -32,12 +32,18 @@ DB_COLUMN_TYPE_JSONB = "jsonb" # @dbColumnType: timestamp_with_tz — timestamp with time zone (legal on field.timestamp). DB_COLUMN_TYPE_TIMESTAMP_TZ = "timestamp_with_tz" +# @dbColumnType: uuid_array — native Postgres uuid[] array column (legal on field.string). +DB_COLUMN_TYPE_UUID_ARRAY = "uuid_array" +# @dbColumnType: text_array — native Postgres text[] array column (legal on field.string). +DB_COLUMN_TYPE_TEXT_ARRAY = "text_array" # The closed set of legal @dbColumnType values. VALID_DB_COLUMN_TYPES = ( DB_COLUMN_TYPE_UUID, DB_COLUMN_TYPE_JSONB, DB_COLUMN_TYPE_TIMESTAMP_TZ, + DB_COLUMN_TYPE_UUID_ARRAY, + DB_COLUMN_TYPE_TEXT_ARRAY, ) # --------------------------------------------------------------------------- diff --git a/server/python/src/metaobjects/spec_metamodel/db.json b/server/python/src/metaobjects/spec_metamodel/db.json index 20b06329e..648ea134e 100644 --- a/server/python/src/metaobjects/spec_metamodel/db.json +++ b/server/python/src/metaobjects/spec_metamodel/db.json @@ -7,7 +7,7 @@ "children": [ { "type": "attr", "subType": "string", "name": "column", "min": 0, "max": 1, "description": "Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy." }, { "type": "attr", "subType": "boolean", "name": "db.indexed", "min": 0, "max": 1, "description": "When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means)." }, - { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." } + { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." } ] }, { diff --git a/server/typescript/packages/metadata/src/persistence/db/db-constants.ts b/server/typescript/packages/metadata/src/persistence/db/db-constants.ts index eeafc4678..49f539cc9 100644 --- a/server/typescript/packages/metadata/src/persistence/db/db-constants.ts +++ b/server/typescript/packages/metadata/src/persistence/db/db-constants.ts @@ -40,12 +40,18 @@ export const DB_COLUMN_TYPE_UUID = "uuid"; export const DB_COLUMN_TYPE_JSONB = "jsonb"; /** `@dbColumnType: timestamp_with_tz` — `timestamp with time zone` (legal on field.timestamp). */ export const DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ = "timestamp_with_tz"; +/** `@dbColumnType: uuid_array` — native Postgres `uuid[]` column (legal on field.string). */ +export const DB_COLUMN_TYPE_UUID_ARRAY = "uuid_array"; +/** `@dbColumnType: text_array` — native Postgres `text[]` column (legal on field.string). */ +export const DB_COLUMN_TYPE_TEXT_ARRAY = "text_array"; /** The closed set of legal `@dbColumnType` values (raw-dialect passthrough deferred). */ export const DB_COLUMN_TYPE_VALUES = [ DB_COLUMN_TYPE_UUID, DB_COLUMN_TYPE_JSONB, DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ, + DB_COLUMN_TYPE_UUID_ARRAY, + DB_COLUMN_TYPE_TEXT_ARRAY, ] as const; export type DbColumnTypeValue = (typeof DB_COLUMN_TYPE_VALUES)[number]; @@ -59,4 +65,6 @@ export const DB_COLUMN_TYPE_LEGAL_SUBTYPES: Readonly>): P * * If character_maximum_length is available, callers should pass `maxLength`. */ -export function pgTypeToSqlType(dataType: string, maxLength?: number | null): SqlType { +export function pgTypeToSqlType(dataType: string, maxLength?: number | null, udtName?: string): SqlType { const dt = dataType.toLowerCase().trim(); + // Array columns: information_schema reports data_type "ARRAY" and the element + // type in udt_name with a leading underscore (e.g. "_uuid", "_text", "_varchar"). + if (dt === "array" && typeof udtName === "string") { + const elemUdt = udtName.replace(/^_/, "").toLowerCase(); + return { kind: "array", element: pgTypeToSqlType(elemUdt, maxLength) }; + } + // Length-bearing text types — may arrive as "character varying(255)" (full // inline) or as bare "character varying" with maxLength from a separate column. const varcharMatch = /^(?:character varying|varchar)\((\d+)\)$/.exec(dt); @@ -279,7 +286,7 @@ async function readColumns(k: Kysely, schema: string, tableName: string): P `.execute(k); return rows.rows.map((r) => { - const sqlType = pgTypeToSqlType(r.data_type, r.character_maximum_length); + const sqlType = pgTypeToSqlType(r.data_type, r.character_maximum_length, r.udt_name); const col: ColumnDescriptor = { name: r.column_name, sqlType, diff --git a/server/typescript/packages/migrate-ts/src/sql-type.ts b/server/typescript/packages/migrate-ts/src/sql-type.ts index c475bca4e..3d043a61a 100644 --- a/server/typescript/packages/migrate-ts/src/sql-type.ts +++ b/server/typescript/packages/migrate-ts/src/sql-type.ts @@ -17,7 +17,8 @@ export type SqlType = | { kind: "time" } | { kind: "json" } | { kind: "blob" } - | { kind: "uuid" }; + | { kind: "uuid" } + | { kind: "array"; element: SqlType }; // native SQL array (e.g. uuid[], text[]) /** Structural equality on SqlType. */ export function sqlTypeEquals(a: SqlType, b: SqlType): boolean { @@ -33,6 +34,8 @@ export function sqlTypeEquals(a: SqlType, b: SqlType): boolean { } case "timestamp": return a.withTimezone === (b as Extract).withTimezone; + case "array": + return sqlTypeEquals(a.element, (b as Extract).element); case "real": case "real4": case "boolean": @@ -92,6 +95,8 @@ export function isWidening(from: SqlType, to: SqlType): boolean { case "blob": case "uuid": case "timestamp": + case "array": + // Array element-type changes are not auto-widened — require explicit allow. return false; } } diff --git a/server/typescript/packages/migrate-ts/test/unit/expected-schema.test.ts b/server/typescript/packages/migrate-ts/test/unit/expected-schema.test.ts index afd7e32e8..5a77b5696 100644 --- a/server/typescript/packages/migrate-ts/test/unit/expected-schema.test.ts +++ b/server/typescript/packages/migrate-ts/test/unit/expected-schema.test.ts @@ -147,6 +147,36 @@ describe("buildExpectedSchema — @autoSet timestamp default", () => { }); }); +// --------------------------------------------------------------------------- +// @dbColumnType uuid_array / text_array → native SQL array columns +// (metaobjects can't model Postgres arrays via field subtypes; the physical +// @dbColumnType escape hatch carries them, emitting uuid[] / text[].) +// --------------------------------------------------------------------------- + +describe("buildExpectedSchema — native array columns (@dbColumnType *_array)", () => { + async function arrayCols() { + const doc = { "metadata.root": { package: "acme", children: [ + { "object.entity": { name: "Bag", children: [ + { "field.long": { name: "id" } }, + { "identity.primary": { "name": "id", "@fields": "id" } }, + { "field.string": { name: "memberIds", "@dbColumnType": "uuid_array" } }, + { "field.string": { name: "tags", "@dbColumnType": "text_array" } }, + ] } }, + ] } }; + const { root, errors } = await new MetaDataLoader().load([ + new InMemoryStringSource(JSON.stringify(doc)), + ]); + expect(errors).toHaveLength(0); + return new Map((buildExpectedSchema(root).tables[0]?.columns ?? []).map((c) => [c.name, c])); + } + + test("uuid_array → array of uuid; text_array → array of text", async () => { + const byName = await arrayCols(); + expect(byName.get("member_ids")?.sqlType).toEqual({ kind: "array", element: { kind: "uuid" } }); + expect(byName.get("tags")?.sqlType).toEqual({ kind: "array", element: { kind: "text" } }); + }); +}); + describe("buildExpectedSchema — indexes + FKs", () => { let snapshot: SchemaSnapshot; diff --git a/server/typescript/packages/migrate-ts/test/unit/introspect-postgres-helpers.test.ts b/server/typescript/packages/migrate-ts/test/unit/introspect-postgres-helpers.test.ts index 9e200d542..5b4f12e5d 100644 --- a/server/typescript/packages/migrate-ts/test/unit/introspect-postgres-helpers.test.ts +++ b/server/typescript/packages/migrate-ts/test/unit/introspect-postgres-helpers.test.ts @@ -68,6 +68,16 @@ describe("pgTypeToSqlType", () => { expect(pgTypeToSqlType(" BIGINT ")).toEqual({ kind: "integer", bits: 64 }); expect(pgTypeToSqlType("TIMESTAMPTZ")).toEqual({ kind: "timestamp", withTimezone: true }); }); + + test("ARRAY columns resolve their element type from udt_name (leading underscore)", () => { + expect(pgTypeToSqlType("ARRAY", null, "_uuid")).toEqual({ kind: "array", element: { kind: "uuid" } }); + expect(pgTypeToSqlType("ARRAY", null, "_text")).toEqual({ kind: "array", element: { kind: "text" } }); + expect(pgTypeToSqlType("array", null, "_int4")).toEqual({ kind: "array", element: { kind: "integer", bits: 32 } }); + }); + + test("ARRAY without a udt_name falls back to text (defensive)", () => { + expect(pgTypeToSqlType("ARRAY")).toEqual({ kind: "text" }); + }); }); describe("parsePgDefault", () => { diff --git a/spec/metamodel/db.json b/spec/metamodel/db.json index 20b06329e..648ea134e 100644 --- a/spec/metamodel/db.json +++ b/spec/metamodel/db.json @@ -7,7 +7,7 @@ "children": [ { "type": "attr", "subType": "string", "name": "column", "min": 0, "max": 1, "description": "Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy." }, { "type": "attr", "subType": "boolean", "name": "db.indexed", "min": 0, "max": 1, "description": "When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means)." }, - { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz, each legal only on a specific logical field subtype (uuid/jsonb on field.string, timestamp_with_tz on field.timestamp). The logical field type and its native binding are unchanged." } + { "type": "attr", "subType": "string", "name": "dbColumnType", "min": 0, "max": 1, "description": "Physical DB column-type override (ADR-0013 escape hatch). Legal values are uuid | jsonb | timestamp_with_tz | uuid_array | text_array, each legal only on a specific logical field subtype (uuid/jsonb/uuid_array/text_array on field.string, timestamp_with_tz on field.timestamp). uuid_array/text_array emit native SQL arrays (uuid[]/text[]). The logical field type and its native binding are unchanged." } ] }, {