Skip to content

Commit 43177a9

Browse files
committed
feat(metadata): rename @dbSetName to @collectionName and add the naming.collections pluralizer
Renames the unreleased @dbSetName to @collectionName (no alias) and registers it in every port on object.entity, object.projection and object.report, marked implementedIn csharp: the generated C# context takes it as the DbSet property name. A project chooses the pluralizer an object without one takes its name from with naming.collections in .metaobjects/config.json (standard by default, none, or one the application registers); every port reads the key and refuses an unknown name. The shared fixture object-port-scoped-attr-dbsetname becomes object-collection-name, and object-port-scoped-attr-object-adapter keeps the port-scoped marker covered.
1 parent a4c515e commit 43177a9

54 files changed

Lines changed: 842 additions & 235 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
7171
- **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.
7272

7373
**Cross-port conformance corpora** (every port runs the shared corpus):
74-
- Metamodel: `fixtures/conformance/` (454 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.
74+
- 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.
7575
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
7676
- 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".)
7777
- 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.

‎CHANGELOG.md‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ _npm `1.3.0` (full lockstep across all 14 `@metaobjectsdev/*` publish candidates
1616

1717
**`metamodelVersion` moves to `1.3`.** This release adds vocabulary (`field.duration`,
1818
`validator.exactlyOne`, `@constraintName`, `@condition`, `@maxLength` on `field.enum`,
19-
`@sequenceStart`, `@sequenceName`, `@flattenedColumnPrefix`, the `pascal` column rule, and
20-
`@dbSetName`, a C#-only attr) and nothing that loaded before stops loading. The registry gains two
19+
`@sequenceStart`, `@sequenceName`, `@flattenedColumnPrefix`, the `pascal` column rule and
20+
`@collectionName`) and nothing that loaded before stops loading. The registry gains two
2121
fields on a type or attr entry, carried into the registry manifest: `implementedIn` (cross-port
2222
vocabulary only the named ports implement so far, e.g. the `view.*` controls, `["typescript"]`) and
23-
`ports` (vocabulary only the named ports ever act on, e.g. `@dbSetName`, `["csharp"]`, and
24-
`@objectAdapter`, `["java", "kotlin"]`: the JVM runtime both share acts on it). The manifest's row
23+
`ports` (vocabulary only the named ports ever act on, e.g. `@objectAdapter`, `["java", "kotlin"]`:
24+
the JVM runtime both share acts on it). The manifest's row
2525
set grows by the `implementedIn` rows that used to be carved out. A port that reports `1.2` does not
2626
know that vocabulary; upgrade every port together.
2727

@@ -50,6 +50,8 @@ Each item below is described in full under Added, Changed or Fixed; this list is
5050
- **`@sequenceStart` records a start; in 1.3.0 it does not move an existing counter.** A later
5151
release is planned to move a counter that sits below the declared start forward to it, never
5252
backward.
53+
- **`@dbSetName` is `@collectionName`.** The attr was never published under its first name, so
54+
there is no alias: a model that set `@dbSetName` in a pre-release build renames it.
5355
- **`metamodelVersion` is `1.3`.** Upgrade every port together.
5456

5557
### Added
@@ -83,22 +85,29 @@ Each item below is described in full under Added, Changed or Fixed; this list is
8385
canonical grows by those rows. `meta types` marks them `[ts; pending elsewhere]` / `[java;
8486
pending elsewhere]` (it used to say `[ts-only]`, "the cross-port contract does not carry it"; its
8587
JSON change is under Changed), and the metamodel docs now cover the view controls.
86-
`@objectAdapter` joins `@dbSetName` as port-scoped (`"ports": ["java", "kotlin"]`). C#, Java,
88+
`@objectAdapter` is port-scoped (`"ports": ["java", "kotlin"]`, below). C#, Java,
8789
Kotlin and Python now register the `metaobjects-ui-web` attrs (`view.textarea @rows` and the
8890
`view.image` attrs), and C# and Python register `@object` / `@objectAdapter`, so a model that
8991
uses any of them loads strictly in every port. Gated by the conformance fixtures
9092
`view-kind1-controls`, `object-kind1-jvm-binding-attrs` and
9193
`error-kind1-view-unregistered-attr`, and by the registry canonical.
92-
- **Port-scoped vocabulary, and `@dbSetName`.** A registry attr may name the ports
93-
that act on it (`"ports": ["csharp"]` on its `spec/metamodel` entry, carried into the registry
94+
- **Port-scoped vocabulary.** A registry attr may name the ports
95+
that act on it (`"ports": ["java", "kotlin"]` on its `spec/metamodel` entry, carried into the registry
9496
manifest). Every port registers and type-checks it, so a model that carries it loads under every
9597
port's strict loader; a port not named does nothing with it. An attr the registry does not carry
9698
still fails, and a wrong value type fails in every port (Java now refuses a list given to a scalar
97-
string attr, as the other ports did). `meta types` marks such an attr `[csharp-only]` and its JSON
98-
carries `ports`. The first one is `@dbSetName`: the EF Core DbSet property name the generated C#
99-
context gives an `object.entity`, `object.projection` or `object.report` (`"@dbSetName":
100-
"Diagnosis"`), in place of the derived plural; the routes, the reverse finders and the api
101-
reference read the property name through it.
99+
string attr, as the other ports did). `meta types` marks such an attr `[java+kotlin]` (one port:
100+
`[csharp-only]`) and its JSON carries `ports`. `@objectAdapter` is the one there is.
101+
- **`@collectionName`, and the `naming.collections` pluralizer.** An `object.entity`,
102+
`object.projection` or `object.report` may name its collection in its own casing
103+
(`"@collectionName": "CaseHistory"`); it need not be a grammatical plural. The generated C#
104+
context uses it as the DbSet property name in place of the derived plural, and the routes, the
105+
reverse finders and the api reference read the property through it. Absent, the project's
106+
pluralizer derives the name: `"naming": { "collections": "standard" }` (the default) or `"none"`
107+
in `.metaobjects/config.json`, or one the application registers (`registerPluralizer` in
108+
TypeScript, `NamingStrategies.RegisterPluralizer` in C#, `NamingStrategies.registerPluralizer` in
109+
Java, `register_pluralizer` in Python). Every port registers the attr and reads the key, and
110+
refuses an unknown pluralizer when it reads the config.
102111

103112
- **An int-backed enum can be an array.** `@intValueMap` together with `isArray: true` (the
104113
map on the field, or inherited from a shared enum) now loads in every port instead of
@@ -635,8 +644,8 @@ Each item below is described in full under Added, Changed or Fixed; this list is
635644
- **C#: one generated context maps each relation to one object.** Two concrete
636645
objects of one context that resolve to the same table, view, procedure or function in the same
637646
`@schema` are refused with `ERR_DUPLICATE_SQL_NAME`, naming both; before, they silently shared it
638-
once `@dbSetName` split their DbSet names. The same name in two schemas is two relations, and an
639-
object that extends another and inherits its source is one EF hierarchy. A `@dbSetName` that is not
647+
once `@collectionName` split their DbSet names. The same name in two schemas is two relations, and an
648+
object that extends another and inherits its source is one EF hierarchy. A `@collectionName` that is not
640649
a C# identifier, or names `Database`, `Model`, `ChangeTracker` or `ContextId`, is refused by name.
641650
- **TypeScript: the collision refusal checks only the names the run emits.** The pluralized module
642651
names are checked when entity modules, queries or a UI tier are wired, and the REST segments when
@@ -682,7 +691,7 @@ Each item below is described in full under Added, Changed or Fixed; this list is
682691
Changed.
683692
- **A generated name collides only in the scope that carries it**. The run-wide
684693
refusal of two objects whose short names pluralize alike is gone. C#: DbSet names are unique
685-
within one generated context (the error says "rename one, or set @dbSetName"), route segments
694+
within one generated context (the error says "rename one, or set @collectionName"), route segments
686695
within one routes entry, reverse finders within their class. TypeScript: the pluralized names
687696
within the entity modules' scope (one directory under the flat layout, one per package under
688697
the package layout), REST segments across the app. Python and JVM keep one run-wide scope,
@@ -858,7 +867,7 @@ Each item below is described in full under Added, Changed or Fixed; this list is
858867
cross-port wire contract and keeps the shared rule (`/api/diagnosises`) until every port
859868
changes together. An adopter that aliased the old DbSet name can drop the alias. **Upgrade
860869
note:** code that names `Diagnosises` or `FindDiagnosisesBy…` stops compiling; use the new name,
861-
or set `"@dbSetName": "Diagnosises"` on the entity to keep the old one.
870+
or set `"@collectionName": "Diagnosises"` on the entity to keep the old one.
862871

863872
- **TypeScript: a derived flattened `field.object` no longer produces flat member keys (#524
864873
follow-up).** A flattened field that carries an `origin.*` owns no column (the Drizzle table and

‎docs/CONFORMANCE.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
2626

2727
| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
2828
|---|---|---|---|---|---|---|
29-
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 454 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
29+
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 455 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
3030
| [`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 |
3131
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 45 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
3232
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
@@ -155,7 +155,7 @@ canonical (`fixtures/registry-conformance/expected-registry.json`).
155155
`registry-conformance.test.ts` in TypeScript; `RegistryManifestConformanceTest` in Java and
156156
Kotlin; `test_registry_conformance.py` in Python).
157157
- *Port-scoped, for comparison* (`ports`, language-specific by nature, never "pending"):
158-
`@dbSetName` (`["csharp"]`) and `@objectAdapter` (`["java", "kotlin"]`).
158+
`@objectAdapter` (`["java", "kotlin"]`).
159159

160160
**Native index vocabulary and `@description` in the ORM models.** `meta migrate` creates
161161
every index (`identity.secondary`, `index.lookup`, a field's `@unique`, with `@where`, `@using`,
@@ -351,7 +351,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
351351

352352
## Fixture-to-doc mapping
353353

354-
### `fixtures/conformance/` — metamodel loader + canonical serializer (454)
354+
### `fixtures/conformance/` — metamodel loader + canonical serializer (455)
355355

356356
| Fixture prefix | Feature doc |
357357
|---|---|
@@ -376,6 +376,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
376376
| `requirement-*`, `error-unknown-attr-requirement` | [features/requirements.md](features/requirements.md) (vocabulary only — the gate has its own corpus, `requirement-check-conformance/`; see "Split coverage" above) |
377377
| `reporting-*`, `error-dimension-*`, `error-measure-*`, `error-ratio-*`, `error-segment-*`, `error-report-*`, `error-relative-date-*` | [features/reporting.md](features/reporting.md) (the `dimension` / `measure` / `segment` / `object.report` vocabulary and its load-time rules; design in the [FR-044 spec](superpowers/specs/2026-10-02-fr-044-core-reporting-design.md)) |
378378
| `provider-extension-*`, `provider-custom-vocabulary-*` | [features/extending-with-providers.md](features/extending-with-providers.md) (a project's own provider: a new subtype plus attributes on an existing type load with it and are refused without it, every port writing the provider in its own language) |
379+
| `object-collection-name` | [features/migrations/physical-naming.md](features/migrations/physical-naming.md) (`@collectionName` and the `collections` pluralizer) |
379380
| `smoke-empty-metadata` | [features/entities.md](features/entities.md) |
380381

381382
### `fixtures/yaml-conformance/` (16)
@@ -712,7 +713,7 @@ JSON and verify payload feeds it unchanged.
712713

713714
## Orphaned fixtures (tested but not yet documented)
714715

715-
The fixtures in the nine corpora mapped above (metamodel 454 + yaml 16 + verify 45
716+
The fixtures in the nine corpora mapped above (metamodel 455 + yaml 16 + verify 45
716717
+ render 18 + persistence 62 + api-contract 123 + source-resolution 25 + scope 10 +
717718
dependency 47) each map to a feature doc, and so do the two requirement corpora, `shared-model-conformance`, the
718719
`cube-model` corpus and `explorer-conformance`, whose case lists live in their own READMEs. None are orphaned today. The remaining

‎docs/features/migrations/physical-naming.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ every runtime `ObjectManager` and `verify` read it, so they always produce the s
2727
|---|---|---|---|
2828
| `columns` | `snake_case`, `literal`, `kebab-case`, `pascal`, or a name your application registered | a field with no `@column` | `snake_case` |
2929
| `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` |
30+
| `collections` | `standard`, `none`, or a name your application registered | an object with no `@collectionName`: the collection name the generated C# DbSet takes | `standard` |
3031

3132
An unknown key fails when the file is read, and so does a name no registered strategy answers.
3233
Index and constraint names are unchanged.
@@ -61,7 +62,9 @@ and is `pascal` otherwise:
6162

6263
Table rules register the same way (`namingStrategies.tables`, `RegisterTables`, `registerTables`,
6364
`register_table_naming_rule`) and name a source with no physical name; the entity-name fallback
64-
is still made plural after the rule. A built-in name cannot be replaced. Register the same
65+
is still made plural after the rule. Pluralizers register the same way (`registerPluralizer`, `RegisterPluralizer`,
66+
`registerPluralizer`, `register_pluralizer`): an object name in, in its declared casing, its
67+
collection name out. `standard` is the cross-port plural and `none` keeps the name as it is. A built-in name cannot be replaced. Register the same
6568
strategy in every port that reads the database, because each port resolves the names itself.
6669
Every port looks a strategy up by name each time it applies one, so registering again under the
6770
same name replaces it for everything that runs afterwards. A test that registers its own calls

‎examples/showcase/site-payload.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"metamodel": "1.3"
88
},
99
"counts": {
10-
"fixtures": 454,
10+
"fixtures": 455,
1111
"corpora": 38,
1212
"baseTypes": 17
1313
},

‎fixtures/conformance/error-port-scoped-attr-wrong-type/input/meta.diagnosis.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"metadata.root": {
33
"package": "acme::clinical",
44
"children": [
5-
{ "object.entity": { "name": "Diagnosis", "@dbSetName": ["Diagnosis"], "children": [
5+
{ "object.entity": { "name": "Diagnosis", "@objectAdapter": ["com.acme.clinical.DiagnosisAdapter"], "children": [
66
{ "field.long": { "name": "id" } },
77
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
88
] } }

fixtures/conformance/object-port-scoped-attr-dbsetname/expected.json renamed to fixtures/conformance/object-collection-name/expected.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"object.entity": {
77
"name": "Diagnosis",
8-
"@dbSetName": "Diagnosis",
8+
"@collectionName": "Diagnosis",
99
"children": [
1010
{
1111
"source.rdb": {
@@ -51,7 +51,7 @@
5151
{
5252
"object.projection": {
5353
"name": "DiagnosisSummary",
54-
"@dbSetName": "DiagnosisSummary",
54+
"@collectionName": "DiagnosisSummary",
5555
"children": [
5656
{
5757
"source.rdb": {
@@ -70,7 +70,7 @@
7070
{
7171
"object.report": {
7272
"name": "DiagnosisCounts",
73-
"@dbSetName": "DiagnosisCounts",
73+
"@collectionName": "DiagnosisCounts",
7474
"@dimensions": [
7575
"code"
7676
],

fixtures/conformance/object-port-scoped-attr-dbsetname/input/meta.diagnosis.json renamed to fixtures/conformance/object-collection-name/input/meta.diagnosis.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
"metadata.root": {
33
"package": "acme::clinical",
44
"children": [
5-
{ "object.entity": { "name": "Diagnosis", "@dbSetName": "Diagnosis", "children": [
5+
{ "object.entity": { "name": "Diagnosis", "@collectionName": "Diagnosis", "children": [
66
{ "source.rdb": { "@table": "diagnoses" } },
77
{ "field.long": { "name": "id" } },
88
{ "field.string": { "name": "code" } },
99
{ "identity.primary": { "name": "pk", "@fields": ["id"] } },
1010
{ "dimension.attribute": { "name": "code", "@of": "Diagnosis.code" } },
1111
{ "measure.aggregate": { "name": "diagnoses", "@agg": "count", "@of": "Diagnosis.id" } }
1212
] } },
13-
{ "object.projection": { "name": "DiagnosisSummary", "@dbSetName": "DiagnosisSummary", "children": [
13+
{ "object.projection": { "name": "DiagnosisSummary", "@collectionName": "DiagnosisSummary", "children": [
1414
{ "source.rdb": { "@kind": "view", "@view": "v_diagnosis_summary" } },
1515
{ "field.string": { "name": "code" } }
1616
] } },
17-
{ "object.report": { "name": "DiagnosisCounts", "@dbSetName": "DiagnosisCounts", "@from": "Diagnosis",
17+
{ "object.report": { "name": "DiagnosisCounts", "@collectionName": "DiagnosisCounts", "@from": "Diagnosis",
1818
"@dimensions": ["code"], "@measures": ["diagnoses"], "children": [
1919
{ "source.rdb": { "@kind": "view", "@view": "v_diagnosis_counts" } }
2020
] } }

0 commit comments

Comments
 (0)