Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.

**Cross-port conformance corpora** (every port runs the shared corpus):
- Metamodel: `fixtures/conformance/` (455 fixtures; 38 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Metamodel: `fixtures/conformance/` (458 fixtures; 38 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ Each item below is described in full under Added, Changed or Fixed; this list is
TypeScript, `NamingStrategies.RegisterPluralizer` in C#, `NamingStrategies.registerPluralizer` in
Java, `register_pluralizer` in Python). Every port registers the attr and reads the key, and
refuses an unknown pluralizer when it reads the config.
- **Reverse `@via` hops.** An origin's `@via` hop may walk an `identity.reference` backwards, from
the entity it targets to the entity holding it, named by the holder's collection name in lower
camel case (`Patient.consents` walks `Consent.patient`; a declared `"@collectionName":
"CaseHistory"` gives `Case.caseHistory`). The hop is to-many, so a passthrough across it is
refused and an aggregate over it is allowed, and two references that answer one name are refused
(`ERR_AMBIGUOUS_PATH`). A declared relationship or reference of the same name still wins. Every
port's loader implements it, gated by `origin-reverse-reference-hop`,
`origin-reverse-hop-collection-name` and `error-origin-reverse-hop-ambiguous`.

- **An int-backed enum can be an array.** `@intValueMap` together with `isArray: true` (the
map on the field, or inherited from a shared enum) now loads in every port instead of
Expand Down
6 changes: 3 additions & 3 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor

| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
|---|---|---|---|---|---|---|
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 455 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 458 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 16 | 16 / 16 | 15 / 16 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 15 / 16 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 16 / 16 |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 45 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
Expand Down Expand Up @@ -354,7 +354,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.

## Fixture-to-doc mapping

### `fixtures/conformance/` — metamodel loader + canonical serializer (455)
### `fixtures/conformance/` — metamodel loader + canonical serializer (458)

| Fixture prefix | Feature doc |
|---|---|
Expand Down Expand Up @@ -716,7 +716,7 @@ JSON and verify payload feeds it unchanged.

## Orphaned fixtures (tested but not yet documented)

The fixtures in the nine corpora mapped above (metamodel 455 + yaml 16 + verify 45
The fixtures in the nine corpora mapped above (metamodel 458 + yaml 16 + verify 45
+ render 18 + persistence 62 + api-contract 123 + source-resolution 25 + scope 10 +
dependency 47) each map to a feature doc, and so do the two requirement corpora, `shared-model-conformance`, the
`cube-model` corpus and `explorer-conformance`, whose case lists live in their own READMEs. None are orphaned today. The remaining
Expand Down
2 changes: 1 addition & 1 deletion docs/features/migrations/physical-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ every runtime `ObjectManager` and `verify` read it, so they always produce the s
|---|---|---|---|
| `columns` | `snake_case`, `literal`, `kebab-case`, `pascal`, or a name your application registered | a field with no `@column` | `snake_case` |
| `tables` | `snake_case`, `literal`, or a name your application registered | a source with no `@table`/`@view`/`@proc`/`@function`: its `name`, else the object's name made plural | `snake_case` |
| `collections` | `standard`, `none`, or a name your application registered | an object with no `@collectionName`: the collection name the generated C# DbSet takes | `standard` |
| `collections` | `standard`, `none`, or a name your application registered | an object with no `@collectionName`: the collection name the generated C# DbSet takes and a backwards `@via` hop is named by | `standard` |

An unknown key fails when the file is read, and so does a name no registered strategy answers.
Index and constraint names are unchanged.
Expand Down
10 changes: 6 additions & 4 deletions docs/features/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,12 @@ reverse-engineered model can `@via` its reference directly:
```

A reference hop is inherently **to-one** (a child names the parent it points at), so it
is valid in a `passthrough` and rejected in an `aggregate`. Inverse navigation
(parent → many children) still needs a `relationship.composition` on the parent — a bare
FK has no inverse edge. Explicit `@via` resolves either kind; single-hop-unique
inference stays relationship-only.
is valid in a `passthrough` and rejected in an `aggregate`. An origin `@via` hop may
also walk an `identity.reference` **backwards** (parent → many children), named by the
holder's collection name in lowerCamel (`Patient.consents`). A reverse hop is to-many, so
it is valid in an `aggregate` and rejected in a `passthrough` (`ERR_ORIGIN_CARDINALITY`);
two candidate holders give `ERR_AMBIGUOUS_PATH`, and a declared relationship or reference
of the same name wins. Single-hop-unique inference stays relationship-only.

## When one entity has two references to the same target

Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/site-payload.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"metamodel": "1.3"
},
"counts": {
"fixtures": 455,
"fixtures": 458,
"corpora": 38,
"baseTypes": 17
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"errors": [
{
"code": "ERR_AMBIGUOUS_PATH",
"source": {
"format": "resolved",
"files": [
"meta.care.json"
],
"jsonPath": "$['metadata.root'].children[2]['object.projection'].children[2]['field.int'].children[0]['origin.aggregate']",
"referrer": "PatientConsentSummary::consentCount",
"target": "acme::care::Patient.consents"
}
}
],
"warnings": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{
"metadata.root": {
"package": "acme::care",
"children": [
{
"object.entity": {
"name": "Patient",
"children": [
{
"source.rdb": {
"@table": "patients"
}
},
{
"field.long": {
"name": "id"
}
},
{
"identity.primary": {
"name": "id",
"@fields": "id"
}
}
]
}
},
{
"object.entity": {
"name": "Consent",
"children": [
{
"source.rdb": {
"@table": "consents"
}
},
{
"field.long": {
"name": "id"
}
},
{
"field.long": {
"name": "patientId"
}
},
{
"field.boolean": {
"name": "onFile"
}
},
{
"field.long": {
"name": "witnessId"
}
},
{
"identity.primary": {
"name": "id",
"@fields": "id"
}
},
{
"identity.reference": {
"name": "patient",
"@fields": "patientId",
"@references": "acme::care::Patient"
}
},
{
"identity.reference": {
"name": "witness",
"@fields": "witnessId",
"@references": "acme::care::Patient"
}
}
]
}
},
{
"object.projection": {
"name": "PatientConsentSummary",
"children": [
{
"source.rdb": {
"@kind": "view",
"@view": "v_patient_consent_summary"
}
},
{
"field.long": {
"name": "id",
"extends": "acme::care::Patient.id"
}
},
{
"field.int": {
"name": "consentCount",
"children": [
{
"origin.aggregate": {
"@agg": "count",
"@of": "acme::care::Consent.id",
"@via": "acme::care::Patient.consents"
}
}
]
}
},
{
"identity.primary": {
"name": "id",
"extends": "acme::care::Patient.id"
}
}
]
}
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["metaobjects-core-types","metaobjects-db"]
158 changes: 158 additions & 0 deletions fixtures/conformance/origin-reverse-hop-collection-name/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
{
"metadata.root": {
"package": "acme::cases",
"children": [
{
"object.entity": {
"name": "Case",
"children": [
{
"source.rdb": {
"@table": "cases"
}
},
{
"field.long": {
"name": "id"
}
},
{
"identity.primary": {
"name": "id",
"@fields": [
"id"
]
}
}
]
}
},
{
"object.entity": {
"name": "CaseHistory",
"@collectionName": "CaseHistory",
"children": [
{
"source.rdb": {
"@table": "case_history"
}
},
{
"field.long": {
"name": "id"
}
},
{
"field.long": {
"name": "caseId"
}
},
{
"field.string": {
"name": "status"
}
},
{
"field.timestamp": {
"name": "createdDate"
}
},
{
"identity.primary": {
"name": "id",
"@fields": [
"id"
]
}
},
{
"identity.reference": {
"name": "caseId",
"@fields": [
"caseId"
],
"@references": "acme::cases::Case"
}
},
{
"dimension.attribute": {
"name": "status",
"@of": "acme::cases::CaseHistory.status"
}
},
{
"measure.aggregate": {
"name": "entries",
"@agg": "count",
"@of": [
"acme::cases::CaseHistory.id"
]
}
}
]
}
},
{
"object.projection": {
"name": "CaseActivity",
"@collectionName": "CaseActivity",
"children": [
{
"source.rdb": {
"@kind": "view",
"@view": "v_case_activity"
}
},
{
"field.long": {
"name": "id",
"extends": "acme::cases::Case.id"
}
},
{
"field.int": {
"name": "historyCount",
"children": [
{
"origin.aggregate": {
"@agg": "count",
"@of": "acme::cases::CaseHistory.id",
"@via": "acme::cases::Case.caseHistory"
}
}
]
}
},
{
"identity.primary": {
"name": "id",
"extends": "acme::cases::Case.id"
}
}
]
}
},
{
"object.report": {
"name": "HistoryCounts",
"@collectionName": "HistoryCounts",
"@dimensions": [
"status"
],
"@from": "acme::cases::CaseHistory",
"@measures": [
"entries"
],
"children": [
{
"source.rdb": {
"@kind": "view",
"@view": "v_history_counts"
}
}
]
}
}
]
}
}
Loading
Loading