Skip to content

Commit 8cf8a66

Browse files
committed
feat(migrate): expression/functional indexes (identity.secondary @expr + @using)
Adds support for functional/expression indexes — e.g. `lower(email)` (btree) and `string_to_array(tags, ',')` (GIN) — which previously could not be modeled and were silently skipped on introspect. - identity.secondary gains two db-provider attrs: - @expr — raw key EXPRESSION; used instead of @fields (which then anchors the underlying column(s) for the loader's required @fields) - @using — access method (gin/gist/hash/…); default btree (not rendered) - IndexDescriptor: expr?, using? - buildExpectedSchema: reads @expr/@using - emit (postgres): renders `USING <method> (<expr>)` - introspect (postgres): captures the access method + lifts the key expression from pg_get_indexdef (expression-key indexes are no longer skipped) - diff/indexEquals: compares using + normalized expr - registered across all ports (TS/Python/Java+Kotlin/C#); spec + embedded def updated; registry-conformance manifest, coverage snapshot, and metamodel docs regenerated - test: expression-index round-trip (expected-schema + emit) for btree functional + GIN expression indexes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 0518071 commit 8cf8a66

19 files changed

Lines changed: 224 additions & 16 deletions

File tree

fixtures/metamodel-docs/expected/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ DB-domain attributes — @column / @db.indexed / @dbColumnType on every field, @
6262
- `field.timestamp`: `@autoSet`, `@column`, `@db.indexed`, `@dbColumnType`
6363
- `field.uuid`: `@column`, `@db.indexed`, `@dbColumnType`
6464
- `identity.reference`: `@constraintName`
65-
- `identity.secondary`: `@orders`, `@where`
65+
- `identity.secondary`: `@expr`, `@orders`, `@using`, `@where`
6666
- `source.rdb`: `@function`, `@kind`, `@materializedView`, `@parameterRef`, `@proc`, `@role`, `@schema`, `@table`, `@view`
6767

6868
## metaobjects-documentation

fixtures/metamodel-docs/expected/types/identity.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ A secondary index (unique by default via @unique).
5656

5757
| Attribute | Type | Required | Default | Allowed values | Provider | Description |
5858
| --- | --- | --- | --- | --- | --- | --- |
59+
| `@expr` | string | no | | | metaobjects-db | Raw key EXPRESSION for a functional/expression index (e.g. "lower(email)"). Used INSTEAD of @fields — the index key is the expression rather than plain columns. RDB-physical — contributed by the db provider. |
5960
| `@fields` | string[] | yes | | | metaobjects-core-types | The field name(s) composing this identity. Single-element for a simple PK/index, multiple for a composite. |
6061
| `@orders` | string[] | no | | `asc`, `desc` | metaobjects-db | Physical index-key sort direction, positional to @fields ('asc' \| 'desc'). Omit for all-ascending (the default); a shorter array leaves trailing keys ascending. Drives DESC-ordered index keys (e.g. a recency index on a timestamp). RDB-physical — contributed by the db provider, not core identity. |
6162
| `@unique` | boolean | no | | | metaobjects-core-types | When true (default), the secondary identity is a UNIQUE index; false makes it a plain (non-unique) index. |
63+
| `@using` | string | no | | | metaobjects-db | Index access method (e.g. "gin", "gist", "hash"); default "btree" (not rendered). Pair with @expr for e.g. a GIN index over an array/jsonb expression. RDB-physical — contributed by the db provider. |
6264
| `@where` | string | no | | | metaobjects-db | Partial-index predicate (raw SQL, e.g. "delivered_at IS NULL"). When set, the index covers only rows matching the predicate — smaller and cheaper for queries that always filter on it. Absent = a full index over every row. RDB-physical — contributed by the db provider. |
6365

6466
**Allowed children**

fixtures/registry-conformance/coverage-report.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@
226226
{
227227
"key": "identity.secondary",
228228
"untestedAttrs": [
229+
"expr",
229230
"orders",
231+
"using",
230232
"where"
231233
]
232234
},

fixtures/registry-conformance/expected-registry.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,6 +2050,13 @@
20502050
"subType": "secondary",
20512051
"description": "A secondary index (unique by default via @unique).",
20522052
"attrs": [
2053+
{
2054+
"name": "expr",
2055+
"valueType": "string",
2056+
"isArray": false,
2057+
"required": false,
2058+
"description": "Raw key EXPRESSION for a functional/expression index (e.g. \"lower(email)\"). Used INSTEAD of @fields — the index key is the expression rather than plain columns. RDB-physical — contributed by the db provider."
2059+
},
20532060
{
20542061
"name": "fields",
20552062
"valueType": "string",
@@ -2071,6 +2078,13 @@
20712078
"required": false,
20722079
"description": "When true (default), the secondary identity is a UNIQUE index; false makes it a plain (non-unique) index."
20732080
},
2081+
{
2082+
"name": "using",
2083+
"valueType": "string",
2084+
"isArray": false,
2085+
"required": false,
2086+
"description": "Index access method (e.g. \"gin\", \"gist\", \"hash\"); default \"btree\" (not rendered). Pair with @expr for e.g. a GIN index over an array/jsonb expression. RDB-physical — contributed by the db provider."
2087+
},
20742088
{
20752089
"name": "where",
20762090
"valueType": "string",

server/csharp/MetaObjects/Persistence/Db/DbConstants.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public static class DbConstants
3030
public const string IDENTITY_ATTR_ORDERS = "orders";
3131
/// <summary>identity.secondary: partial-index predicate (raw SQL). When set, the index covers only matching rows.</summary>
3232
public const string IDENTITY_ATTR_WHERE = "where";
33+
/// <summary>identity.secondary: raw key EXPRESSION for a functional/expression index (used instead of @fields).</summary>
34+
public const string IDENTITY_ATTR_EXPR = "expr";
35+
/// <summary>identity.secondary: index access method (e.g. "gin"); default "btree".</summary>
36+
public const string IDENTITY_ATTR_USING = "using";
3337
/// <summary>identity.reference: physical FK constraint-name override. Absent =&gt; the auto-derived default.</summary>
3438
public const string IDENTITY_ATTR_CONSTRAINT_NAME = "constraintName";
3539

server/csharp/MetaObjects/Persistence/Db/DbProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void RegisterTypes(TypeRegistry registry)
4848
registry.Extend(
4949
MetaObjects.Shared.BaseTypes.TYPE_IDENTITY,
5050
IdentityConstants.IDENTITY_SUBTYPE_SECONDARY,
51-
attributes: [DbSchema.OrdersSchema, DbSchema.WhereSchema]);
51+
attributes: [DbSchema.OrdersSchema, DbSchema.WhereSchema, DbSchema.ExprSchema, DbSchema.UsingSchema]);
5252
registry.Extend(
5353
MetaObjects.Shared.BaseTypes.TYPE_IDENTITY,
5454
IdentityConstants.IDENTITY_SUBTYPE_REFERENCE,

server/csharp/MetaObjects/Persistence/Db/DbSchema.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ public static class DbSchema
6161
Required: false,
6262
Description: "Partial-index predicate (raw SQL, e.g. \"delivered_at IS NULL\"). When set, the index covers only rows matching the predicate — smaller and cheaper for queries that always filter on it. Absent = a full index over every row. RDB-physical — contributed by the db provider.");
6363

64+
/// <summary><c>@expr</c> — raw key expression for a functional/expression index; on identity.secondary.</summary>
65+
public static readonly AttrSchema ExprSchema = new AttrSchema(
66+
Name: DbConstants.IDENTITY_ATTR_EXPR,
67+
ValueType: AttrConstants.ATTR_SUBTYPE_STRING,
68+
Required: false,
69+
Description: "Raw key EXPRESSION for a functional/expression index (e.g. \"lower(email)\"). Used INSTEAD of @fields — the index key is the expression rather than plain columns. RDB-physical — contributed by the db provider.");
70+
71+
/// <summary><c>@using</c> — index access method (e.g. "gin"); default "btree"; on identity.secondary.</summary>
72+
public static readonly AttrSchema UsingSchema = new AttrSchema(
73+
Name: DbConstants.IDENTITY_ATTR_USING,
74+
ValueType: AttrConstants.ATTR_SUBTYPE_STRING,
75+
Required: false,
76+
Description: "Index access method (e.g. \"gin\", \"gist\", \"hash\"); default \"btree\" (not rendered). Pair with @expr for e.g. a GIN index over an array/jsonb expression. RDB-physical — contributed by the db provider.");
77+
6478
/// <summary><c>@constraintName</c> — physical FK constraint-name override; on identity.reference.</summary>
6579
public static readonly AttrSchema ConstraintNameSchema = new AttrSchema(
6680
Name: DbConstants.IDENTITY_ATTR_CONSTRAINT_NAME,

server/java/metadata/src/main/java/com/metaobjects/identity/SecondaryIdentity.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public class SecondaryIdentity extends MetaIdentity {
3131
public static final String ATTR_ORDERS = "orders";
3232
/** Partial-index predicate (raw SQL) — db-provider attr (RDB-physical). */
3333
public static final String ATTR_WHERE = "where";
34+
/** Raw key EXPRESSION for a functional/expression index — db-provider attr (RDB-physical). */
35+
public static final String ATTR_EXPR = "expr";
36+
/** Index access method (e.g. "gin"); default "btree" — db-provider attr (RDB-physical). */
37+
public static final String ATTR_USING = "using";
3438

3539
public SecondaryIdentity(String name) {
3640
super(SUBTYPE_SECONDARY, name);
@@ -60,6 +64,8 @@ public static void registerTypes(MetaDataRegistry registry) {
6064
// (asc|desc) is not part of the v1 manifest contract.
6165
def.optionalAttributeWithConstraints(ATTR_ORDERS).ofType(StringAttribute.SUBTYPE_STRING).asArray();
6266
def.optionalAttributeWithConstraints(ATTR_WHERE).ofType(StringAttribute.SUBTYPE_STRING).asSingle();
67+
def.optionalAttributeWithConstraints(ATTR_EXPR).ofType(StringAttribute.SUBTYPE_STRING).asSingle();
68+
def.optionalAttributeWithConstraints(ATTR_USING).ofType(StringAttribute.SUBTYPE_STRING).asSingle();
6369

6470
// ACCEPTS ANY ATTRIBUTES (for extensibility from service providers)
6571
def.optionalChild(MetaAttribute.TYPE_ATTR, "*", "*");

server/python/src/metaobjects/meta/persistence/db/db_constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,9 @@
5151
IDENTITY_SECONDARY_ATTR_ORDERS = "orders"
5252
# identity.secondary: partial-index predicate (raw SQL).
5353
IDENTITY_SECONDARY_ATTR_WHERE = "where"
54+
# identity.secondary: raw key EXPRESSION for a functional/expression index (used instead of @fields).
55+
IDENTITY_SECONDARY_ATTR_EXPR = "expr"
56+
# identity.secondary: index access method (e.g. "gin"); default "btree".
57+
IDENTITY_SECONDARY_ATTR_USING = "using"
5458
# identity.reference: physical FK constraint-name override.
5559
IDENTITY_REFERENCE_ATTR_CONSTRAINT_NAME = "constraintName"

server/python/src/metaobjects/meta/persistence/db/db_provider.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
IDENTITY_REFERENCE_ATTR_CONSTRAINT_NAME,
3131
IDENTITY_SECONDARY_ATTR_ORDERS,
3232
IDENTITY_SECONDARY_ATTR_WHERE,
33+
IDENTITY_SECONDARY_ATTR_EXPR,
34+
IDENTITY_SECONDARY_ATTR_USING,
3335
)
3436

3537
# Every field subtype the DB-domain attrs apply to: the shared FIELD_SUBTYPES tuple
@@ -77,6 +79,26 @@
7779
"RDB-physical — contributed by the db provider."
7880
),
7981
)
82+
_EXPR_SCHEMA = AttrSchema(
83+
name=IDENTITY_SECONDARY_ATTR_EXPR,
84+
value_type=ATTR_SUBTYPE_STRING,
85+
required=False,
86+
description=(
87+
'Raw key EXPRESSION for a functional/expression index (e.g. "lower(email)"). '
88+
"Used INSTEAD of @fields — the index key is the expression rather than plain "
89+
"columns. RDB-physical — contributed by the db provider."
90+
),
91+
)
92+
_USING_SCHEMA = AttrSchema(
93+
name=IDENTITY_SECONDARY_ATTR_USING,
94+
value_type=ATTR_SUBTYPE_STRING,
95+
required=False,
96+
description=(
97+
'Index access method (e.g. "gin", "gist", "hash"); default "btree" (not '
98+
"rendered). Pair with @expr for e.g. a GIN index over an array/jsonb "
99+
"expression. RDB-physical — contributed by the db provider."
100+
),
101+
)
80102
_CONSTRAINT_NAME_SCHEMA = AttrSchema(
81103
name=IDENTITY_REFERENCE_ATTR_CONSTRAINT_NAME,
82104
value_type=ATTR_SUBTYPE_STRING,
@@ -101,7 +123,7 @@ def _register(registry: TypeRegistry) -> None:
101123
registry.extend(
102124
TYPE_IDENTITY,
103125
IDENTITY_SUBTYPE_SECONDARY,
104-
attributes=[_ORDERS_SCHEMA, _WHERE_SCHEMA],
126+
attributes=[_ORDERS_SCHEMA, _WHERE_SCHEMA, _EXPR_SCHEMA, _USING_SCHEMA],
105127
)
106128
registry.extend(
107129
TYPE_IDENTITY,

0 commit comments

Comments
 (0)