Skip to content

Commit b160a59

Browse files
committed
Merge worktree-fr5e-wrapup-2026-05-27 — FR5e wrap-up (envelope schema lock + per-port shape tests + design closure)
2 parents f556398 + abd0f4c commit b160a59

5 files changed

Lines changed: 273 additions & 29 deletions

File tree

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
1-
# FR5e — Database-source loader errors (sketch, future)
1+
# FR5e — Database-source loader errors
22

3-
**Status:** Forward-looking — gated on FR-003 (OMDB persistence engine)
4-
**Date:** 2026-05-25
5-
**Scope:** Java first (FR-003 is the Java OMDB persistence engine), then any port that
6-
gains database-sourced metadata loading.
7-
**Depends on:** [ADR-0009](../../../spec/decisions/ADR-0009-loader-error-envelope-and-source-on-node.md) (the `format: "database"` slot is already reserved), FR-003 (database-loader implementation).
3+
**Status:** Schema reserved + cross-port envelope shape locked (2026-05-27). Awaiting a real database-source loader.
4+
**Date:** 2026-05-25 (sketched), 2026-05-27 (design questions resolved + per-port shape tests added)
5+
**Scope:** Java first (FR-003 ships the OMDB persistence engine), then any port that gains database-sourced metadata loading.
6+
**Depends on:** [ADR-0009](../../../spec/decisions/ADR-0009-loader-error-envelope-and-source-on-node.md) (the `format: "database"` slot was always reserved).
7+
8+
## What's shipped
9+
10+
The envelope schema is fully locked across all four ports:
11+
12+
| Port | Source type | Location |
13+
|---------|----------------------------|----------|
14+
| TS | `format: "database"; dbLocation: { table, id }; jsonPath?` | `server/typescript/packages/metadata/src/source.ts` |
15+
| Java | `record DatabaseSource(DbLocation dbLocation, String jsonPath)` + `record DbLocation(String table, String id)` | `server/java/metadata/src/main/java/com/metaobjects/source/` |
16+
| C# | `sealed record DatabaseSource(DbLocation DbLocation, string? JsonPath)` + `sealed record DbLocation(string Table, string Id)` | `server/csharp/MetaObjects/Source/ErrorSource.cs` |
17+
| Python | `@dataclass(frozen=True) DatabaseSource(db_location, json_path)` + `@dataclass(frozen=True) DbLocation(table, id)` | `server/python/src/metaobjects/source/error_source.py` |
18+
19+
Per-port shape tests pin the envelope:
20+
21+
- TS: `server/typescript/packages/metadata/test/source.test.ts``describe("ErrorSource — database variant (FR5e schema lock)")`
22+
- Java: `server/java/metadata/src/test/java/com/metaobjects/source/DatabaseSourceShapeTest.java`
23+
- C#: `server/csharp/MetaObjects.Conformance.Tests/Fr5eDatabaseSourceShapeTests.cs`
24+
- Python: `server/python/tests/source/test_fr5e_database_source_shape.py`
25+
26+
A future database-source loader (FR-003 deliberately did NOT ship one — OMDB persists *user data*, not *metamodel*) has a guaranteed-correct envelope target.
827

928
## Goal
1029

11-
When metadata is loaded from a database (instead of files), error envelopes report
12-
the database location of the offending record:
30+
When metadata is loaded from a database (instead of files), error envelopes report the database location of the offending record:
1331

1432
```jsonc
1533
{
@@ -26,33 +44,48 @@ the database location of the offending record:
2644
}
2745
```
2846

29-
## Why this exists now as a sketch
47+
## Design lock (resolved 2026-05-27)
3048

31-
ADR-0009 reserves `format: "database"` in the envelope so the schema is forward-stable.
32-
FR5e is the placeholder for the implementation FR that lands alongside FR-003. No work
33-
happens until FR-003 ships.
49+
The three open questions from the original sketch are settled:
3450

35-
## Open design questions (deferred)
51+
1. **`dbLocation` shape: `{ table: string, id: string }` — locked.**
52+
Composite primary keys are encoded into `id` as a single delimited string
53+
(e.g. `"fld_abc123/currency"` — row id + attr name joined with `/`). Reasons:
54+
- Keeps the envelope dialect-neutral. Composite-key schemas vary by RDBMS;
55+
a richer `dbLocation` shape would bake one schema into the envelope.
56+
- Matches the pattern that JSON pointers / JSONPath already use for
57+
nested addressing.
58+
- Future loaders can pick the delimiter convention; the envelope just
59+
records the string.
3660

37-
All of these wait for FR-003 to settle:
61+
2. **Relationship between `dbLocation` and `jsonPath`: `jsonPath` is OPTIONAL.**
62+
When metadata is database-sourced, the JSONPath is a derived projection
63+
over the row's payload. `dbLocation` is the primary locator; `jsonPath`
64+
adds precision when the offending node is nested within a row's
65+
payload (e.g. inside a JSONB blob of attributes).
3866

39-
1. **`dbLocation` shape** — is `{ table, id }` enough, or do we need composite keys?
40-
FR-003's schema dictates this.
41-
2. **Relationship between `dbLocation` and `jsonPath`** — when metadata is database-
42-
sourced, the JSONPath is a derived projection. Is `jsonPath` optional or required?
43-
3. **Multi-source metadata** — what if a single load aggregates from files + database?
44-
The envelope discriminator allows it; do consumers need a `format: "mixed"` variant?
67+
3. **Mixed-source metadata: NOT NEEDED in the envelope.**
68+
Each error carries the source format of the offending node, not the
69+
overall load. A mixed file+database load just produces errors with the
70+
appropriate per-error `format`. No `format: "mixed"` variant required.
4571

46-
## What FR5e WON'T do
72+
## Out of scope
4773

48-
- Define the OMDB schema or persistence semantics — that's FR-003.
49-
- Define how database-sourced metadata is overlaid with file-sourced metadata — FR-003
50-
again.
74+
- The OMDB metadata-table schema itself (which tables hold which metadata
75+
rows) — that's a separate FR when a real database-source loader is built.
76+
- The relationship between database-sourced and file-sourced metadata
77+
overlay/merge — same.
78+
- A reference database-source loader implementation — also separate.
5179

52-
## Open questions
80+
## Implementing the loader (future, separate FR)
5381

54-
To be settled during brainstorm when FR-003 nears delivery:
82+
When a database-source loader is built, the implementer must:
5583

56-
1. Database location shape (table + id vs richer composite).
57-
2. Mixed-source error attribution.
58-
3. Whether to ship FR5e as part of FR-003 or as a follow-on.
84+
1. Emit `DatabaseSource` envelopes from every error / warning path.
85+
2. Use the existing parser/super-resolution/validation pipeline (same as the
86+
file-source loaders).
87+
3. Add a conformance fixture under `fixtures/conformance/` that exercises a
88+
`format: "database"` error end-to-end (will require a synthetic test loader
89+
or an in-memory schema for the metaobjects tables).
90+
4. Decide overlay semantics with file-source metadata (will require a
91+
separate ADR — this is a real cross-language decision).
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// FR5e — cross-port envelope shape lock for `format: "database"`.
2+
//
3+
// The schema is reserved by ADR-0009 and instantiable in every port today;
4+
// a real database-source loader is future work. This test pins the shape so
5+
// a future loader (FR5e implementation) has a guaranteed-correct target.
6+
7+
using MetaObjects.Source;
8+
using Xunit;
9+
10+
namespace MetaObjects.Conformance.Tests;
11+
12+
public class Fr5eDatabaseSourceShapeTests
13+
{
14+
[Fact]
15+
public void FormatIsDatabase()
16+
{
17+
var src = new DatabaseSource(new DbLocation("metaobjects_field_attr", "fld_abc123/currency"));
18+
Assert.Equal("database", src.Format);
19+
}
20+
21+
[Fact]
22+
public void DbLocationCarriesTableAndId()
23+
{
24+
var loc = new DbLocation("metaobjects_object", "obj_42");
25+
Assert.Equal("metaobjects_object", loc.Table);
26+
Assert.Equal("obj_42", loc.Id);
27+
}
28+
29+
[Fact]
30+
public void JsonPathIsOptional()
31+
{
32+
var noPath = new DatabaseSource(new DbLocation("t", "id"));
33+
Assert.Null(noPath.JsonPath);
34+
35+
var withPath = new DatabaseSource(
36+
new DbLocation("metaobjects_field_attr", "fld_abc123/currency"),
37+
"$.metadata.root.children[0].field.currency.@currency");
38+
Assert.NotNull(withPath.JsonPath);
39+
Assert.Equal("$.metadata.root.children[0].field.currency.@currency", withPath.JsonPath);
40+
}
41+
42+
[Fact]
43+
public void CompositeKeyEncodedInIdString()
44+
{
45+
// FR5e design lock: composite primary keys are encoded as a single
46+
// delimited string in the id, e.g. "row_id/attr_name". This keeps the
47+
// envelope schema dialect-neutral and avoids a deeper DbLocation shape.
48+
var loc = new DbLocation("metaobjects_field_attr", "fld_abc123/currency");
49+
Assert.Equal("fld_abc123/currency", loc.Id);
50+
}
51+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2026 Doug Mealing LLC. All Rights Reserved.
3+
*
4+
* FR5e — cross-port envelope shape lock for `format: "database"`. The schema
5+
* is reserved by ADR-0009 and instantiable in every port today; a real
6+
* database-source loader is future work. This test pins the shape so a
7+
* future loader (FR5e implementation) has a guaranteed-correct target.
8+
*/
9+
package com.metaobjects.source;
10+
11+
import org.junit.Test;
12+
13+
import static org.junit.Assert.assertEquals;
14+
import static org.junit.Assert.assertNotNull;
15+
import static org.junit.Assert.assertNull;
16+
import static org.junit.Assert.assertThrows;
17+
18+
public class DatabaseSourceShapeTest {
19+
20+
@Test
21+
public void formatIsDatabase() {
22+
DatabaseSource src = new DatabaseSource(new DbLocation("metaobjects_field_attr", "fld_abc123/currency"));
23+
assertEquals("database", src.format());
24+
}
25+
26+
@Test
27+
public void dbLocationCarriesTableAndId() {
28+
DbLocation loc = new DbLocation("metaobjects_object", "obj_42");
29+
assertEquals("metaobjects_object", loc.table());
30+
assertEquals("obj_42", loc.id());
31+
}
32+
33+
@Test
34+
public void jsonPathIsOptional() {
35+
DatabaseSource noPath = new DatabaseSource(new DbLocation("t", "id"));
36+
assertNull(noPath.jsonPath());
37+
38+
DatabaseSource withPath = new DatabaseSource(
39+
new DbLocation("metaobjects_field_attr", "fld_abc123/currency"),
40+
"$.metadata.root.children[0].field.currency.@currency");
41+
assertNotNull(withPath.jsonPath());
42+
assertEquals("$.metadata.root.children[0].field.currency.@currency", withPath.jsonPath());
43+
}
44+
45+
@Test
46+
public void compositeKeyEncodedInIdString() {
47+
// FR5e design lock: composite primary keys are encoded as a single
48+
// delimited string in the id, e.g. "row_id/attr_name". This keeps the
49+
// envelope schema dialect-neutral and avoids a deeper DbLocation shape.
50+
DbLocation loc = new DbLocation("metaobjects_field_attr", "fld_abc123/currency");
51+
assertEquals("fld_abc123/currency", loc.id());
52+
}
53+
54+
@Test
55+
public void rejectsNullDbLocation() {
56+
assertThrows(NullPointerException.class,
57+
() -> new DatabaseSource(null, null));
58+
}
59+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""FR5e — cross-port envelope shape lock for ``format: "database"``.
2+
3+
The schema is reserved by ADR-0009 and instantiable in every port today;
4+
a real database-source loader is future work. This test pins the shape so
5+
a future loader (FR5e implementation) has a guaranteed-correct target.
6+
"""
7+
from __future__ import annotations
8+
9+
import pytest
10+
11+
from metaobjects.source import DatabaseSource, DbLocation
12+
13+
14+
def test_format_is_database():
15+
src = DatabaseSource(db_location=DbLocation(table="metaobjects_field_attr",
16+
id="fld_abc123/currency"))
17+
assert src.format == "database"
18+
19+
20+
def test_db_location_carries_table_and_id():
21+
loc = DbLocation(table="metaobjects_object", id="obj_42")
22+
assert loc.table == "metaobjects_object"
23+
assert loc.id == "obj_42"
24+
25+
26+
def test_json_path_is_optional():
27+
no_path = DatabaseSource(db_location=DbLocation(table="t", id="id"))
28+
assert no_path.json_path is None
29+
30+
with_path = DatabaseSource(
31+
db_location=DbLocation(table="metaobjects_field_attr", id="fld_abc123/currency"),
32+
json_path="$.metadata.root.children[0].field.currency.@currency",
33+
)
34+
assert with_path.json_path == "$.metadata.root.children[0].field.currency.@currency"
35+
36+
37+
def test_composite_key_encoded_in_id_string():
38+
"""FR5e design lock: composite primary keys are encoded as a single
39+
delimited string in the id, e.g. ``"row_id/attr_name"``. This keeps the
40+
envelope schema dialect-neutral and avoids a deeper DbLocation shape.
41+
"""
42+
loc = DbLocation(table="metaobjects_field_attr", id="fld_abc123/currency")
43+
assert loc.id == "fld_abc123/currency"
44+
45+
46+
def test_frozen_dataclass_is_immutable():
47+
src = DatabaseSource(db_location=DbLocation(table="t", id="id"))
48+
with pytest.raises(Exception): # FrozenInstanceError
49+
src.json_path = "/nope" # type: ignore[misc]

server/typescript/packages/metadata/test/source.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,55 @@ describe("LoaderWarning envelope shape", () => {
5454
expect(w.code.startsWith("WARN_")).toBe(true);
5555
});
5656
});
57+
58+
// FR5e — cross-port envelope shape lock for `format: "database"`. The schema
59+
// is reserved by ADR-0009 and instantiable in every port today; a real
60+
// database-source loader is future work. These tests pin the shape so a
61+
// future loader has a guaranteed-correct target.
62+
describe("ErrorSource — database variant (FR5e schema lock)", () => {
63+
test("format is 'database', dbLocation carries table + id", () => {
64+
const s: ErrorSource = {
65+
format: "database",
66+
dbLocation: { table: "metaobjects_field_attr", id: "fld_abc123/currency" },
67+
};
68+
expect(s.format).toBe("database");
69+
if (s.format === "database") {
70+
expect(s.dbLocation.table).toBe("metaobjects_field_attr");
71+
expect(s.dbLocation.id).toBe("fld_abc123/currency");
72+
}
73+
});
74+
75+
test("jsonPath is optional", () => {
76+
const noPath: ErrorSource = {
77+
format: "database",
78+
dbLocation: { table: "t", id: "id" },
79+
};
80+
if (noPath.format === "database") {
81+
expect(noPath.jsonPath).toBeUndefined();
82+
}
83+
84+
const withPath: ErrorSource = {
85+
format: "database",
86+
dbLocation: { table: "metaobjects_field_attr", id: "fld_abc123/currency" },
87+
jsonPath: "$.metadata.root.children[0].field.currency.@currency",
88+
};
89+
if (withPath.format === "database") {
90+
expect(withPath.jsonPath).toBe(
91+
"$.metadata.root.children[0].field.currency.@currency",
92+
);
93+
}
94+
});
95+
96+
test("composite key is encoded as a delimited string in id", () => {
97+
// FR5e design lock: composite primary keys go through `id` as
98+
// "row_id/attr_name". Keeps the envelope dialect-neutral and avoids a
99+
// deeper DbLocation shape.
100+
const s: ErrorSource = {
101+
format: "database",
102+
dbLocation: { table: "metaobjects_field_attr", id: "fld_abc123/currency" },
103+
};
104+
if (s.format === "database") {
105+
expect(s.dbLocation.id).toBe("fld_abc123/currency");
106+
}
107+
});
108+
});

0 commit comments

Comments
 (0)