Skip to content

Commit 5ecacb0

Browse files
dmealingclaude
andcommitted
feat(#208): register @SQL + @Unmanaged on source.rdb (5-port coordinated)
Task 1 of the #208 DDL-ownership escape-valve plan. Registers two new attributes on source.rdb across all five ports so they load without ERR_UNKNOWN_ATTR and the cross-port registry-conformance byte-match stays green — registration only, no validation or migrate/verify behavior yet (later tasks). - @SQL (string): a hand-written DB-object body the tool registers + fingerprints + drift-checks but never authors or parses. - @Unmanaged (boolean): the object is managed elsewhere (Flyway/hand-migration); migrate skips create/drop/drift. Canonical spec/metamodel/db.json → regenerated TS db-definition.embedded.ts + source constants; Java RdbSource; Python + C# providers + their committed SpecMetamodel copies; fixtures/registry-conformance/expected-registry.json; the regenerated metamodel-docs fixtures; and a positive source-sql-escape conformance fixture (@SQL on a view projection + @Unmanaged on a table entity — both legal shapes that survive the later validation tasks). Verified: registry-conformance + the conformance corpus green on all five ports (TS 2187 + typecheck; Java metadata 1175+; Python 629; C# conformance 770). Kotlin rides Java's JVM registry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGQ7oSuNcjhsMHWwZzhBwr
1 parent fc93ffe commit 5ecacb0

16 files changed

Lines changed: 232 additions & 1 deletion

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.projection": {
7+
"name": "ProgramSummaryEscaped",
8+
"children": [
9+
{
10+
"source.rdb": {
11+
"@kind": "view",
12+
"@sql": "SELECT id, count(*) AS n FROM program GROUP BY id",
13+
"@view": "v_program_summary_escaped"
14+
}
15+
},
16+
{
17+
"field.long": {
18+
"name": "id"
19+
}
20+
}
21+
]
22+
}
23+
},
24+
{
25+
"object.entity": {
26+
"name": "LegacyAccount",
27+
"children": [
28+
{
29+
"source.rdb": {
30+
"@table": "legacy_account",
31+
"@unmanaged": true
32+
}
33+
},
34+
{
35+
"field.long": {
36+
"name": "id"
37+
}
38+
},
39+
{
40+
"identity.primary": {
41+
"name": "id",
42+
"@fields": [
43+
"id"
44+
]
45+
}
46+
}
47+
]
48+
}
49+
}
50+
]
51+
}
52+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.projection": {
7+
"name": "ProgramSummaryEscaped",
8+
"children": [
9+
{
10+
"source.rdb": {
11+
"@kind": "view",
12+
"@view": "v_program_summary_escaped",
13+
"@sql": "SELECT id, count(*) AS n FROM program GROUP BY id"
14+
}
15+
},
16+
{
17+
"field.long": {
18+
"name": "id"
19+
}
20+
}
21+
]
22+
}
23+
},
24+
{
25+
"object.entity": {
26+
"name": "LegacyAccount",
27+
"children": [
28+
{
29+
"source.rdb": {
30+
"@table": "legacy_account",
31+
"@unmanaged": true
32+
}
33+
},
34+
{
35+
"field.long": {
36+
"name": "id"
37+
}
38+
},
39+
{
40+
"identity.primary": {
41+
"name": "id",
42+
"@fields": "id"
43+
}
44+
}
45+
]
46+
}
47+
}
48+
]
49+
}
50+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["metaobjects-core-types","metaobjects-db"]

fixtures/metamodel-docs/expected/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ DB-domain attributes — @column / @db.indexed / @dbColumnType on every field, @
7575
- `identity.reference`: `@constraintName`
7676
- `identity.secondary`: `@expr`, `@orders`, `@using`, `@where`
7777
- `index.lookup`: `@expr`, `@orders`, `@using`, `@where`
78-
- `source.rdb`: `@function`, `@kind`, `@materializedView`, `@parameterRef`, `@proc`, `@role`, `@schema`, `@table`, `@view`
78+
- `source.rdb`: `@function`, `@kind`, `@materializedView`, `@parameterRef`, `@proc`, `@role`, `@schema`, `@sql`, `@table`, `@unmanaged`, `@view`
7979

8080
## metaobjects-documentation
8181

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ The relational-database paradigm source (ADR-0007): binds an object to a physica
4545
| `@proc` | string | no | | | metaobjects-db | Physical SQL stored-procedure name for source.rdb @kind: "storedProc". Same internal slot as @table. |
4646
| `@role` | string | no | | `primary`, `replica`, `index`, `cache`, `publish`, `mirror` | metaobjects-db | Role this source plays when an object has multiple sources: primary (default, system of record), replica, index, cache, publish, or mirror. |
4747
| `@schema` | string | no | | | metaobjects-db | Optional database schema name (e.g. 'catalog', 'public'). Postgres defaults to 'public'; SQLite rejects any non-default value. |
48+
| `@sql` | string | no | | | metaobjects-db | FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS + fingerprints + drift-checks but never authors or parses. The body goes INSIDE `CREATE <kind> <physicalName> AS …` (never the CREATE wrapper, never the object name). Legal only on a read-only kind (not @kind: table); migrate lowers it on @kind: view (matview/proc/tableFunction: registered but not yet migrate-managed). Mutually exclusive with @unmanaged; forbids origin.* children (two sources of truth). |
4849
| `@table` | string | no | | | metaobjects-db | Physical SQL table name for source.rdb @kind: "table" (default). FR-016: Defaults from the source's bare structural `name` via the project's columnNamingStrategy when omitted, then from the owning entity's name. Pre-1.0 legacy spelling for view/materializedView/storedProc/tableFunction kinds during the transition; canonical-serializer rewrites to the kind-matching alias. |
50+
| `@unmanaged` | boolean | no | | | metaobjects-db | FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / a hand-migration owns its DDL). meta migrate does NOT create, drop, or drift-check it; verify --db reports it as external (declared). Legal on any @kind including table (the externally-managed-entity case). Mutually exclusive with @sql. |
4951
| `@view` | string | no | | | metaobjects-db | Physical SQL view name for source.rdb @kind: "view". Same internal slot as @table. |
5052

5153
**Allowed children**

fixtures/registry-conformance/expected-registry.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,13 +3615,27 @@
36153615
"required": false,
36163616
"description": "Optional database schema name (e.g. 'catalog', 'public'). Postgres defaults to 'public'; SQLite rejects any non-default value."
36173617
},
3618+
{
3619+
"name": "sql",
3620+
"valueType": "string",
3621+
"isArray": false,
3622+
"required": false,
3623+
"description": "FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS + fingerprints + drift-checks but never authors or parses. The body goes INSIDE `CREATE <kind> <physicalName> AS …` (never the CREATE wrapper, never the object name). Legal only on a read-only kind (not @kind: table); migrate lowers it on @kind: view (matview/proc/tableFunction: registered but not yet migrate-managed). Mutually exclusive with @unmanaged; forbids origin.* children (two sources of truth)."
3624+
},
36183625
{
36193626
"name": "table",
36203627
"valueType": "string",
36213628
"isArray": false,
36223629
"required": false,
36233630
"description": "Physical SQL table name for source.rdb @kind: \"table\" (default). FR-016: Defaults from the source's bare structural `name` via the project's columnNamingStrategy when omitted, then from the owning entity's name. Pre-1.0 legacy spelling for view/materializedView/storedProc/tableFunction kinds during the transition; canonical-serializer rewrites to the kind-matching alias."
36243631
},
3632+
{
3633+
"name": "unmanaged",
3634+
"valueType": "boolean",
3635+
"isArray": false,
3636+
"required": false,
3637+
"description": "FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / a hand-migration owns its DDL). meta migrate does NOT create, drop, or drift-check it; verify --db reports it as external (declared). Legal on any @kind including table (the externally-managed-entity case). Mutually exclusive with @sql."
3638+
},
36253639
{
36263640
"name": "view",
36273641
"valueType": "string",

server/csharp/MetaObjects/Persistence/Source/SourceConstants.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ public static class SourceConstants
7878
/// <summary>Default Postgres schema when @schema is omitted from a source.</summary>
7979
public const string DEFAULT_DB_SCHEMA_POSTGRES = "public";
8080

81+
/// <summary>
82+
/// FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS +
83+
/// fingerprints + drift-checks but never authors or parses. Legal only on a
84+
/// read-only @kind (not "table"). Mutually exclusive with
85+
/// <see cref="SOURCE_ATTR_UNMANAGED"/>; forbids origin.* children.
86+
/// </summary>
87+
public const string SOURCE_ATTR_SQL = "sql";
88+
89+
/// <summary>
90+
/// FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway /
91+
/// a hand-migration owns its DDL); meta migrate does not
92+
/// create/drop/drift-check it. Legal on any @kind including "table".
93+
/// Mutually exclusive with <see cref="SOURCE_ATTR_SQL"/>.
94+
/// </summary>
95+
public const string SOURCE_ATTR_UNMANAGED = "unmanaged";
96+
8197
/// <summary>
8298
/// FR-015: name or FQN of an object.value describing the input shape of a
8399
/// callable source (@kind: "storedProc" / "tableFunction"). Symmetric with

server/csharp/MetaObjects/Persistence/Source/SourceSchema.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ public static class SourceSchema
7575
Required: false,
7676
Description: "Optional database schema name (e.g. 'catalog', 'public'). Postgres defaults to 'public'; SQLite rejects any non-default value."),
7777

78+
// FR-024/#208 escape valve — registration only, no validation/lowering here.
79+
new AttrSchema(
80+
Name: SourceConstants.SOURCE_ATTR_SQL,
81+
ValueType: AttrConstants.ATTR_SUBTYPE_STRING,
82+
Required: false,
83+
Description: "FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS + " +
84+
"fingerprints + drift-checks but never authors or parses. The body goes INSIDE " +
85+
"`CREATE <kind> <physicalName> AS …` (never the CREATE wrapper, never the object name). " +
86+
"Legal only on a read-only kind (not @kind: table); migrate lowers it on @kind: view " +
87+
"(matview/proc/tableFunction: registered but not yet migrate-managed). Mutually " +
88+
"exclusive with @unmanaged; forbids origin.* children (two sources of truth)."),
89+
90+
// FR-024/#208 escape valve — registration only, no validation/lowering here.
91+
new AttrSchema(
92+
Name: SourceConstants.SOURCE_ATTR_UNMANAGED,
93+
ValueType: AttrConstants.ATTR_SUBTYPE_BOOLEAN,
94+
Required: false,
95+
Description: "FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / " +
96+
"a hand-migration owns its DDL). meta migrate does NOT create, drop, or drift-check " +
97+
"it; verify --db reports it as external (declared). Legal on any @kind including " +
98+
"table (the externally-managed-entity case). Mutually exclusive with @sql."),
99+
78100
// FR-015: typed-input shape for a callable source (storedProc / tableFunction).
79101
// Symmetric with template.@payloadRef — reuses object.value as the parameter shape.
80102
new AttrSchema(

server/csharp/MetaObjects/SpecMetamodel/db.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
{ "type": "attr", "subType": "string", "name": "kind", "min": 0, "max": 1, "allowedValues": ["table", "view", "materializedView", "storedProc", "tableFunction"], "description": "The kind of database object this source represents: table (default, writable), view, materializedView, storedProc, or tableFunction. Non-table kinds are read-only." },
4444
{ "type": "attr", "subType": "string", "name": "role", "min": 0, "max": 1, "allowedValues": ["primary", "replica", "index", "cache", "publish", "mirror"], "description": "Role this source plays when an object has multiple sources: primary (default, system of record), replica, index, cache, publish, or mirror." },
4545
{ "type": "attr", "subType": "string", "name": "schema", "min": 0, "max": 1, "description": "Optional database schema name (e.g. 'catalog', 'public'). Postgres defaults to 'public'; SQLite rejects any non-default value." },
46+
{ "type": "attr", "subType": "string", "name": "sql", "min": 0, "max": 1, "description": "FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS + fingerprints + drift-checks but never authors or parses. The body goes INSIDE `CREATE <kind> <physicalName> AS …` (never the CREATE wrapper, never the object name). Legal only on a read-only kind (not @kind: table); migrate lowers it on @kind: view (matview/proc/tableFunction: registered but not yet migrate-managed). Mutually exclusive with @unmanaged; forbids origin.* children (two sources of truth)." },
47+
{ "type": "attr", "subType": "boolean", "name": "unmanaged", "min": 0, "max": 1, "description": "FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / a hand-migration owns its DDL). meta migrate does NOT create, drop, or drift-check it; verify --db reports it as external (declared). Legal on any @kind including table (the externally-managed-entity case). Mutually exclusive with @sql." },
4648
{ "type": "attr", "subType": "string", "name": "parameterRef", "min": 0, "max": 1, "description": "FR-015: name or FQN of an object.value describing the input shape of this source's callable interface. Permitted on @kind: \"storedProc\" / \"tableFunction\"; rejected on non-callable kinds (table / view / materializedView). Field children of the referenced object.value become the call-site parameter list in declaration order. Symmetric with template.@payloadRef in FR-004 — the typed-input pattern reuses object.value rather than minting a new parameter.* node type." }
4749
]
4850
},

server/java/metadata/src/main/java/com/metaobjects/source/RdbSource.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.metaobjects.source;
22

3+
import com.metaobjects.attr.BooleanAttribute;
34
import com.metaobjects.attr.StringAttribute;
45
import com.metaobjects.registry.MetaDataRegistry;
56

@@ -26,6 +27,22 @@ public class RdbSource extends MetaSource {
2627
*/
2728
public static final String ATTR_PARAMETER_REF = "parameterRef";
2829

30+
/**
31+
* FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS +
32+
* fingerprints + drift-checks but never authors or parses. Legal only on a
33+
* read-only {@code @kind} (not "table"). Mutually exclusive with
34+
* {@link #ATTR_UNMANAGED}; forbids {@code origin.*} children.
35+
*/
36+
public static final String ATTR_SQL = "sql";
37+
38+
/**
39+
* FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway /
40+
* a hand-migration owns its DDL); {@code meta migrate} does not
41+
* create/drop/drift-check it. Legal on any {@code @kind} including
42+
* "table". Mutually exclusive with {@link #ATTR_SQL}.
43+
*/
44+
public static final String ATTR_UNMANAGED = "unmanaged";
45+
2946
// -----------------------------------------------------------------------
3047
// Constructor
3148
// -----------------------------------------------------------------------
@@ -93,6 +110,16 @@ public static void registerTypes(MetaDataRegistry registry) {
93110
def.optionalAttributeWithConstraints(ATTR_SCHEMA)
94111
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
95112

113+
// FR-024/#208 — @sql escape valve (hand-written SQL body); registration
114+
// only, no validation/lowering here.
115+
def.optionalAttributeWithConstraints(ATTR_SQL)
116+
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
117+
118+
// FR-024/#208 — @unmanaged escape valve (externally-managed DB object);
119+
// registration only, no validation/lowering here.
120+
def.optionalAttributeWithConstraints(ATTR_UNMANAGED)
121+
.ofType(BooleanAttribute.SUBTYPE_BOOLEAN).asSingle();
122+
96123
// FR-015: @parameterRef — call-parameters reference for callable sources.
97124
def.optionalAttributeWithConstraints(ATTR_PARAMETER_REF)
98125
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();

0 commit comments

Comments
 (0)