Skip to content

Commit 318840d

Browse files
dmealingclaude
andcommitted
fix(java): source+identity auto-name; emit byte-parity with TS canonical output
CanonicalJsonParser.processNode used to fall back to name = subType for every node missing an authored name -- a Java convention TS does not share. TS leaves name='' and the serializer omits the name key on emit. Java's fallback spuriously emitted a name key on every nameless source.rdb and identity.primary, breaking byte-parity with the shared conformance corpus (e.g. source.rdb without an authored name emitted "name": "rdb"; identity .primary without one emitted "name": "primary"). Targeted fix: the parser fallback now skips types in isAutoNamingType. We add MetaSource.TYPE_SOURCE and MetaIdentity.TYPE_IDENTITY to that set alongside the existing validator/view entries -- both go through createOrOverlayMetaData's sequential auto-naming (rdb1, primary1, ...) and the serializer's existing isAutoGeneratedName check then suppresses the emitted name (matching the ^<subType>\d+$ pattern), preserving the TS oracle byte form. Explicitly-named identity.secondary children (e.g. name: "byEmail") continue to emit their authored name. Also bundles a previously-uncommitted ConformanceTest improvement: the canonical-serialization-mismatch failure now includes the expected/got diff, not just a one-line label -- essential for diagnosing the next set of remaining gaps. Tests: * Adds MetaSourceTest.sourceRdbWithoutNameRoundTripsByteIdentical -- a focused round-trip smoke confirming the source.rdb body is emitted as exactly {"@table": "..."} with no name key. * Updates two existing lookups (MetaSourceTest, CanonicalJsonParserTest) that asserted the old "subType-as-name" qualified form; they now look up the auto-named form. Un-gates 31 source-v2 stage-1 conformance fixtures (now pass on the Java loader/serializer pipeline): attr-default-polymorphic, auto-set-on-create, auto-set-on-create-and-update, auto-set-on-update, enum-array, enum-inline, extends-multi-level, extends-single-level, field-decimal-precision-scale, field-object-storage-flattened, field-object-storage-flattened-nullable, field-object-storage-jsonb-array, field-object-storage-jsonb-single, field-string-maxlength, identity-primary-and-secondary, identity-reference-simple, loader-basic-explicit-subtype, loader-basic-multi-file-same-package, loader-basic-single-entity, loader-filterable-on-indexed-no-warning, overlay-attr-last-writer-wins, overlay-merge-flag-explicit, overlay-same-object-different-files, relationship-one-to-many, source-db-table-default-schema-omitted, source-db-table-explicit, source-db-table-with-schema, source-db-view-with-schema, source-rdb-column, source-rdb-referential-actions, subtype-entity-with-identity. 567/567 pass in metadata; full reactor BUILD SUCCESS across all 18 modules. The remaining 41 ledger entries represent honest gaps in Java metatype coverage (origin/layout/template/currency), validator implementations (error-* expecting specific ERR_* codes Java does not emit yet), warnings surface (no Java implementation), and the loader-root-name leak (known H3a gap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c0fc474 commit 318840d

6 files changed

Lines changed: 122 additions & 50 deletions

File tree

server/java/metadata/conformance-expected-failures.json

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
{
22
"language": "java",
33
"fixtures": [
4-
"attr-default-polymorphic",
54
"attr-filter-explicit-ops",
65
"attr-filter-shorthand",
76
"attr-properties-basic",
8-
"auto-set-on-create",
9-
"auto-set-on-create-and-update",
10-
"auto-set-on-update",
117
"currency-default-usd",
128
"currency-explicit-jpy",
139
"currency-precedence-field-vs-view",
1410
"enum-abstract-extends",
15-
"enum-array",
16-
"enum-inline",
1711
"error-attr-bad-allowed-value",
1812
"error-attr-filter-bad-field",
1913
"error-attr-filter-bad-op",
@@ -34,42 +28,17 @@
3428
"error-unknown-relationship-subtype",
3529
"extends-abstract-base",
3630
"extends-cross-file",
37-
"extends-multi-level",
38-
"extends-single-level",
39-
"field-decimal-precision-scale",
40-
"field-object-storage-flattened",
41-
"field-object-storage-flattened-nullable",
42-
"field-object-storage-jsonb-array",
43-
"field-object-storage-jsonb-single",
44-
"field-string-maxlength",
45-
"identity-primary-and-secondary",
46-
"identity-reference-simple",
4731
"layout-data-grid-basic",
4832
"layout-data-grid-multiple-named",
49-
"loader-basic-explicit-subtype",
50-
"loader-basic-multi-file-same-package",
51-
"loader-basic-single-entity",
52-
"loader-filterable-on-indexed-no-warning",
5333
"origin-aggregate-count",
5434
"origin-aggregate-sum",
5535
"origin-collection-simple",
5636
"origin-multi-level-via",
5737
"origin-passthrough-simple",
58-
"overlay-attr-last-writer-wins",
59-
"overlay-merge-flag-explicit",
60-
"overlay-same-object-different-files",
61-
"relationship-one-to-many",
6238
"smoke-empty-metadata",
63-
"source-db-table-default-schema-omitted",
64-
"source-db-table-explicit",
65-
"source-db-table-with-schema",
6639
"source-db-view-projection",
67-
"source-db-view-with-schema",
6840
"source-multi-source-roles",
69-
"source-rdb-column",
70-
"source-rdb-referential-actions",
7141
"subtype-entity-missing-primary-warning",
72-
"subtype-entity-with-identity",
7342
"template-output-and-prompt",
7443
"template-prompt-simple",
7544
"warning-filterable-no-index"

server/java/metadata/src/main/java/com/metaobjects/loader/parser/BaseMetaDataParser.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.metaobjects.registry.TypeDefinition;
1515
import com.metaobjects.registry.ChildRequirement;
1616
import com.metaobjects.relationship.MetaRelationship;
17+
import com.metaobjects.source.MetaSource;
1718
import com.metaobjects.util.MetaDataUtil;
1819
import com.metaobjects.validator.MetaValidator;
1920
import com.metaobjects.view.MetaView;
@@ -905,17 +906,28 @@ protected String determineAttributeSubType(String attrName, String stringValue)
905906
* Returns {@code true} if nodes of the given type use auto-naming (i.e. the parser
906907
* may generate a sequential name when no explicit name is provided).
907908
*
908-
* <p>Currently only {@link MetaValidator#TYPE_VALIDATOR} and {@link MetaView#TYPE_VIEW}
909-
* nodes are auto-named. All other types require an explicit name. This predicate is
910-
* the single source of truth for the auto-naming type set — both the parser and the
909+
* <p>Currently {@link MetaValidator#TYPE_VALIDATOR}, {@link MetaView#TYPE_VIEW},
910+
* {@link MetaSource#TYPE_SOURCE}, and {@link MetaIdentity#TYPE_IDENTITY} nodes are
911+
* auto-named. All other types require an explicit name. This predicate is the
912+
* single source of truth for the auto-naming type set — both the parser and the
911913
* serializer ({@link com.metaobjects.io.json.CanonicalJsonSerializer}) consume it so
912914
* the two stay in sync automatically.</p>
913915
*
916+
* <p><b>Cross-language parity:</b> The TS oracle does not synthesize {@code name = subType}
917+
* when no name is authored; it leaves the name empty and the canonical serializer omits
918+
* the {@code name} key on emit. To preserve byte-parity with the shared conformance corpus,
919+
* types listed here participate in auto-naming (sequential {@code subTypeN}) AND have the
920+
* auto-name suppressed on emit by {@code isAutoGeneratedName}. Types NOT in this set fall
921+
* back to {@code name = subType}.</p>
922+
*
914923
* @param typeName the type string as it appears in metadata (e.g. {@code "validator"})
915924
* @return {@code true} if the type participates in auto-naming
916925
*/
917926
public static boolean isAutoNamingType(String typeName) {
918-
return MetaValidator.TYPE_VALIDATOR.equals(typeName) || MetaView.TYPE_VIEW.equals(typeName);
927+
return MetaValidator.TYPE_VALIDATOR.equals(typeName)
928+
|| MetaView.TYPE_VIEW.equals(typeName)
929+
|| MetaSource.TYPE_SOURCE.equals(typeName)
930+
|| MetaIdentity.TYPE_IDENTITY.equals(typeName);
919931
}
920932

921933

server/java/metadata/src/main/java/com/metaobjects/loader/parser/json/CanonicalJsonParser.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,17 @@ private void processNode(MetaData parent, String type, String subType,
366366
// This handles identity.primary (no name → name = "primary") and any other
367367
// type that follows the subType-as-name convention. Registry-driven: no
368368
// per-type hardcoding.
369+
//
370+
// EXCEPTION: types that participate in auto-naming (validator, view, source)
371+
// skip this fallback. They receive a sequential auto-name from
372+
// createOrOverlayMetaData (rdb1, rdb2, …) and the serializer's
373+
// isAutoGeneratedName check suppresses emission of that auto-name on the way
374+
// out. This preserves byte-parity with the TS oracle, which leaves the name
375+
// empty and omits the `name` key in canonical output.
369376
if (name == null || name.isEmpty()) {
370-
name = subType;
377+
if (!BaseMetaDataParser.isAutoNamingType(type)) {
378+
name = subType;
379+
}
371380
}
372381

373382
// Create or overlay the MetaData via the format-agnostic base method.

server/java/metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ private static void runConformanceChecks(FixtureDiscovery.Fixture fix,
250250
}
251251
String got = CanonicalJsonSerializer.canonicalSerialize(loader.getRoot()).trim();
252252
if (!want.equals(got)) {
253-
failures.add("canonical serialization mismatch");
253+
failures.add("canonical serialization mismatch:\n--- expected ---\n"
254+
+ want + "\n--- got ---\n" + got);
254255
}
255256
}
256257

server/java/metadata/src/test/java/com/metaobjects/loader/parser/json/CanonicalJsonParserTest.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ public void readsCanonicalSingleEntity() {
8686
assertEquals("entity", product.getSubType());
8787
assertEquals("long", product.getChildOfType("field", "id").getSubType());
8888
assertEquals("string", product.getChildOfType("field", "name").getSubType());
89-
// Identity nodes get the document package prefix (same as other non-field children)
90-
assertNotNull(product.getChildOfType("identity", "acme::commerce::primary"));
89+
// Identity nodes are auto-named when no explicit name is authored (the canonical
90+
// serializer suppresses the auto-name on emit to match the TS oracle byte form).
91+
// First identity.primary under the entity gets auto-name "primary1", qualified
92+
// with the document package: "acme::commerce::primary1".
93+
assertNotNull(product.getChildOfType("identity", "acme::commerce::primary1"));
9194
}
9295

9396
// -----------------------------------------------------------------------
@@ -273,8 +276,10 @@ public void deserializesBareStringAsArrayForIdentityFields() {
273276
parser.loadFromStream(new ByteArrayInputStream(canonical.getBytes(StandardCharsets.UTF_8)));
274277

275278
MetaData item = loader.getRoot().getChildOfType("object", "acme::Item");
276-
// Identity nodes get the document package prefix (base parser behavior for non-field children)
277-
MetaData identity = item.getChildOfType("identity", "acme::primary");
279+
// Identity nodes are auto-named when no explicit name is authored
280+
// (sequential <subType>N — first unnamed identity.primary → "primary1").
281+
// The base parser's package-qualification applies on top.
282+
MetaData identity = item.getChildOfType("identity", "acme::primary1");
278283
assertNotNull("primary identity should exist", identity);
279284

280285
// The fields attribute should exist and hold "id"

server/java/metadata/src/test/java/com/metaobjects/source/MetaSourceTest.java

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,19 @@ public void sourceRdbIsRegistered() {
7878

7979
@Test
8080
public void rdbSourceWithTableLoads() {
81-
// When no explicit "name" key is provided, the parser uses the subType as node name.
82-
// So { "source.rdb": { "@table": "products" } } creates a node named "acme::rdb"
83-
// (package is inherited from the document default package "acme").
81+
// When no explicit "name" key is provided, source nodes participate in
82+
// auto-naming (sequential <subType>N) — the canonical serializer suppresses
83+
// the auto-name on emit so the output stays byte-identical to the TS oracle.
84+
// First source.rdb under the entity gets auto-name "rdb1", qualified with the
85+
// document's default package "acme" → "acme::rdb1".
8486
String canonical = entityWith("{ \"source.rdb\": { \"@table\": \"products\" } }");
8587
MetaDataLoader loader = loadThrough(canonical, "source-rdb-table-test.json");
8688

8789
MetaData product = loader.getRoot().getChildOfType("object", "acme::Product");
8890
assertNotNull("Product entity should exist", product);
8991

90-
// The parser qualifies the node name with the document package: "acme::rdb".
91-
MetaData src = product.getChildOfType(MetaSource.TYPE_SOURCE, "acme::rdb");
92-
assertNotNull("source.rdb child 'acme::rdb' should exist", src);
92+
MetaData src = product.getChildOfType(MetaSource.TYPE_SOURCE, "acme::rdb1");
93+
assertNotNull("source.rdb child 'acme::rdb1' should exist", src);
9394
assertEquals("subtype should be rdb", RdbSource.SUBTYPE_RDB, src.getSubType());
9495
assertTrue("src must be a MetaSource", src instanceof MetaSource);
9596
assertEquals("@table attr should be 'products'",
@@ -265,9 +266,10 @@ public void schemaRoundTripsThroughLoader() {
265266
MetaData product = loader.getRoot().getChildOfType("object", "acme::Product");
266267
assertNotNull("Product entity should exist", product);
267268

268-
// The parser qualifies the node name with the document package: "acme::rdb".
269-
MetaData src = product.getChildOfType(MetaSource.TYPE_SOURCE, "acme::rdb");
270-
assertNotNull("source.rdb child 'acme::rdb' should exist", src);
269+
// Source nodes are auto-named (sequential <subType>N); auto-name is suppressed
270+
// on canonical serialization. Lookup by the resolved qualified auto-name.
271+
MetaData src = product.getChildOfType(MetaSource.TYPE_SOURCE, "acme::rdb1");
272+
assertNotNull("source.rdb child 'acme::rdb1' should exist", src);
271273
assertTrue("src must be a MetaSource", src instanceof MetaSource);
272274
assertEquals("@schema should round-trip as 'public'",
273275
"public", ((MetaSource) src).getSchema());
@@ -364,4 +366,78 @@ public void registryCreatesRdbSource() {
364366
assertEquals("SubType must be rdb", RdbSource.SUBTYPE_RDB, src.getSubType());
365367
assertEquals("Name must be set", "registryTest", src.getName());
366368
}
369+
370+
// -----------------------------------------------------------------------
371+
// 12 — Canonical byte-parity: source.rdb with no authored name round-trips
372+
// cleanly without emitting a spurious "name" key.
373+
//
374+
// This is the regression test for the cross-language divergence where Java's
375+
// parser used to fall back to name = subType for every nameless node, causing
376+
// the canonical serializer to emit `"name": "rdb"` on every source.rdb that
377+
// wasn't authored with a name — diverging from the TS oracle by exactly that
378+
// one key.
379+
// -----------------------------------------------------------------------
380+
381+
@Test
382+
public void sourceRdbWithoutNameRoundTripsByteIdentical() throws Exception {
383+
// Authored shape — note the absence of any "name" key on the source.rdb body.
384+
// The canonical serializer must NOT emit a spurious "name" key on the
385+
// source.rdb node — that's the cross-language divergence this test guards.
386+
String authored =
387+
"{\n" +
388+
" \"metadata.root\": {\n" +
389+
" \"package\": \"acme::commerce\",\n" +
390+
" \"children\": [\n" +
391+
" {\n" +
392+
" \"object.entity\": {\n" +
393+
" \"name\": \"Customer\",\n" +
394+
" \"children\": [\n" +
395+
" { \"source.rdb\": { \"@table\": \"customers\" } },\n" +
396+
" { \"field.long\": { \"name\": \"id\" } },\n" +
397+
" { \"identity.primary\": { \"@fields\": [\"id\"] } }\n" +
398+
" ]\n" +
399+
" }\n" +
400+
" }\n" +
401+
" ]\n" +
402+
" }\n" +
403+
"}";
404+
405+
MetaDataLoader loader = loadThrough(authored, "source-rdb-byte-parity-test.json");
406+
407+
String got =
408+
com.metaobjects.io.json.CanonicalJsonSerializer.canonicalSerialize(
409+
loader.getRoot()).trim();
410+
411+
// The root-level divergence guard: no "name": "rdb..." anywhere in the
412+
// serialized output. Before the fix, this string would appear inside
413+
// the source.rdb body as "name": "rdb".
414+
assertFalse(
415+
"Serializer must NOT emit a spurious 'name' key for a nameless source.rdb:\n" + got,
416+
got.contains("\"name\": \"rdb"));
417+
418+
// Extract the source.rdb body and assert it is exactly {"@table": "customers"}.
419+
com.google.gson.JsonObject gotRoot =
420+
com.google.gson.JsonParser.parseString(got).getAsJsonObject();
421+
com.google.gson.JsonObject metadataRoot =
422+
gotRoot.getAsJsonObject("metadata.root");
423+
com.google.gson.JsonArray rootChildren = metadataRoot.getAsJsonArray("children");
424+
com.google.gson.JsonObject entity = rootChildren.get(0).getAsJsonObject()
425+
.getAsJsonObject("object.entity");
426+
com.google.gson.JsonArray entityChildren = entity.getAsJsonArray("children");
427+
428+
com.google.gson.JsonObject sourceBody = null;
429+
for (com.google.gson.JsonElement child : entityChildren) {
430+
com.google.gson.JsonObject wrapper = child.getAsJsonObject();
431+
if (wrapper.has("source.rdb")) {
432+
sourceBody = wrapper.getAsJsonObject("source.rdb");
433+
break;
434+
}
435+
}
436+
assertNotNull("serialized output must contain a source.rdb child", sourceBody);
437+
assertFalse("source.rdb body must NOT carry a 'name' key", sourceBody.has("name"));
438+
assertEquals("source.rdb body must carry only @table",
439+
"customers", sourceBody.get("@table").getAsString());
440+
assertEquals("source.rdb body must be exactly {@table: ...}",
441+
1, sourceBody.size());
442+
}
367443
}

0 commit comments

Comments
 (0)