diff --git a/CHANGELOG.md b/CHANGELOG.md index a20ccf233..aca1dafb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,104 +10,30 @@ here.** ## [Unreleased] -### Changed — one physical-naming setting, read by every port (ADR-0058) - -**This changes generated column names in C#, Java, Kotlin's runtime and Python unless you add -one line.** Column and table names that the metadata does not give (`@column`, `@table`, -`@view`, a source's `name`) now come from one project setting, `naming` in -`.metaobjects/config.json`: -`{ "naming": { "columns": "snake_case" | "literal" | "kebab-case", "tables": "snake_case" | "literal" } }`. -`meta migrate`, every port's codegen (the names generators included), every runtime -`ObjectManager` and `verify` read it from the loaded model, so codegen and the runtime can -no longer disagree. Both rules default to `snake_case` in every port, which is what -`meta migrate` creates; C#, Java, the JVM runtime and Python defaulted to `literal`. A -database whose columns are named exactly like the fields adds -`"naming": { "columns": "literal" }`. Every former place to set the rule is refused with a -message naming the setting and the guide, never silently ignored: `columnNamingStrategy` in -`metaobjects.config.ts` and on `ObjectManager` and `agentDocsFile()` (TypeScript), -`--column-naming` (C#, Python), the `columnNaming` generator arg in `pom.xml` and -`SimpleMappingHandlerDB.setColumnNaming` (Java, Kotlin), `ObjectManager(column_naming=)` -(Python). The engine options that carried the rule (`buildExpectedSchema`, -`buildProjectionViews`, `buildReportViews`, `computeDrift`, `planOffline`, -`baselineFromMetadata`) read it from the model instead. Table names gain a `literal` rule. -Every port runs the shared `fixtures/naming-conformance/physical-names/` corpus. -Migration: `docs/features/migrations/physical-naming.md`. - -Found by the application-building test of 1.1.0-rc.3: following the migration guide, a plain -`metaobjects gen` rewrote every `*_names.py` with camelCase column names the snake_case -database does not have. - -### Added - -- **Python: `templates` in `metaobjects.config.yaml`.** The template directory was a flag - only; `gen`, `verify --codegen` and `verify --templates` read the key, and a `--templates` - that contradicts it is refused. -- **Maven: `requires` is checked.** `mvn metaobjects:generate` fails, naming the missing - companion and its class, when a listed generator's output references what an unlisted one - writes (a Spring controller without the DTO, repository and filter allowlist; a Kotlin - controller without the Exposed tables, filter allowlists and relation queries), where it - used to exit 0 and leave the build to fail in `javac` or `kotlinc`. - `true` runs such a list when the files are - supplied another way. Every Java and Kotlin `requires` closure, and the skills' pom - examples, are compiled over the fitness corpus in a test. - -### Fixed -- `verify --codegen` (Python, C#) says when the committed output was generated with another - column rule, another table rule, or both, and names the `naming` setting that reproduces it, - instead of advising a regeneration that would commit the wrong column or table names. -- Every "removed in 1.1.0" message and skill reference names the migration guide by its full - public URL; the repository path they printed exists in no installed package. -- The Java and Kotlin skills' only `pom.xml` examples generated code that did not compile; - each now lists the whole REST tier, and the Kotlin skill lists the runtime dependencies the - generated code needs and that the controller needs `KotlinRelationsGenerator`. -- The migration guide and the skills give each port's documentation its own directory: in a - shared one, every port's `verify --codegen` reported the other ports' pages as drift. -- `dotnet meta gen --list` prints the expression that lists each generator in - `codegen/Generators.cs` (`new MetaObjects.Codegen.Generators.RenderHelperGenerator("prompts")`) - and its options. -- The C# "agent context is stale" notice no longer fires on build metadata alone - (`1.1.0-rc.3` against `1.1.0-rc.3+`). -- `meta verify --docs` prints its removal line once, as plain text. -- `metaobjects verify --templates` reads `metadata` and `templates` from - `metaobjects.config.yaml` instead of refusing the config. -- The skills no longer say metadata lives under `metaobjects/` (it is the default only), and - name C#'s positional `` (there is no `--metadata-dir`). -- Pre-release tooling: `prerelease:publish` reads what is released from public npm and the - release tags, not `package.json`, so a cut during release preparation is not numbered one - minor too high, and it refuses a `--base` that skips an unpublished CHANGELOG section; the - iteration counter reads every page of the registry listing; a SIGTERM restores the version - files. `prerelease-link.sh` links and repins every Maven and Python project at any depth and - repins the .NET local tool manifest. - -- A generated documentation page that no current object produces (for example the - `.md` page of an object that was removed) is reported as drift. 1.0's - `meta verify --docs` was silent about every such page outside `agent/`, so an adopter kept - ten orphan pages with the gate green. `meta verify --codegen`, which checks documentation in - 1.1.0 (see **Removed** under 1.1.0), reports each as a file a fresh run would not write. - Hand-written files in the docs directory are left alone. -- The `hand-rolled-aggregate` advisory now names `object.report` for grouped, served or reported - figures, and says an `object.projection` with `origin.aggregate` still fits a per-row figure. -- **The api reference (`apiDocsFile()`) honours `routesFile({ expose })` and - `routesFileHono({ expose })`.** The API pages listed all five verbs for an entity whose routes file mounts fewer (`DELETE` for an entity - exposed as `["list", "get"]`), so the adopter overrode the pages by hand. The generators now - carry the option in a `mountsRoutes` marker, and `meta gen` reads it off the declared list - (`runRoutesExpose`) per routes surface, even in a run narrowed by `--generators`, so a narrowed Fastify routes file beside a default Hono - one (or the reverse) narrows only its own section, and documents only the verbs mounted: `update` lists both `PATCH` and `PUT`, - a read-only projection or report still lists `GET`, and a TPH base is narrowed from its - read-only pair. Output without `expose` is byte-identical. **An owned (ejected) copy needs the - marker to be honoured:** run `meta eject routes --force` (and `routes-hono --force`), or add - `mountsRoutes: opts?.expose !== undefined ? { expose: opts.expose } : {}` beside the generator's - `name:`. TypeScript only: no other port has an `expose` option. - ## [1.1.0] — 2026-10-10 _npm `1.1.0` (all 14 `@metaobjectsdev/*` packages in lockstep), PyPI `1.1.0`, NuGet `1.1.0` and -Maven Central `8.1.0`. **`metamodelVersion` moves `1.0` to `1.1`.** The vocabulary is additive: a -model that uses none of the new names generates byte-for-byte what it did under 1.0. What loads -changes only where the Java and Python parser fixes under **Fixed** bring those ports into line -with TypeScript, and each entry says which models that affects. This is a MINOR because the +Maven Central `8.1.0`. **`metamodelVersion` moves `1.0` to `1.1`.** The vocabulary is additive: +a model that uses none of the new names loads as it did under 1.0, and what loads changes only +where the Java and Python parser fixes under **Fixed** bring those ports into line with +TypeScript. What it generates is NOT unchanged in every port: in C#, Java, Kotlin's runtime and +Python the default for column and table names flips from the field name to `snake_case`, so a +1.0 estate whose columns are named like its fields must add +`"naming": { "columns": "literal" }` (see **Breaking** below). Each entry says which models it +affects. This is a MINOR because the metamodel moved and reporting is a new feature, not because the package surface broke._ +**Breaking, C#, Java, Kotlin's runtime and Python: the default physical names are `snake_case`.** +Column and table names the metadata does not give now come from one `naming` setting in +`.metaobjects/config.json`, defaulting to `snake_case` in every port (it was the field name in +those four). A database whose columns are named exactly like the fields adds +`"naming": { "columns": "literal" }`. The per-port flags that used to carry the rule +(`--column-naming`, the `columnNaming` pom arg, `ObjectManager(column_naming=)`, +`columnNamingStrategy`) are removed and refused with a message naming the setting. Nothing in the +build fails if you skip this: the first failure is a query against a column that does not exist. +Migration: [physical-naming.md](docs/features/migrations/physical-naming.md). The entry is under +**Changed**. + **Breaking, in every port: documentation is generated the way code is.** Every generator, documentation included, is an entry in the project's generator list; one command runs the list; nothing is generated that the list does not declare. The docs commands are removed: @@ -185,6 +111,18 @@ generator catalog and the Atlas evaluation are 1.2 work. ### Added +- **Python: `templates` in `metaobjects.config.yaml`.** The template directory was a flag + only; `gen`, `verify --codegen` and `verify --templates` read the key, and a `--templates` + that contradicts it is refused. +- **Maven: `requires` is checked.** `mvn metaobjects:generate` fails, naming the missing + companion and its class, when a listed generator's output references what an unlisted one + writes (a Spring controller without the DTO, repository and filter allowlist; a Kotlin + controller without the Exposed tables, filter allowlists and relation queries), where it + used to exit 0 and leave the build to fail in `javac` or `kotlinc`. + `true` runs such a list when the files are + supplied another way. Every Java and Kotlin `requires` closure, and the skills' pom + examples, are compiled over the fitness corpus in a test. + - **The eject contract: every generator hands over its emit, and a test in every port holds it to that** (ADR-0034 Amendment 8). `fixtures/generator-registry-conformance/registry.json` now says, for each generator, `"eject": "full"` or an exemption with its reason, and lists @@ -888,7 +826,32 @@ generator catalog and the Atlas evaluation are 1.2 work. ### Changed -Eight corrections that shipped with report serving and reach models that declare no report. +**One physical-naming setting, read by every port (ADR-0058).** **This changes generated column names in C#, Java, Kotlin's runtime and Python unless you add +one line.** Column and table names that the metadata does not give (`@column`, `@table`, +`@view`, a source's `name`) now come from one project setting, `naming` in +`.metaobjects/config.json`: +`{ "naming": { "columns": "snake_case" | "literal" | "kebab-case", "tables": "snake_case" | "literal" } }`. +`meta migrate`, every port's codegen (the names generators included), every runtime +`ObjectManager` and `verify` read it from the loaded model, so codegen and the runtime can +no longer disagree. Both rules default to `snake_case` in every port, which is what +`meta migrate` creates; C#, Java, the JVM runtime and Python defaulted to `literal`. A +database whose columns are named exactly like the fields adds +`"naming": { "columns": "literal" }`. Every former place to set the rule is refused with a +message naming the setting and the guide, never silently ignored: `columnNamingStrategy` in +`metaobjects.config.ts` and on `ObjectManager` and `agentDocsFile()` (TypeScript), +`--column-naming` (C#, Python), the `columnNaming` generator arg in `pom.xml` and +`SimpleMappingHandlerDB.setColumnNaming` (Java, Kotlin), `ObjectManager(column_naming=)` +(Python). The engine options that carried the rule (`buildExpectedSchema`, +`buildProjectionViews`, `buildReportViews`, `computeDrift`, `planOffline`, +`baselineFromMetadata`) read it from the model instead. Table names gain a `literal` rule. +Every port runs the shared `fixtures/naming-conformance/physical-names/` corpus. +Migration: `docs/features/migrations/physical-naming.md`. + +Found by the application-building test of 1.1.0-rc.3: following the migration guide, a plain +`metaobjects gen` rewrote every `*_names.py` with camelCase column names the snake_case +database does not have. + +**Eight further changes.** Eight corrections that shipped with report serving and reach models that declare no report. Each one changes generated code or generated docs on the next gen run, as its entry says. A drift gate that covers that output reports it until you regenerate. @@ -1293,6 +1256,76 @@ until you regenerate. ### Fixed +- **SQLite and D1: a backtick-quoted CHECK constraint name is read.** Introspection read + `CONSTRAINT "name"` and a bare name but not `` CONSTRAINT `name` `` (the quoting Drizzle-written + migrations use) or `[name]`, so the stored check had an empty name, and `meta verify --db` and + the `meta migrate` re-diff reported a false `add-check` / `drop-check` pair. The expression + comparison also treats `` `col` `` and `"col"` as the same identifier (a backtick inside a + string literal stays data). +- **`meta gen --baseline=fresh` overwrites a file the committed manifest has no record of, even + when a git-ignored merge-base snapshot exists for it.** It used to three-way merge against that + stale snapshot (which survives a `git checkout`), so the old content stayed as a "hand edit". + A path the manifest does record still merges; only `fresh` ignores the stale body. +- **Maven (Kotlin): `spring-config` requires the validator only when it emits the validator + hook.** A pom with `validatorEnabled=false` that generated under 1.0 was refused for lacking + `KotlinValidatorGenerator`; the requirement now follows the generator's configured args + (`ConfiguredRequires`). `packageName is required` from the validator and spring-config + generators now names the generator. +- **Python: the api reference files each page under its own object's package.** Every page of a + model loaded from several files was written under the first file's package, and the back-links + pointed there. +- **Python: M:N navigations are emitted as `Field(default_factory=list)`**, not a mutable `= []` + class default. +- Pre-release tooling: a `prerelease:publish --dry-run` no longer ends with "is on the + pre-release registry", and the Java step is labelled for what it runs (`package`). + +- `verify --codegen` (Python, C#) says when the committed output was generated with another + column rule, another table rule, or both, and names the `naming` setting that reproduces it, + instead of advising a regeneration that would commit the wrong column or table names. +- Every "removed in 1.1.0" message and skill reference names the migration guide by its full + public URL; the repository path they printed exists in no installed package. +- The Java and Kotlin skills' only `pom.xml` examples generated code that did not compile; + each now lists the whole REST tier, and the Kotlin skill lists the runtime dependencies the + generated code needs and that the controller needs `KotlinRelationsGenerator`. +- The migration guide and the skills give each port's documentation its own directory: in a + shared one, every port's `verify --codegen` reported the other ports' pages as drift. +- `dotnet meta gen --list` prints the expression that lists each generator in + `codegen/Generators.cs` (`new MetaObjects.Codegen.Generators.RenderHelperGenerator("prompts")`) + and its options. +- The C# "agent context is stale" notice no longer fires on build metadata alone + (`1.1.0-rc.3` against `1.1.0-rc.3+`). +- `meta verify --docs` prints its removal line once, as plain text. +- `metaobjects verify --templates` reads `metadata` and `templates` from + `metaobjects.config.yaml` instead of refusing the config. +- The skills no longer say metadata lives under `metaobjects/` (it is the default only), and + name C#'s positional `` (there is no `--metadata-dir`). +- Pre-release tooling: `prerelease:publish` reads what is released from public npm and the + release tags, not `package.json`, so a cut during release preparation is not numbered one + minor too high, and it refuses a `--base` that skips an unpublished CHANGELOG section; the + iteration counter reads every page of the registry listing; a SIGTERM restores the version + files. `prerelease-link.sh` links and repins every Maven and Python project at any depth and + repins the .NET local tool manifest. + +- A generated documentation page that no current object produces (for example the + `.md` page of an object that was removed) is reported as drift. 1.0's + `meta verify --docs` was silent about every such page outside `agent/`, so an adopter kept + ten orphan pages with the gate green. `meta verify --codegen`, which checks documentation in + 1.1.0 (see **Removed** under 1.1.0), reports each as a file a fresh run would not write. + Hand-written files in the docs directory are left alone. +- The `hand-rolled-aggregate` advisory now names `object.report` for grouped, served or reported + figures, and says an `object.projection` with `origin.aggregate` still fits a per-row figure. +- **The api reference (`apiDocsFile()`) honours `routesFile({ expose })` and + `routesFileHono({ expose })`.** The API pages listed all five verbs for an entity whose routes file mounts fewer (`DELETE` for an entity + exposed as `["list", "get"]`), so the adopter overrode the pages by hand. The generators now + carry the option in a `mountsRoutes` marker, and `meta gen` reads it off the declared list + (`runRoutesExpose`) per routes surface, even in a run narrowed by `--generators`, so a narrowed Fastify routes file beside a default Hono + one (or the reverse) narrows only its own section, and documents only the verbs mounted: `update` lists both `PATCH` and `PUT`, + a read-only projection or report still lists `GET`, and a TPH base is narrowed from its + read-only pair. Output without `expose` is byte-identical. **An owned (ejected) copy needs the + marker to be honoured:** run `meta eject routes --force` (and `routes-hono --force`), or add + `mountsRoutes: opts?.expose !== undefined ? { expose: opts.expose } : {}` beside the generator's + `name:`. TypeScript only: no other port has an `expose` option. + - **The Java PlantUML generator no longer throws on a single-segment package.** `PlantUMLGenerator` failed with `StringIndexOutOfBoundsException` for any model whose package has one segment (`acme`, not `acme::shop`), which is most small models. It was in diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/agent-context/skills/metaobjects-codegen/references/java.md b/agent-context/skills/metaobjects-codegen/references/java.md index 2b5d0a0e0..3f211a60d 100644 --- a/agent-context/skills/metaobjects-codegen/references/java.md +++ b/agent-context/skills/metaobjects-codegen/references/java.md @@ -28,6 +28,24 @@ All artifacts share `groupId` `com.metaobjects` and one version property: | `metaobjects-maven-plugin` | the build-time codegen plugin | | `metaobjects-core-spring` | optional Spring wiring | +## Column and table names + +**Column and table names come from `naming` in `.metaobjects/config.json`, the one setting +every port reads** (ADR-0058): `meta migrate`, every port's codegen, and the OMDB runtime. The +default is `snake_case`, what `meta migrate` creates. Through 1.0 this port defaulted to the +field names; a database whose columns are named exactly like the fields declares it once: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-generator or runtime naming: the `columnNaming` generator arg fails +`mvn metaobjects:generate`, and `SimpleMappingHandlerDB.setColumnNaming` throws. A Spring +application that loads its metadata from the classpath points `metaobjects.physical-naming-config` +at the project's `.metaobjects/config.json` packaged as a resource (for example +`classpath:.metaobjects/config.json`). Details and the 1.0 to 1.1 steps: +. + ## Plugin config in `pom.xml` The plugin's `generate` goal binds to the `generate-sources` phase. Point its diff --git a/agent-context/skills/metaobjects-codegen/references/kotlin.md b/agent-context/skills/metaobjects-codegen/references/kotlin.md index 6ccae0b58..3a047c508 100644 --- a/agent-context/skills/metaobjects-codegen/references/kotlin.md +++ b/agent-context/skills/metaobjects-codegen/references/kotlin.md @@ -45,6 +45,25 @@ plugin does not bring: Keep all four Exposed artifacts on one version. The port guide's `pom.xml` lists them with versions. +## Column and table names + +**Column and table names come from `naming` in `.metaobjects/config.json`, the one setting +every port reads** (ADR-0058): `meta migrate`, every port's codegen, and the Exposed tables the Kotlin generators write. The +default is `snake_case`, what `meta migrate` creates. Through 1.0 the Kotlin runtime's JDBC side defaulted to the +field names; a database whose columns are named exactly like the fields declares it once: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-generator or runtime naming: the `columnNaming` generator arg fails +`mvn metaobjects:generate`, and `SimpleMappingHandlerDB.setColumnNaming` throws (the Exposed +tables and the names generator already read the setting). A Spring +application that loads its metadata from the classpath points `metaobjects.physical-naming-config` +at the project's `.metaobjects/config.json` packaged as a resource (for example +`classpath:.metaobjects/config.json`). Details and the 1.0 to 1.1 steps: +. + ## Plugin config in `pom.xml` The plugin's `generate` goal binds to the `generate-sources` phase. Point its diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index e13486f46..1eafb2d1f 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -82,6 +82,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/docs/features/cli.md b/docs/features/cli.md index 3c34c2980..b59d9d86f 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -232,7 +232,7 @@ one-time fix. |---|---|---| | `--baseline=default` | all | the above: refuse what cannot be proved to be generated output | | `--baseline=adopt` | TS · Python · C# | record the files you HAVE as the merge base and **write nothing** | -| `--baseline=fresh` | TS only | overwrite the refused files from fresh output and re-baseline (**discards** hand edits) | +| `--baseline=fresh` | TS only | overwrite the refused files from fresh output and re-baseline (**discards** hand edits). A local snapshot body for a path the manifest has no record of is ignored, not merged against, so stale content from an earlier checkout is not kept as a hand edit | **`adopt` exists because the refusal's own remedy was unreachable.** It told you to commit `.hashes.json` — but nothing writes a manifest until a `gen` succeeds, and a run where diff --git a/docs/features/migrations/docs-are-generators.md b/docs/features/migrations/docs-are-generators.md index 4c307f00a..c845145ca 100644 --- a/docs/features/migrations/docs-are-generators.md +++ b/docs/features/migrations/docs-are-generators.md @@ -620,6 +620,12 @@ to the same paths as the 8.0 pom above: | the page header's project label, which was the module's `artifactId` | `projectName`, default `project`; set `${project.artifactId}` to keep 8.0's pages byte for byte | | `-Dmetaobjects.docs.*` properties | none; use Maven properties inside `` if you need to override from the command line | +**Remove any `columnNaming` generator arg from the same pom.** 8.1.0 refuses it, and the +refusal points here because this is the pom you are editing. Column and table names are one +setting now, `naming` in `.metaobjects/config.json`; if your database's columns are named +exactly like your fields, add `"naming": { "columns": "literal" }` there. See +[physical-naming.md](physical-naming.md), section "Java and Kotlin". + **Give each api-docs entry an `outputDir` that holds generated files only.** `metaobjects:verify` now checks the pages, as it checks code: it regenerates each entry's `outputDir` and compares the whole tree, so a hand-written file there reads as stale. diff --git a/docs/features/migrations/physical-naming.md b/docs/features/migrations/physical-naming.md index c3b746652..bc223026f 100644 --- a/docs/features/migrations/physical-naming.md +++ b/docs/features/migrations/physical-naming.md @@ -3,7 +3,7 @@ **Changed in:** 1.1.0 (Maven 8.1.0) · **Ports affected:** all five **Automatic fix:** none. Each section below is written so you, or a coding agent working in your repository, can make the change by hand. Decision record: -[ADR-0058](../../../spec/decisions/ADR-0058-one-physical-naming-setting.md). +[ADR-0058](https://github.com/metaobjectsdev/metaobjects/blob/main/spec/decisions/ADR-0058-one-physical-naming-setting.md). ## What changed @@ -92,7 +92,7 @@ regenerate. | 1.0 | 1.1.0 | |---|---| | `metaobjects gen --column-naming `, `metaobjects verify --codegen --column-naming ` | `"naming": { "columns": "" }` in `.metaobjects/config.json`. The flag is refused (exit 2) | -| `ObjectManager(..., column_naming="")` | `ObjectManager(...)`. The manager reads the rule from the loaded root; the argument raises. A model loaded outside the project directory gets the project's rules with `attach_naming(root, project_dir)` from `metaobjects.naming`, which reads that project's `.metaobjects/config.json` | +| `ObjectManager(..., column_naming="")` | `ObjectManager(...)`. The manager reads the rule from the loaded root; the argument raises. A model loaded outside the project directory gets the project's rules with `attach_naming(root, project_dir)` from `metaobjects.naming`, which reads that project's `.metaobjects/config.json` and returns the root, so `root = attach_naming(root, project_dir)` | | no flag: `*_names.py` and the `ObjectManager` used the field names (`literal`) | no setting: snake_case. Add `"naming": { "columns": "literal" }` to keep 1.0's column names | `metaobjects verify --codegen` reports drift that another column rule, another table rule, or diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/java.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/java.md index 2b5d0a0e0..3f211a60d 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/java.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/java.md @@ -28,6 +28,24 @@ All artifacts share `groupId` `com.metaobjects` and one version property: | `metaobjects-maven-plugin` | the build-time codegen plugin | | `metaobjects-core-spring` | optional Spring wiring | +## Column and table names + +**Column and table names come from `naming` in `.metaobjects/config.json`, the one setting +every port reads** (ADR-0058): `meta migrate`, every port's codegen, and the OMDB runtime. The +default is `snake_case`, what `meta migrate` creates. Through 1.0 this port defaulted to the +field names; a database whose columns are named exactly like the fields declares it once: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-generator or runtime naming: the `columnNaming` generator arg fails +`mvn metaobjects:generate`, and `SimpleMappingHandlerDB.setColumnNaming` throws. A Spring +application that loads its metadata from the classpath points `metaobjects.physical-naming-config` +at the project's `.metaobjects/config.json` packaged as a resource (for example +`classpath:.metaobjects/config.json`). Details and the 1.0 to 1.1 steps: +. + ## Plugin config in `pom.xml` The plugin's `generate` goal binds to the `generate-sources` phase. Point its diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/kotlin.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/kotlin.md index 6ccae0b58..3a047c508 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/kotlin.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/kotlin.md @@ -45,6 +45,25 @@ plugin does not bring: Keep all four Exposed artifacts on one version. The port guide's `pom.xml` lists them with versions. +## Column and table names + +**Column and table names come from `naming` in `.metaobjects/config.json`, the one setting +every port reads** (ADR-0058): `meta migrate`, every port's codegen, and the Exposed tables the Kotlin generators write. The +default is `snake_case`, what `meta migrate` creates. Through 1.0 the Kotlin runtime's JDBC side defaulted to the +field names; a database whose columns are named exactly like the fields declares it once: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-generator or runtime naming: the `columnNaming` generator arg fails +`mvn metaobjects:generate`, and `SimpleMappingHandlerDB.setColumnNaming` throws (the Exposed +tables and the names generator already read the setting). A Spring +application that loads its metadata from the classpath points `metaobjects.physical-naming-config` +at the project's `.metaobjects/config.json` packaged as a resource (for example +`classpath:.metaobjects/config.json`). Details and the 1.0 to 1.1 steps: +. + ## Plugin config in `pom.xml` The plugin's `generate` goal binds to the `generate-sources` phase. Point its diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index d69ed2f6d..970a7b7ec 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md index d69ed2f6d..970a7b7ec 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/java.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/java.md index 2b5d0a0e0..3f211a60d 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/java.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/java.md @@ -28,6 +28,24 @@ All artifacts share `groupId` `com.metaobjects` and one version property: | `metaobjects-maven-plugin` | the build-time codegen plugin | | `metaobjects-core-spring` | optional Spring wiring | +## Column and table names + +**Column and table names come from `naming` in `.metaobjects/config.json`, the one setting +every port reads** (ADR-0058): `meta migrate`, every port's codegen, and the OMDB runtime. The +default is `snake_case`, what `meta migrate` creates. Through 1.0 this port defaulted to the +field names; a database whose columns are named exactly like the fields declares it once: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-generator or runtime naming: the `columnNaming` generator arg fails +`mvn metaobjects:generate`, and `SimpleMappingHandlerDB.setColumnNaming` throws. A Spring +application that loads its metadata from the classpath points `metaobjects.physical-naming-config` +at the project's `.metaobjects/config.json` packaged as a resource (for example +`classpath:.metaobjects/config.json`). Details and the 1.0 to 1.1 steps: +. + ## Plugin config in `pom.xml` The plugin's `generate` goal binds to the `generate-sources` phase. Point its diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index ccd4313a3..ddac81bba 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md index ccd4313a3..ddac81bba 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index 4742a4c60..a15894058 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -57,6 +57,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md index 4742a4c60..a15894058 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md @@ -57,6 +57,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index 10501d7e9..7e9604803 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md index 10501d7e9..7e9604803 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index fa28663b9..2cae76911 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -47,6 +47,24 @@ exact config, generator names and commands for this project; read it. The other | `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project | | `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project | +## Upgrading a project from 1.0 to 1.1 + +Read both guides before the first `gen` of an upgrade, for every port the project uses; the +build can stay green while the result is wrong. + +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every + port (C#, Java, Kotlin's runtime and Python defaulted to the field names). A database whose + columns are named like its fields needs `"naming": { "columns": "literal" }` in + `.metaobjects/config.json`, or regenerated code queries columns that do not exist. The + per-port naming flags and generator args are removed. +- https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs + commands are removed. +- **Resync the generators you own.** An ejected copy keeps the logic it was copied with, and a + plain `gen` does not refresh it. `meta eject --list` (`dotnet meta eject`, `metaobjects eject`, + `mvn metaobjects:eject`) reports each copy that differs from the reference; + `eject --force` takes the new reference for a copy you never edited, and a copy you + edited is three-way merged instead. Steps for 1.0.x: https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md. + ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md index 41e31dd36..e8e7c0c6e 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md index 41e31dd36..e8e7c0c6e 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md @@ -60,6 +60,8 @@ MetaObjects ships as separate packages per language on DIFFERENT version lines ( `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. +**Upgrading from 1.0 to 1.1:** read both guides before you run `gen`, because the build can stay green while the result is wrong. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/physical-naming.md: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist. https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/docs-are-generators.md: documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. + ## Going deeper (Claude Code) For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the matching `metaobjects-*` skill — its body links the `references/.md` fragment diff --git a/fixtures/generator-registry-conformance/registry.json b/fixtures/generator-registry-conformance/registry.json index eff2ba41f..682902f50 100644 --- a/fixtures/generator-registry-conformance/registry.json +++ b/fixtures/generator-registry-conformance/registry.json @@ -442,6 +442,7 @@ "com.metaobjects.generator.GeneratorIOWriter", "com.metaobjects.generator.ProjectClassLoaderAware", "com.metaobjects.generator.direct.MultiFileDirectGeneratorBase", + "com.metaobjects.generator.kotlin.ConfiguredRequires", "com.metaobjects.generator.kotlin.ExposedApi", "com.metaobjects.generator.kotlin.ExposedImports", "com.metaobjects.generator.kotlin.FindInbound", @@ -473,6 +474,7 @@ "metaobjects.codegen.generators.find_inbound", "metaobjects.codegen.generators.tph_plan", "metaobjects.codegen.instance_artifacts", + "metaobjects.codegen.model_walk", "metaobjects.codegen.project_config", "metaobjects.codegen.requirement_hooks", "metaobjects.codegen.requirement_walk", diff --git a/scripts/prerelease.mjs b/scripts/prerelease.mjs index 4ece092b4..121b3fb60 100644 --- a/scripts/prerelease.mjs +++ b/scripts/prerelease.mjs @@ -406,10 +406,14 @@ if (wants("java")) { await run(`cd server/java && mvn -B -s ${settings} -Dmaven.repo.local=${repo} ${goal} -DskipTests` + ` -DaltDeploymentRepository="mo-prerelease::${MAVEN_URL}"`, { stdio: "inherit" }); - ok(DRY ? "java: dry run (install only)" : `java: reactor @ ${V.maven} → ${expectedHost}`); + ok(DRY ? "java: dry run, built (package), nothing deployed" : `java: reactor @ ${V.maven} → ${expectedHost}`); } restore(); -console.log(`\n\x1b[32m\x1b[1m✅ ${CANON} is on the pre-release registry.\x1b[0m`); -console.log(` link a consumer: tools/prerelease/prerelease-link.sh link --project --version ${CANON}`); -console.log(` consumer guide: docs/features/prerelease.md\n`); +if (DRY) { + console.log(`\n\x1b[32m\x1b[1m✔ dry run of ${CANON} passed; nothing was published.\x1b[0m\n`); +} else { + console.log(`\n\x1b[32m\x1b[1m✅ ${CANON} is on the pre-release registry.\x1b[0m`); + console.log(` link a consumer: tools/prerelease/prerelease-link.sh link --project --version ${CANON}`); + console.log(` consumer guide: docs/features/prerelease.md\n`); +} diff --git a/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/ConfiguredRequires.kt b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/ConfiguredRequires.kt new file mode 100644 index 000000000..9c90b5a69 --- /dev/null +++ b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/ConfiguredRequires.kt @@ -0,0 +1,15 @@ +package com.metaobjects.generator.kotlin + +/** + * A generator whose `requires` depends on how it is configured. + * + * [GeneratorInfo.requires] is the static answer, the one `--list` prints: what the generator + * needs with its default args. A generator that needs less under other args (spring-config emits + * no reference to the generated validator when `validatorEnabled` is `false`) implements this, and + * `mvn metaobjects:generate` asks the instance it is about to run, after its args are set, so a + * pom that never needed the companion is not refused for lacking it. + */ +interface ConfiguredRequires { + /** The stable names this instance's output references, given its configured args. */ + fun configuredRequires(): List +} diff --git a/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringConfigGenerator.kt b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringConfigGenerator.kt index a93dd6197..587088bc9 100644 --- a/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringConfigGenerator.kt +++ b/server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringConfigGenerator.kt @@ -4,6 +4,7 @@ package com.metaobjects.generator.kotlin // com.metaobjects.generator.kotlin when this generator is copied out via // `mvn metaobjects:eject` and its own package is renamed — an explicit import, not // same-package bare-name resolution, is what keeps the ejected copy compiling. +import com.metaobjects.generator.kotlin.ConfiguredRequires import com.metaobjects.generator.kotlin.ExposedApi import com.metaobjects.generator.kotlin.KotlinPoetFileWriter @@ -49,7 +50,15 @@ import java.nio.file.Paths * (only emits the {@code Database.connect} wiring) * */ -open class KotlinSpringConfigGenerator : MultiFileDirectGeneratorBase() { +open class KotlinSpringConfigGenerator : MultiFileDirectGeneratorBase(), ConfiguredRequires { + + /** `validatorEnabled` (default `true`); the one reading [execute] and [configuredRequires] share. */ + private fun validatorEnabled(): Boolean = + (getArg("validatorEnabled", "true") ?: "true").equals("true", ignoreCase = true) + + /** The generated validator is referenced only when the startup hook is emitted. */ + override fun configuredRequires(): List = + if (validatorEnabled()) listOf("validator") else emptyList() /** See [KotlinExposedTableGenerator.exposedApi] — same arg, same default (issue #390). */ protected fun exposedApi(): ExposedApi = ExposedApi.parse(getArg(ExposedApi.ARG_EXPOSED_API)) @@ -60,10 +69,9 @@ open class KotlinSpringConfigGenerator : MultiFileDirectGeneratorBase() parseArgs() val api = exposedApi() val pkg = getArg("packageName") - ?: throw GeneratorException("packageName is required") + ?: throw GeneratorException("KotlinValidatorGenerator (\"validator\"): packageName is required") val outRoot = Paths.get(outDir.absolutePath) val entries = loader.metaObjects diff --git a/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinPackageNameRequiredTest.kt b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinPackageNameRequiredTest.kt new file mode 100644 index 000000000..132b72147 --- /dev/null +++ b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinPackageNameRequiredTest.kt @@ -0,0 +1,54 @@ +package com.metaobjects.generator.kotlin + +import com.metaobjects.generator.GeneratorException +import com.metaobjects.metadata.ktx.loadString +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * `packageName` is required by the two generators that write a file of their own beside the + * entities. A pom that added one without it used to fail with `packageName is required`, + * which names neither the generator nor the arg's owner among a dozen in the same + * ``. + */ +class KotlinPackageNameRequiredTest { + + private val model = """{ "metadata.root": { "package": "acme::demo", "children": [ + { "object.entity": { "name": "Author", "children": [ { "field.long": { "name": "id" } } ] } } + ] } }""" + + private fun message(gen: com.metaobjects.generator.GeneratorBase): String? { + val out = Files.createTempDirectory("kgen-pkg-") + try { + gen.setArgs(mapOf("outputDir" to out.toString())) + return assertFailsWith { gen.execute(loadString("pkg", model)) }.message + } finally { + out.toFile().deleteRecursively() + } + } + + @Test fun `the validator generator names itself when packageName is missing`() { + val m = message(KotlinValidatorGenerator()) + assertTrue(m!!.contains("KotlinValidatorGenerator (\"validator\")") && m.contains("packageName is required"), m) + } + + @Test fun `the spring-config generator names itself when packageName is missing`() { + val m = message(KotlinSpringConfigGenerator()) + assertTrue(m!!.contains("KotlinSpringConfigGenerator (\"spring-config\")") && m.contains("packageName is required"), m) + } + + @Test fun `spring-config requires the validator only while the validator hook is emitted`() { + assertEquals(listOf("validator"), KotlinSpringConfigGenerator().configuredRequires()) + assertEquals( + listOf("validator"), + KotlinSpringConfigGenerator().apply { setArgs(mapOf("validatorEnabled" to "TRUE")) }.configuredRequires(), + ) + assertEquals( + emptyList(), + KotlinSpringConfigGenerator().apply { setArgs(mapOf("validatorEnabled" to "false")) }.configuredRequires(), + ) + } +} diff --git a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataGeneratorMojo.java b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataGeneratorMojo.java index a12dc643e..99ce1411e 100644 --- a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataGeneratorMojo.java +++ b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataGeneratorMojo.java @@ -71,9 +71,7 @@ protected void executeGenerators(MetaDataLoader loader, List generato /** Fail the goal, naming each missing companion, when the list does not declare what its * generators' output references; with {@link #allowUnmetRequires}, warn instead. */ void refuseUnmetRequires(List generatorImpls) { - List classnames = new ArrayList<>(); - for (Generator g : generatorImpls) classnames.add(g.getClass().getName()); - List unmet = RequiresCheck.unmet(classnames); + List unmet = RequiresCheck.unmetFor(generatorImpls); if (unmet.isEmpty()) return; if (allowUnmetRequires) { for (String line : unmet) getLog().warn("MetaData Mojo > " + line); diff --git a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/RequiresCheck.java b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/RequiresCheck.java index d9d2a7c1f..140b829ec 100644 --- a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/RequiresCheck.java +++ b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/RequiresCheck.java @@ -1,6 +1,8 @@ package com.metaobjects.mojo; +import com.metaobjects.generator.Generator; import com.metaobjects.generator.GeneratorRegistry; +import com.metaobjects.generator.kotlin.ConfiguredRequires; import com.metaobjects.generator.kotlin.GeneratorInfo; import com.metaobjects.generator.kotlin.GeneratorRegistryKt; @@ -25,6 +27,10 @@ * copy {@code mvn metaobjects:eject} wrote, by its simple class name. A generator the registries * do not know (the adopter's own) has no requirements, and does not satisfy one. Requirements * are per port: a Kotlin controller is not satisfied by a Java DTO generator. + * + *

A generator that needs less under some args ({@link ConfiguredRequires}: spring-config with + * {@code validatorEnabled=false} references no validator) is asked for its requirements after + * its args are set, so the check follows the configuration rather than the default. */ final class RequiresCheck { @@ -64,11 +70,31 @@ private static Known resolve(List known, String classname) { /** * One line per declared generator whose requirements the same list does not declare, naming * everything missing (the requirements of a missing requirement included) and the class to - * add; empty when every requirement is met. + * add; empty when every requirement is met. Each generator needs what its registry entry + * says. * * @param classnames the {@code } of each {@code }, in declared order */ static List unmet(List classnames) { + return unmet(classnames, Map.of()); + } + + /** + * As {@link #unmet(List)} for the instances a run is about to execute, args set: a + * {@link ConfiguredRequires} generator is asked what its configuration needs. + */ + static List unmetFor(List generators) { + List classnames = new ArrayList<>(); + Map> configured = new LinkedHashMap<>(); + for (Generator g : generators) { + String classname = g.getClass().getName(); + classnames.add(classname); + if (g instanceof ConfiguredRequires c) configured.putIfAbsent(classname, c.configuredRequires()); + } + return unmet(classnames, configured); + } + + private static List unmet(List classnames, Map> configured) { List known = known(); // One {@code port:stableName} index: membership and the "what class fixes this" // lookup read the same structure, so adding a port cannot leave one behind. @@ -90,7 +116,7 @@ static List unmet(List classnames) { // declared requirement's own requirements are reported on its own line. List missing = new ArrayList<>(); Set seen = new LinkedHashSet<>(); - List todo = new ArrayList<>(k.requires()); + List todo = new ArrayList<>(configured.getOrDefault(d.getKey(), k.requires())); while (!todo.isEmpty()) { String r = todo.remove(0); String key = k.port() + ":" + r; diff --git a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/GeneratorRequiresCompileTest.java b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/GeneratorRequiresCompileTest.java index 0ae908081..045651441 100644 --- a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/GeneratorRequiresCompileTest.java +++ b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/GeneratorRequiresCompileTest.java @@ -167,6 +167,30 @@ public void theControllerAloneDoesNotCompileAndTheGoalRefusesIt() throws Excepti mojo.refuseUnmetRequires(List.of(construct(controller))); } + /** spring-config references the generated validator only when {@code validatorEnabled} is on, + * so the check follows the instance's args: a pom with it off built before the check existed. */ + @Test + public void springConfigRequiresTheValidatorOnlyWhenTheValidatorIsEnabled() throws Exception { + String classname = "com.metaobjects.generator.kotlin.KotlinSpringConfigGenerator"; + MetaDataGeneratorMojo mojo = new MetaDataGeneratorMojo(); + + Generator disabled = construct(classname).setArgs(Map.of("validatorEnabled", "false")); + mojo.refuseUnmetRequires(List.of(disabled)); + assertEquals(List.of(), RequiresCheck.unmetFor(List.of(disabled))); + + for (Generator enabled : List.of(construct(classname), construct(classname).setArgs(Map.of("validatorEnabled", "true")))) { + List unmet = RequiresCheck.unmetFor(List.of(enabled)); + assertEquals(1, unmet.size()); + assertTrue(unmet.get(0), unmet.get(0).contains("\"validator\"")); + try { + mojo.refuseUnmetRequires(List.of(enabled)); + fail("spring-config with the validator hook ran without the validator"); + } catch (MetaDataException expected) { + assertTrue(expected.getMessage(), expected.getMessage().contains("\"validator\"")); + } + } + } + @Test public void anOwnedCopyIsMatchedByItsSimpleNameAndAnUnknownGeneratorRequiresNothing() { assertEquals(1, RequiresCheck.unmet(List.of("com.acme.codegen.SpringControllerGenerator")).size()); diff --git a/server/python/src/metaobjects/codegen/generators/entity_model.py b/server/python/src/metaobjects/codegen/generators/entity_model.py index 367139cef..42587b2ee 100644 --- a/server/python/src/metaobjects/codegen/generators/entity_model.py +++ b/server/python/src/metaobjects/codegen/generators/entity_model.py @@ -555,7 +555,13 @@ def _emit_field_lines( else: element = d.target_entity imports.add(f"from .{d.target_entity} import {d.target_entity}") - lines.append(f" {d.relation_name}: list[{element}] = []") + # default_factory, not `= []`: a class-level list literal is one shared object + # as far as a linter or a reader can tell (pydantic copies it, a dataclass-style + # reader would not). + lines.append( + f" {d.relation_name}: list[{element}] = Field(default_factory=list)" + ) + uses_field = True return lines, uses_field def _emit_create_lines( @@ -655,9 +661,9 @@ def render_entity_model( When *object_index* is supplied, M:N navigations (``relationship.*`` ``@cardinality:"many" + @through``) are emitted as nested Pydantic - collections (``tags: list[Tag] = []``); a self-join uses a forward-ref string - (``following: list["Person"] = []``). Without an index, only scalar/object - fields are emitted (back-compat). *config* carries the FR-019 @provided-enum + collections (``tags: list[Tag] = Field(default_factory=list)``); a self-join uses a + forward-ref string (``following: list["Person"] = Field(default_factory=list)``). + Without an index, only scalar/object fields are emitted (back-compat). *config* carries the FR-019 @provided-enum resolution; when omitted, only the non-provided shared-materialize path is reachable.""" imports: set[str] = set() base_class = "BaseModel" diff --git a/server/python/src/metaobjects/codegen/runtime/api_docs_builder.py b/server/python/src/metaobjects/codegen/runtime/api_docs_builder.py index 0f3754c18..12087e470 100644 --- a/server/python/src/metaobjects/codegen/runtime/api_docs_builder.py +++ b/server/python/src/metaobjects/codegen/runtime/api_docs_builder.py @@ -38,6 +38,7 @@ FieldShape, ) from metaobjects.codegen import type_map +from metaobjects.codegen.model_walk import package_of from .m2m_codegen import ( build_object_index, resolve_m2m_descriptors, @@ -210,7 +211,7 @@ def _build_object_unit( if not symbols: return None - return ApiUnit(obj.name, _package_of(obj), unit_kind, symbols) + return ApiUnit(obj.name, package_of(obj), unit_kind, symbols) def _build_report_unit(self, obj: MetaObject, root: MetaData) -> ApiUnit: """The unit for a SERVED report (FR-044 Table G): the row model plus the read-only @@ -229,7 +230,7 @@ def _build_report_unit(self, obj: MetaObject, root: MetaData) -> ApiUnit: ), *self._read_only_symbols(model_obj), ] - return ApiUnit(obj.name, _package_of(obj), "report", symbols) + return ApiUnit(obj.name, package_of(obj), "report", symbols) def _read_only_symbols(self, obj: MetaObject) -> list[ApiSymbol]: """What the READ-ONLY router (``is_read_only_routed``) actually mounts and emits, @@ -431,7 +432,7 @@ def _build_template_unit(self, tmpl: MetaData, root: MetaData) -> ApiUnit: ) ) - return ApiUnit(name, _package_of(tmpl), "template", symbols) + return ApiUnit(name, package_of(tmpl), "template", symbols) # ----- field shapes ------------------------------------------------------- @@ -471,18 +472,6 @@ def _payload_fields(self, vo: MetaObject) -> list[FieldShape]: return rows -def _package_of(node: MetaData) -> str: - """The unit's metadata package (``"acme::shop"``) — drives the doc-page layout - path. Resolved from the nearest ancestor that carries one (objects inherit the - file/root package).""" - pkg = node.package - parent = node.parent - while pkg is None and parent is not None: - pkg = parent.package - parent = parent.parent - return pkg or "" - - def build_api_model(root: MetaData, project: str) -> ApiModel: """Module-level convenience: build the Python SDK api-surface IR.""" return PythonApiModelBuilder().build(root, project) diff --git a/server/python/tests/codegen/test_m2m_codegen.py b/server/python/tests/codegen/test_m2m_codegen.py index beee40949..a197d81ba 100644 --- a/server/python/tests/codegen/test_m2m_codegen.py +++ b/server/python/tests/codegen/test_m2m_codegen.py @@ -180,15 +180,22 @@ def test_entity_without_m2m_has_no_descriptors() -> None: def test_pydantic_nested_collection_emitted() -> None: src = render_entity_model(_ENTITIES["Post"], _INDEX) - assert "tags: list[Tag] = []" in src + assert "tags: list[Tag] = Field(default_factory=list)" in src assert "from .Tag import Tag" in src +def test_pydantic_nested_collection_default_is_a_factory_and_imports_field() -> None: + for name in ("Post", "Person"): + src = render_entity_model(_ENTITIES[name], _INDEX) + assert "= []" not in src, src + assert "from pydantic import" in src and "Field" in src.split("from pydantic import", 1)[1].split("\n", 1)[0], src + + def test_pydantic_self_referential_collection() -> None: src = render_entity_model(_ENTITIES["Person"], _INDEX) # Self-join: the element type is the entity itself (forward ref string). - assert 'following: list["Person"] = []' in src - assert 'friends: list["Person"] = []' in src + assert 'following: list["Person"] = Field(default_factory=list)' in src + assert 'friends: list["Person"] = Field(default_factory=list)' in src # --------------------------------------------------------------------------- diff --git a/server/python/tests/test_api_docs_packages.py b/server/python/tests/test_api_docs_packages.py new file mode 100644 index 000000000..9ac1799cc --- /dev/null +++ b/server/python/tests/test_api_docs_packages.py @@ -0,0 +1,64 @@ +"""Each API page lands under its own object's package, not the first loaded file's. + +A model loaded from several files has one root; ``root.package`` is the first file's. The +builder used to read the package off the node and then its ancestors, so every object whose +own file declared another package was filed (and back-linked) under the root's. +""" +from __future__ import annotations + +import tempfile +from pathlib import Path + +from metaobjects import MetaDataLoader +from metaobjects.apidocs import PythonApiModelBuilder +from metaobjects.codegen.config import GenConfig +from metaobjects.codegen.generator import GenContext +from metaobjects.codegen.generators.api_docs_generator import ApiDocsGenerator +from metaobjects.shared.base_types import TYPE_OBJECT + + +def _entity_file(package: str, name: str) -> str: + return ( + '{ "metadata.root": { "package": "%s", "children": [\n' + ' { "object.entity": { "name": "%s", "children": [\n' + ' { "source.rdb": { "@table": "t_%s" } },\n' + ' { "field.long": { "name": "id" } },\n' + ' { "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }\n' + " ]}}\n" + "]}}" % (package, name, name.lower()) + ) + + +def _load_two_packages(): + with tempfile.TemporaryDirectory() as d: + # Sorted load order makes "alpha" the file whose package the merged root keeps. + (Path(d) / "meta.alpha.json").write_text(_entity_file("alpha::lib", "Widget"), encoding="utf-8") + (Path(d) / "meta.beta.json").write_text(_entity_file("beta::app", "Order"), encoding="utf-8") + r = MetaDataLoader.from_directory(d) + assert not r.errors, r.errors + return r.root + + +def test_every_unit_carries_its_own_package() -> None: + model = PythonApiModelBuilder().build(_load_two_packages(), "two-packages") + assert {u.node: u.package for u in model.units} == {"Widget": "alpha::lib", "Order": "beta::app"} + + +def test_pages_and_back_links_are_filed_under_each_package() -> None: + root = _load_two_packages() + ctx = GenContext( + entities=[c for c in root.own_children() if c.type == TYPE_OBJECT], + loaded_root=root, + matches=lambda _e: True, + config=GenConfig(out_dir="/tmp/out"), + warn=lambda _m: None, + ) + files = {f.path: f.content for f in ApiDocsGenerator(api_subdir="api").generate(ctx)} + + assert "api/alpha/lib/Widget.md" in files + assert "api/beta/app/Order.md" in files + assert not [p for p in files if p.endswith("Order.md") and "alpha" in p], sorted(files) + # The back-link to the model page stays in the page's own package. + assert "alpha/lib/Widget.md" in files["api/alpha/lib/Widget.md"] + assert "beta/app/Order.md" in files["api/beta/app/Order.md"] + assert "alpha" not in files["api/beta/app/Order.md"] diff --git a/server/typescript/packages/codegen-ts/src/overwrite-policy.ts b/server/typescript/packages/codegen-ts/src/overwrite-policy.ts index 64c63d06d..ed3a29b58 100644 --- a/server/typescript/packages/codegen-ts/src/overwrite-policy.ts +++ b/server/typescript/packages/codegen-ts/src/overwrite-policy.ts @@ -503,13 +503,26 @@ function classifyWrite( // File exists. Load the canonical snapshot if any. const genStateDir = resolveGenStateDir(opts); const relPath = opts.outputRelPath ?? defaultOutputRelPath(path); - const snapshot = readSnapshotChecked(genStateDir, relPath); + const storedSnapshot = readSnapshotChecked(genStateDir, relPath); const current = readFileSync(path, "utf-8"); + // A snapshot body is a gitignored local cache, so it outlives a `git checkout` that + // moves the file to content it never described. `--baseline=fresh` is documented to + // overwrite a file the committed manifest has NO RECORD of; a stale body for such a path + // is not a merge base, and merging against it keeps the old content as a "hand edit". + // Only `fresh`, and only for a path with no recorded hash: every other mode still merges. + const snapshot = + storedSnapshot !== undefined && + (opts.baseline ?? "default") === "fresh" && + readGeneratedHash(genStateDir, relPath) === undefined + ? undefined + : storedSnapshot; + // First-time regen on a pre-existing file (no snapshot) — caveat 3. `baseline` // only ever applies here: BaselineMode's own doc comment says so ("When // .gen-state is absent but the file exists"), and a snapshot body — handled - // below — always gets the real three-way merge regardless of baseline. + // below — always gets the real three-way merge, except the unrecorded-path + // case of `fresh` just above. if (snapshot === undefined) { if ((opts.baseline ?? "default") === "fresh") { // Opt-in escape hatch: overwrite and seed the snapshot from fresh. diff --git a/server/typescript/packages/codegen-ts/test/baseline-fresh-stale-snapshot.test.ts b/server/typescript/packages/codegen-ts/test/baseline-fresh-stale-snapshot.test.ts new file mode 100644 index 000000000..ed27f3612 --- /dev/null +++ b/server/typescript/packages/codegen-ts/test/baseline-fresh-stale-snapshot.test.ts @@ -0,0 +1,89 @@ +// `--baseline=fresh` over a stale merge-base snapshot. +// +// The snapshot bodies under `.metaobjects/.gen-state/` are a gitignored local cache, so they +// survive a `git checkout` that moves the output files to content they never described. +// `--baseline=fresh` is documented to overwrite a file the committed manifest has no record of; +// it used to three-way merge against such a body instead, keeping the old content as a "hand +// edit" that `verify --codegen` then reported. + +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { MetaDataLoader } from "@metaobjectsdev/metadata"; +import { FileSource } from "@metaobjectsdev/metadata/core"; +import { runGen } from "../src/runner.js"; +import { defineConfig } from "../src/metaobjects-config.js"; +import { entityFile } from "../src/generators/entity-file.js"; +import { queriesFile } from "../src/generators/queries-file.js"; +import type { BaselineMode } from "../src/overwrite-policy.js"; + +const FIXTURE = resolve(import.meta.dir, "fixtures", "single-entity.json"); + +let projectRoot: string; +beforeEach(() => { projectRoot = mkdtempSync(join(tmpdir(), "adopt-baseline-")); }); +afterEach(() => { rmSync(projectRoot, { recursive: true, force: true }); }); + +const genStateDir = (): string => join(projectRoot, ".metaobjects", ".gen-state"); +const outDir = (): string => join(projectRoot, "generated"); +const hashesFile = (): string => join(genStateDir(), ".hashes.json"); + +const STALE = "STALE LINE WRITTEN BY AN OLDER ENGINE\n"; + +async function gen(baseline?: BaselineMode) { + const { root } = await new MetaDataLoader().load([new FileSource(FIXTURE)]); + return runGen({ + config: defineConfig({ + outDir: outDir(), + extStyle: "none", + dbImport: "~/server/db", + dialect: "sqlite", + generators: [entityFile(), queriesFile()], + }), + metadata: root, + projectRoot, + ...(baseline !== undefined && { baseline }), + }); +} + +/** Append stale content to every generated file and forget its manifest record, keeping the snapshot bodies. */ +function staleAndUnrecorded(): string[] { + const names = readdirSync(outDir()).filter((e) => statSync(join(outDir(), e)).isFile()); + for (const n of names) writeFileSync(join(outDir(), n), readFileSync(join(outDir(), n), "utf-8") + STALE); + writeFileSync(hashesFile(), "{}\n"); + return names; +} + +describe("--baseline=fresh with a stale snapshot body", () => { + test("overwrites a file the manifest has no record of, instead of merging the old content back", async () => { + await gen(); + const names = staleAndUnrecorded(); + // The cache the adopter still has: a body for each path, no manifest entry. + expect(existsSync(join(genStateDir(), "generated", names[0]!))).toBe(true); + + const result = await gen("fresh"); + + for (const n of names) { + expect(readFileSync(join(outDir(), n), "utf-8")).not.toContain("STALE LINE"); + } + expect(result.files.some((f) => f.status === "merged" || f.status === "conflict")).toBe(false); + expect(result.files.filter((f) => f.status === "overwrite").length).toBe(names.length); + }); + + test("without --baseline=fresh the same state still merges (the cache is only ignored by fresh)", async () => { + await gen(); + const names = staleAndUnrecorded(); + await gen(); + // The stale line is a hand edit against the snapshot base, so the default flow keeps it. + expect(readFileSync(join(outDir(), names[0]!), "utf-8")).toContain("STALE LINE"); + }); + + test("a recorded path still three-way merges under fresh", async () => { + await gen(); + const names = readdirSync(outDir()).filter((e) => statSync(join(outDir(), e)).isFile()); + const f = join(outDir(), names[0]!); + writeFileSync(f, readFileSync(f, "utf-8") + "// my hand edit\n"); + await gen("fresh"); + expect(readFileSync(f, "utf-8")).toContain("// my hand edit"); + }); +}); diff --git a/server/typescript/packages/migrate-ts/src/check-expr-compare.ts b/server/typescript/packages/migrate-ts/src/check-expr-compare.ts index de7d059b4..33c3b1e8a 100644 --- a/server/typescript/packages/migrate-ts/src/check-expr-compare.ts +++ b/server/typescript/packages/migrate-ts/src/check-expr-compare.ts @@ -28,13 +28,26 @@ // are fixed in the quote-aware walker below, which is the only place that can tell a real // cast from a `::` inside a regex literal. +/** + * `` `status` `` → `status`, OUTSIDE single-quoted literals. SQLite stores a CHECK exactly as + * it was written, and Drizzle-written migrations quote identifiers with backticks, so the + * stored text and the double-quoted one generated here would otherwise never compare equal. + * A backtick inside a literal (a regex pattern) is data and is left alone. + */ +function unwrapBacktickIdentifiers(expr: string): string { + return expr + .split(/('(?:[^']|'')*')/) + .map((part, i) => (i % 2 === 1 ? part : part.replace(/`(\w+)`/g, "$1"))) + .join(""); +} + /** * Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`, * lower-case, collapse whitespace, canonicalize cast target types, and collapse spacing * around commas, casts and SYMBOLIC operators (outside single-quoted literals). */ export function normalizeCheckExpr(expr: string): string { - const stripped = expr + const stripped = unwrapBacktickIdentifiers(expr) .toLowerCase() // Drop `::text` / `::"MyType"` type casts PG adds to literals. The lookbehind // restricts the strip to a cast that immediately follows a CLOSING single diff --git a/server/typescript/packages/migrate-ts/src/introspect/sqlite-shared.ts b/server/typescript/packages/migrate-ts/src/introspect/sqlite-shared.ts index 7d2c8ea5b..271e9fd4d 100644 --- a/server/typescript/packages/migrate-ts/src/introspect/sqlite-shared.ts +++ b/server/typescript/packages/migrate-ts/src/introspect/sqlite-shared.ts @@ -126,12 +126,17 @@ export function parseSqliteChecks(createSql: string | null | undefined): CheckDe // constraint. Positions are 1:1, so the balanced expression is sliced from the // ORIGINAL (with its real quotes intact). const masked = maskCommentsAndStrings(createSql); - // An optional `CONSTRAINT ` prefix then `CHECK (`. Named → group 1/2; + // An optional `CONSTRAINT ` prefix then `CHECK (`. Named → group 1-4; // bare inline `CHECK (` → name defaults to "" (anonymous, expression-matched). - const re = /(?:\bCONSTRAINT\s+(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_$]*))\s+)?\bCHECK\s*\(/gi; + // SQLite accepts four identifier quotings: "name", `name` (what Drizzle-written migrations + // use), [name] and a bare word. A quoting not read here leaves the name empty, and the + // declared check then never matches its own stored twin: a false add/drop pair. + const re = /(?:\bCONSTRAINT\s+(?:"((?:[^"]|"")+)"|`((?:[^`]|``)+)`|\[([^\]]+)\]|([A-Za-z_][A-Za-z0-9_$]*))\s+)?\bCHECK\s*\(/gi; let m: RegExpExecArray | null; while ((m = re.exec(masked)) !== null) { - const name = m[1] !== undefined ? m[1].replace(/""/g, '"') : (m[2] ?? ""); + const name = m[1] !== undefined ? m[1].replace(/""/g, '"') + : m[2] !== undefined ? m[2].replace(/``/g, "`") + : (m[3] ?? m[4] ?? ""); const open = re.lastIndex - 1; // position of the "(" the regex just consumed let depth = 0; let inString = false; diff --git a/server/typescript/packages/migrate-ts/test/check-evolution/normalize.test.ts b/server/typescript/packages/migrate-ts/test/check-evolution/normalize.test.ts index 52927b452..24723d9d5 100644 --- a/server/typescript/packages/migrate-ts/test/check-evolution/normalize.test.ts +++ b/server/typescript/packages/migrate-ts/test/check-evolution/normalize.test.ts @@ -14,6 +14,10 @@ describe("normalizeCheckExpr", () => { expect(checkExprEquals("length(code) >= 3", "(length(code) >= 3)")).toBe(true); expect(checkExprEquals("status IN ('A', 'B')", "status in ('A', 'B')")).toBe(true); }); + test("a backtick-quoted identifier equals the double-quoted one, but a backtick in a literal is data", () => { + expect(checkExprEquals("`status` IN ('open', 'closed')", '"status" IN (\'open\', \'closed\')')).toBe(true); + expect(checkExprEquals("slug ~ 'a`b`'", "slug ~ 'ab'")).toBe(false); + }); test("PG `= ANY (ARRAY[...])` rewrite of an IN-list equals the generated IN form", () => { // The crux idempotency case: PG stores `status IN ('OPEN','CLOSED')` and // pg_get_constraintdef returns `status = ANY (ARRAY['OPEN'::text, 'CLOSED'::text])`. diff --git a/server/typescript/packages/migrate-ts/test/unit/review-holes.test.ts b/server/typescript/packages/migrate-ts/test/unit/review-holes.test.ts index 5e10551fd..3a08b6d39 100644 --- a/server/typescript/packages/migrate-ts/test/unit/review-holes.test.ts +++ b/server/typescript/packages/migrate-ts/test/unit/review-holes.test.ts @@ -70,6 +70,22 @@ describe("parseSqliteChecks — anonymous + comment/literal safety", () => { }); }); +describe("parseSqliteChecks — every identifier quoting SQLite accepts names the constraint", () => { + // Drizzle-written migrations quote with backticks. An unread quoting left the name empty, + // so the declared check never matched its stored twin: a false add-check/drop-check pair. + test.each([ + ['"items_status_chk"', "items_status_chk"], + ["`items_status_chk`", "items_status_chk"], + ["[items_status_chk]", "items_status_chk"], + ["items_status_chk", "items_status_chk"], + ["`odd``name`", "odd`name"], + ['"odd""name"', 'odd"name'], + ])("CONSTRAINT %s", (quoted, name) => { + const sql = `CREATE TABLE "items" ("id" INTEGER, \`status\` text CONSTRAINT ${quoted} CHECK (\`status\` IN ('open', 'closed')))`; + expect(parseSqliteChecks(sql)).toEqual([{ name, expression: "`status` IN ('open', 'closed')" }]); + }); +}); + describe("hole 2 — drop-view exemption is keyed by (schema, name), not name", () => { const view = (name: string, schema?: string): ViewDescriptor => ({ name, sql: "SELECT 1", ...(schema !== undefined ? { schema } : {}) }) as ViewDescriptor;