Skip to content

feat: add exactlyOne validator, int-backed enum arrays and naming strategies - #554

Merged
dmealing merged 7 commits into
mainfrom
fm/mo-workpc-replay-pr1
Nov 11, 2026
Merged

dmealing merged 7 commits into
mainfrom
fm/mo-workpc-replay-pr1

Conversation

@dmealing

@dmealing dmealing commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Adds metamodel vocabulary across the ports: validator.exactlyOne, @constraintName, @condition, field.duration, @sequenceStart (the counter is never moved on an existing sequence), @dbSetName (C#-only, via a new port-scoped ports field in the registry), and the pascal column rule. validator.atLeastOne no longer takes @max; upgrading adopters must switch to validator.exactlyOne and remove the @max.
  • Lets an @intValueMap enum be an array (ERR_ENUM_INT_VALUE_MAP_ARRAY removed). TypeScript and Python map elements on write and read; C# emits working array converters; Java and Kotlin refuse or defer the case, recorded in KNOWN_GAPS.md. C# durations need "wire": { "durations": "iso" } to keep ISO-8601 output.
  • Adds named naming strategies ("naming": { "columns": "<name>" }) that each port's application can register, plus shared conformance fixtures and persistence/api-contract scenarios for these changes.

Known limits

  • Kotlin: the Exposed table generator emits a scalar column for an enum array, so generated code for an int-backed enum array does not compile yet. The compile gates leave out IntEnumArrayController.kt and its repository files. This gap predates this PR; see KNOWN_GAPS.md.
  • The reserved-vocabulary changes are not in this PR. They land in a follow-up together with the Java, Kotlin and Python registry catch-up.

Gate

Validated in 2 no-mistakes runs. Run 1 passed. After it, the hosted csharp check failed: the golden tests found their files through [CallerFilePath], and a CI build (CI=true turns on ContinuousIntegrationBuild) maps source paths to /_/. Run 2 validated that fix (the golden files are now found from the repo root). Run 2's test step ended with an approved exception. Its live scenarios drove TypeScript only. Every port's full suite, including the integration suites, ran to completion locally before the pipeline. Hosted CI re-runs every port on this push.

Validation

Risk Assessment

⚠️ Medium: Re-review found no defect: the only change since the starting head is a wording fix in CHANGELOG.md, and my spot checks of the earlier fix rounds held up. The change is still very large (367 files across five ports), and I read only the paths listed below, so medium reflects scale rather than a known problem.

Testing

Baseline ts-fast and ts-unit passed earlier. Live Postgres scenarios pass in TypeScript: enum @default on create, int-backed enum array round-trip, @Schema on reads and writes (with the same-name table scenario), and @sequenceStart never moving the counter. A runtime mutation dropping @Schema on insert was caught and reverted. Not finished: the full integration-tests regression was stopped before it reported, and C#, Java, Kotlin and Python ports were not driven live. Verdict is inconclusive.

  • Live validation: ⚠️ inconclusive - 5 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Enum @default on create: omitted field stores the default (TS, live Postgres, generated create + Drizzle + ObjectManager) ✅ pass live pg-scenarios-1.log
Int-backed enum array round-trip (TS, live Postgres) ✅ pass live pg-scenarios-1.log
@Schema reaches reads and writes; same-named tables in two schemas stay separate (TS, live Postgres) ✅ pass live pg-scenarios-1.log, pg-scenarios-2.log
Adversarial: insert drops @Schema and writes to the default table ✅ pass live mutation-schema-drop-insert.log
@sequenceStart never moves a live counter; TS migrate only ALTERs START WITH (live Postgres) ✅ pass live pg-scenarios-2.log
C# @Schema write path and enum default on create ⏸️ untested no Not driven. dotnet is on PATH; the C# integration tests were not run in this session. Needs a dotnet test run against Postgres.
Java and Kotlin @Schema write path, enum default, int enum array ⏸️ untested no Not driven. java is on PATH; the Java and Kotlin persistence conformance lanes were not run in this session.
Python @Schema write path and enum default ⏸️ untested no Not driven. The Python conformance lane was not run in this session.
Full TypeScript integration-tests regression ⏸️ untested no Run killed before completion; the log has no summary and ends exit=143. The background task notification showing exit code 0 reflects the wrapper after the kill, not a test result.
Evidence: PG scenario run 1 (enum default, int enum array, api-contract enum default, query)
bun test v1.3.14 (0d9b296a)

 70 pass
 0 fail
 41 expect() calls
Ran 70 tests across 4 files. [374.50s]
Evidence: PG scenario run 2 (sequence-start migration, schema-qualified query)
bun test v1.3.14 (0d9b296a)

 2 pass
 5 filtered out
 0 fail
Ran 2 tests across 1 file. [8.12s]
---
bun test v1.3.14 (0d9b296a)

 1 pass
 54 filtered out
 0 fail
Ran 1 test across 1 file. [3.08s]
Evidence: Mutation: schema dropped on insert fails schema-qualified-same-name
bun test v1.3.14 (0d9b296a)

test/query.test.ts:
275 | 
276 | function assertResult(scenarioPath: string, spec: QuerySpec, actual: unknown, engine: string): void {
277 |   const expectedJson = canonicalizeExpected(spec.expect, spec.op);
278 |   const actualJson = canonicalizeActual(actual, spec.op);
279 |   if (expectedJson !== actualJson)
280 |     throw new Error(
                    ^
error: {run-worktree}/fixtures/persistence-conformance/queries/schema-qualified-same-name.yaml / roundtrip-writes-the-declared-schema: result mismatch on postgres
  expected: {"memo":"archived-new"}
  actual:   null
      at assertResult ({run-worktree}/server/typescript/packages/integration-tests/src/query-scenario.ts:280:15)
      at runScenarioQueries ({run-worktree}/server/typescript/packages/integration-tests/src/query-scenario.ts:109:5)
      at async runQueryScenario ({run-worktree}/server/typescript/packages/integration-tests/src/query-scenario.ts:71:11)
      at async <anonymous> ({run-worktree}/server/typescript/packages/integration-tests/test/query.test.ts:14:15)
(fail) persistence conformance — query scenarios > schema-qualified-same-name [4510.25ms]

 0 pass
 54 filtered out
 1 fail
Ran 1 test across 1 file. [4.72s]
Evidence: Full integration-tests run, killed before completion (exit=143)
bun test v1.3.14 (0d9b296a)
exit=143
stopped-by-test-phase: full regression interrupted before exit
  • Outcome: ⚠️ 3 issues (1 error, 2 warnings) across 1 run (28m20s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info

✅ No issues found.

  • ℹ️ server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KNOWN_GAPS.md:231 - Admitting @intValueMap + isArray in every port leaves Kotlin with a build-time failure in place of the old load-time error. KotlinExposedTableGenerator.enumColumnSpec emits a scalar column for any enum array, string-backed or int-backed. The generated controller then fails to compile, and the fitness corpus' IntEnumArrayController.kt is left out of the compile gates. Java OMDB refuses an int-backed array at runtime on write and read. KNOWN_GAPS.md and docs/ports/kotlin.md describe this honestly. The CHANGELOG line "Kotlin's Exposed generator emits no enum array column" is looser: it emits a scalar column and the build fails. Fixing it means adding an Exposed array column, which is a feature beyond this replay. Recorded as a known, documented gap, not a blocker.

🔧 Fix applied.
1 info still open:

  • ℹ️ server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KNOWN_GAPS.md:231 - Admitting @intValueMap + isArray in every port leaves Kotlin with a build-time failure in place of the old load-time error. KotlinExposedTableGenerator.enumColumnSpec emits a scalar column for any enum array, string-backed or int-backed. The generated controller then fails to compile, and the fitness corpus' IntEnumArrayController.kt is left out of the compile gates. Java OMDB refuses an int-backed array at runtime on write and read. KNOWN_GAPS.md and docs/ports/kotlin.md describe this honestly. The CHANGELOG line "Kotlin's Exposed generator emits no enum array column" is looser: it emits a scalar column and the build fails. Fixing it means adding an Exposed array column, which is a feature beyond this replay. Recorded as a known, documented gap, not a blocker.
⚠️ **Test** - 3 issues (1 error, 2 warnings)
  • 🚨 server/typescript/packages/integration-tests - Full integration-tests regression (bun test in server/typescript/packages/integration-tests) never completed. I stopped it, so the log has no pass/fail summary and ends with exit=143. A later task notification reported the wrapper as exit code 0, but that is the wrapper's status after the kill, not a test result. Rerun it to a finished summary before deciding.
  • ⚠️ Port fixes for @Schema writes, enum @default on create, and int-backed enum arrays in C#, Java, Kotlin and Python were not driven live. Only the TypeScript port was exercised against Postgres.
  • ⚠️ live validation verdict: inconclusive (5 of 9 scenarios were driven live against the product); untested: C# @Schema write path and enum default on create, Java and Kotlin @Schema write path, enum default, int enum array, Python @Schema write path and enum default, Full TypeScript integration-tests regression
  • Live validation: ⚠️ inconclusive - 5 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Enum @default on create: omitted field stores the default (TS, live Postgres, generated create + Drizzle + ObjectManager) ✅ pass live pg-scenarios-1.log
Int-backed enum array round-trip (TS, live Postgres) ✅ pass live pg-scenarios-1.log
@Schema reaches reads and writes; same-named tables in two schemas stay separate (TS, live Postgres) ✅ pass live pg-scenarios-1.log, pg-scenarios-2.log
Adversarial: insert drops @Schema and writes to the default table ✅ pass live mutation-schema-drop-insert.log
@sequenceStart never moves a live counter; TS migrate only ALTERs START WITH (live Postgres) ✅ pass live pg-scenarios-2.log
C# @Schema write path and enum default on create ⏸️ untested no Not driven. dotnet is on PATH; the C# integration tests were not run in this session. Needs a dotnet test run against Postgres.
Java and Kotlin @Schema write path, enum default, int enum array ⏸️ untested no Not driven. java is on PATH; the Java and Kotlin persistence conformance lanes were not run in this session.
Python @Schema write path and enum default ⏸️ untested no Not driven. The Python conformance lane was not run in this session.
Full TypeScript integration-tests regression ⏸️ untested no Run killed before completion; the log has no summary and ends exit=143. The background task notification showing exit code 0 reflects the wrapper after the kill, not a test result.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test test/enum-default-pg.test.ts test/enum-intvaluemap-array.test.ts test/api-contract-enum-default.test.ts test/query.test.ts (server/typescript/packages/integration-tests, Postgres via Docker testcontainers): 70 pass, 0 fail
  • bun test test/migration.test.ts --test-name-pattern 'sequence|bootstrap': 2 pass
  • bun test test/query.test.ts --test-name-pattern 'schema-qualified': 1 pass
  • Mutation: runtime-ts src/drivers/kysely-driver.ts insert path forced to undefined schema; query.test.ts schema-qualified fails (expected {memo: archived-new}, actual null). Reverted with git checkout; worktree clean.
  • Full regression: bun test in server/typescript/packages/integration-tests, started in background, killed before completion (log ends exit=143, no summary).
  • CHANGELOG.md upgrade note read: atLeastOne to exactlyOne with @max removed; wire.durations iso note present. Fixture count 451 in docs/CONFORMANCE.md, CLAUDE.md, AGENTS.md matches 451 directories.
⚠️ **Document** - 1 info
  • ℹ️ Pre-existing C# and Kotlin source comments on main still cite internal rulings by date (e.g. "Owner ruling 2026-09-20" in M2MNavigation.cs, M2MUnpairedJunctionLoadErrorTests.cs, EntityGenerator.cs, requirements ruling in RequirementConstants.cs). This change did not add them, so they are out of scope. Follow-up: one sweep that replaces ruling identifiers with the rule itself across code comments.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…onto main

Brings the work from a downstream C# application session onto main, minus the
reserved-vocabulary changes (they land separately, with the Java, Kotlin and
Python registry catch-up):

- C# EF model fixes: @default, assigned keys, TPH subtype fields and
  references, nested value objects, constraint names and principal keys, the
  int-backed enum write refusal, -sis plurals, a thread-safe node cache, a
  split OnModelCreating, the index vocabulary and @description, and
  @sequenceStart with a start-only change that never restarts.
- C# generator hooks: one protected virtual per emitted line on the DbContext,
  entity and names generators. The golden files are main's pre-hook output
  plus only the lines the EF model fixes add, each named in the test.
- The pascal column rule and application-registered naming strategies in
  every port (ADR-0058 Amendment 1), with a test-only reset.
- Int-backed enum arrays load in every port.
- An enum's @default on an untouched record; generated-name collisions scoped
  to what carries them; doc pages keyed by package; @Schema in the Python
  runtime; Kotlin index @using/@where; Java create defaults.
- Port-scoped registry attrs ("ports"), with @dbSetName C#-only.

Completed on main for every port:

- @sequenceStart never moves the counter: meta migrate writes
  ALTER SEQUENCE ... START WITH n only (no setval). New persistence migration
  scenario sequence-start-change-keeps-counter.
- @Schema reaches every mapping: TS Kysely and Drizzle, Java OMDB, Kotlin
  Exposed tables. The canonical model gains LedgerEntry and an archive-schema
  ArchivedLedgerEntry of the same table name; new query scenario
  schema-qualified-same-name (Postgres).
- Java and Kotlin create defaults: a string default is a literal unless it is
  a database expression (shared isSqlExpressionDefault), escaped correctly;
  the Java switch uses the subtype constants.
- New api-contract sub-corpus enum-default/, generated lane in all five ports.
- Java/Kotlin api docs default to the package layout in the eject test.
- Kotlin compile gates leave out the IntEnumArray controller and repository
  until enum arrays get an Exposed array column (KNOWN_GAPS.md).
- Docs: CHANGELOG upgrade notes, CONFORMANCE counts and per-port status,
  api-contract "Defaults on create", regenerated metamodel docs.

Conflicts resolved toward main's reworks: validator.exactlyOne, the native
duration wire default with wire.durations "iso", the provider field in
meta types JSON, and MappedBaseClause for the entity base class (the
duplicate helper is dropped, the DbContext half is kept). The C# generator
that wrote expression indexes as SQL is left out: schema DDL stays with
meta migrate (ADR-0015).
…lePath

A CI build (CI=true turns on ContinuousIntegrationBuild) maps source paths
to /_/, so the [CallerFilePath]-based golden directory did not exist on the
runner and the three golden tests failed with DirectoryNotFoundException.
Use CorpusPaths.RepoRoot() like the other tests in this project.
@dmealing dmealing changed the title feat: add int-backed enum arrays, naming strategies and pascal column rule feat: add exactlyOne validator, int-backed enum arrays and naming strategies Nov 11, 2026
@dmealing
dmealing merged commit ea57ad0 into main Nov 11, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-workpc-replay-pr1 branch November 11, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant