REGISTRY = buildRegistry();
/** Classpath root every ejectable Java generator's reference source is shipped under
diff --git a/server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringNamesGenerator.java b/server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringNamesGenerator.java
index db2a63beb..a4729117a 100644
--- a/server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringNamesGenerator.java
+++ b/server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringNamesGenerator.java
@@ -100,17 +100,14 @@
* Args:
*
* - {@code outputDir} (required): output directory root.
- * - {@code columnNaming} (optional): the column-naming strategy — one of
- * {@link ColumnNaming#LITERAL}, {@link ColumnNaming#SNAKE_CASE},
- * {@link ColumnNaming#KEBAB_CASE}. Defaults to {@link ColumnNaming#DEFAULT}
- * (literal) — matching {@code ObjectManagerDB}'s runtime resolution, NOT Kotlin's
- * {@code snake_case} codegen default. A Java artifact defaulting differently from
- * the Java runtime would itself be the kind of drift this program exists to
- * remove. Codegen cannot see a runtime {@code setColumnNaming(...)} call on
- * {@code ObjectManagerDB} — a project pairing that call with this generator must
- * pass the SAME strategy string to both, by hand (see
- * {@code docs/features/field-types.md}).
*
+ *
+ * {@code columnNaming} is REMOVED (ADR-0058, 1.1.0). Column naming is one
+ * project setting now — {@code naming.columns} in {@code .metaobjects/config.json}, read
+ * from the loaded root ({@link com.metaobjects.database.PhysicalNaming#of(MetaData)}) —
+ * the SAME rule OMDB's {@code SimpleMappingHandlerDB} resolves, so a Java artifact and the
+ * Java runtime cannot disagree any more. A pom still passing {@code } fails
+ * the build by name.
*/
public class SpringNamesGenerator extends MultiFileDirectGeneratorBase
implements EmitsPhysicalNameConstants {
@@ -124,7 +121,14 @@ protected Class getFilterClass() {
public void execute(MetaDataLoader loader) {
parseArgs();
Path outRoot = Paths.get(outDir.getAbsolutePath());
- String strategy = getArg("columnNaming", ColumnNaming.DEFAULT);
+ // ADR-0058 — the pom generator arg is REMOVED; refuse by name rather than silently
+ // ignoring a value that used to work, which would re-point this artifact's columns
+ // without telling anyone.
+ if (hasArg("columnNaming")) {
+ throw new GeneratorException(ColumnNaming.removedConfigurationMessage(
+ "the columnNaming generator arg in pom.xml", getArg("columnNaming")));
+ }
+ String strategy = com.metaobjects.database.PhysicalNaming.of(loader.getRoot()).columns();
// Pass 1 — every object that participates in the database (#248).
Set emitted = new HashSet<>();
for (MetaObject entity : loader.getMetaObjects()) {
diff --git a/server/java/codegen-spring/src/test/java/com/metaobjects/generator/spring/SpringNamesGeneratorTest.java b/server/java/codegen-spring/src/test/java/com/metaobjects/generator/spring/SpringNamesGeneratorTest.java
index 1bca109a1..f09f76138 100644
--- a/server/java/codegen-spring/src/test/java/com/metaobjects/generator/spring/SpringNamesGeneratorTest.java
+++ b/server/java/codegen-spring/src/test/java/com/metaobjects/generator/spring/SpringNamesGeneratorTest.java
@@ -101,9 +101,23 @@ public class SpringNamesGeneratorTest extends SharedRegistryTestBase {
// -------------------------------------------------------------------------
private String emit(String fixtureJson, String relativeFilePath, Map extraArgs) throws IOException {
+ return emit(fixtureJson, relativeFilePath, extraArgs, null);
+ }
+
+ /**
+ * @param columnsRule ADR-0058 — when non-null, set on the loaded root as the project's
+ * {@code naming.columns} rule before the generator runs (replaces the removed
+ * {@code columnNaming} generator arg).
+ */
+ private String emit(String fixtureJson, String relativeFilePath, Map extraArgs,
+ String columnsRule) throws IOException {
Path outDir = tempFolder.newFolder().toPath();
Path workspace = tempFolder.newFolder().toPath();
MetaDataLoader loader = SpringTestFixtures.loadFixture(workspace, "names-fixture", fixtureJson);
+ if (columnsRule != null) {
+ loader.getRoot().setPhysicalNaming(
+ com.metaobjects.database.PhysicalNaming.of(columnsRule, "snake_case"));
+ }
SpringNamesGenerator gen = new SpringNamesGenerator();
Map args = new HashMap<>(extraArgs);
@@ -124,6 +138,10 @@ private String emitFor(String entityShortName, Map args) throws
return emit(AUTHOR_FIXTURE, "acme/blog/" + entityShortName + "Names.java", args);
}
+ private String emitFor(String entityShortName, String columnsRule) throws IOException {
+ return emit(AUTHOR_FIXTURE, "acme/blog/" + entityShortName + "Names.java", Map.of(), columnsRule);
+ }
+
// -------------------------------------------------------------------------
// Step 1 tests (task-7-brief), plus the corrections/extensions its own text calls for
// -------------------------------------------------------------------------
@@ -156,8 +174,9 @@ public void emitsStaticFinalConstantsForTableAndColumns() throws IOException {
assertTrue(src, src.contains("public static final String IDENTITY_PRIMARY_SUB_TYPE = \"primary\";"));
assertFalse(src, src.contains("IDENTITY_PRIMARY_INDEX"));
assertTrue(src, src.contains("public static final String CREATED_AT_FIELD = \"createdAt\";"));
- // Java's default strategy is `literal`, matching ObjectManagerDB -- NOT snake_case.
- assertTrue(src, src.contains("public static final String CREATED_AT_COLUMN = \"createdAt\";"));
+ // ADR-0058: the project default is now `snake_case` in every port (what
+ // `meta migrate` has always created), not Java's former `literal` default.
+ assertTrue(src, src.contains("public static final String CREATED_AT_COLUMN = \"created_at\";"));
assertTrue(src, src.contains("protected AuthorNames() {}"));
}
@@ -171,21 +190,38 @@ public void anImplicitlyNamedSourceStillResolvesItsPhysicalName() throws IOExcep
}
@Test
- public void theColumnNamingArgIsHonoured() throws IOException {
- assertTrue(emitFor("Author", Map.of("columnNaming", "snake_case"))
+ public void theProjectsNamingColumnsRuleIsHonoured() throws IOException {
+ // ADR-0058: the rule is read from the loaded root (naming.columns in
+ // .metaobjects/config.json), never a generator arg.
+ assertTrue(emitFor("Author", "literal")
+ .contains("public static final String CREATED_AT_COLUMN = \"createdAt\";"));
+ assertTrue(emitFor("Author", "snake_case")
.contains("public static final String CREATED_AT_COLUMN = \"created_at\";"));
}
+ @Test
+ public void theColumnNamingGeneratorArgIsRefused() throws IOException {
+ try {
+ emitFor("Author", Map.of("columnNaming", "snake_case"));
+ fail("expected the removed columnNaming generator arg to fail the build");
+ } catch (GeneratorException e) {
+ assertEquals(
+ com.metaobjects.database.ColumnNaming.removedConfigurationMessage(
+ "the columnNaming generator arg in pom.xml", "snake_case"),
+ e.getMessage());
+ }
+ }
+
@Test
public void anExplicitColumnBeatsTheStrategyUnderBothArms() throws IOException {
// Neither strategy would independently produce "purpose_code"
// (literal -> "callPurpose", snake_case -> "call_purpose") -- the explicit
// @column must win under BOTH.
- String literal = emitFor("Author");
+ String literal = emitFor("Author", "literal");
assertTrue(literal, literal.contains("public static final String CALL_PURPOSE_COLUMN = \"purpose_code\";"));
assertFalse(literal, literal.contains("\"call_purpose\""));
- String snake = emitFor("Author", Map.of("columnNaming", "snake_case"));
+ String snake = emitFor("Author", "snake_case");
assertTrue(snake, snake.contains("public static final String CALL_PURPOSE_COLUMN = \"purpose_code\";"));
assertFalse(snake, snake.contains("\"call_purpose\""));
}
@@ -539,17 +575,20 @@ public void everyEmittedColumnConstantExistsInTheCanonicalSchema() throws IOExce
Path workspace = tempFolder.newFolder().toPath();
MetaDataLoader loader = SpringTestFixtures.loadFixture(workspace, "fitness", canonicalMeta);
- SpringNamesGenerator gen = new SpringNamesGenerator();
- Map args = new HashMap<>();
- args.put("outputDir", outDir.toString());
// The canonical schema is pinned to the `literal` strategy -- see
// server/typescript/packages/integration-tests/src/canonical-schema.ts's
// CANONICAL_COLUMN_NAMING and fixtures/persistence-conformance/README.md's
- // "Result format" section, which both say so outright. That is ALSO Java's
- // own codegen default (see SpringNamesGenerator's javadoc), but it is passed
- // explicitly here rather than left implicit, so this test keeps pinning the
- // resolver against real DDL even if that default ever changes.
- args.put("columnNaming", "literal");
+ // "Result format" section, which both say so outright, and (ADR-0058) the
+ // corpus's own committed fixtures/persistence-conformance/.metaobjects/
+ // config.json declares it: {"naming": {"columns": "literal"}}. Read through the
+ // SAME NeutralConfig reader every port uses, and set on the loaded root — never
+ // a generator arg (removed).
+ loader.getRoot().setPhysicalNaming(
+ com.metaobjects.config.SourceResolver.resolvePhysicalNaming(corpusRoot));
+
+ SpringNamesGenerator gen = new SpringNamesGenerator();
+ Map args = new HashMap<>();
+ args.put("outputDir", outDir.toString());
gen.setArgs(args);
gen.execute(loader);
@@ -608,11 +647,15 @@ public void anObjectWithMoreThanTenFieldsCompiles() throws Exception {
Path outDir = tempFolder.newFolder().toPath();
Path workspace = tempFolder.newFolder().toPath();
MetaDataLoader loader = SpringTestFixtures.loadFixture(workspace, "fitness-compile", canonicalMeta);
+ // ADR-0058: the corpus's committed naming.columns: literal (see
+ // everyEmittedColumnConstantExistsInTheCanonicalSchema above) — read from the
+ // root, never a generator arg.
+ loader.getRoot().setPhysicalNaming(
+ com.metaobjects.config.SourceResolver.resolvePhysicalNaming(corpusRoot));
SpringNamesGenerator gen = new SpringNamesGenerator();
Map args = new HashMap<>();
args.put("outputDir", outDir.toString());
- args.put("columnNaming", "literal");
gen.setArgs(args);
gen.execute(loader);
diff --git a/server/java/core-spring/src/main/java/com/metaobjects/spring/MetaDataLoaderConfiguration.java b/server/java/core-spring/src/main/java/com/metaobjects/spring/MetaDataLoaderConfiguration.java
index 41a16c764..87cd1930d 100644
--- a/server/java/core-spring/src/main/java/com/metaobjects/spring/MetaDataLoaderConfiguration.java
+++ b/server/java/core-spring/src/main/java/com/metaobjects/spring/MetaDataLoaderConfiguration.java
@@ -1,5 +1,6 @@
package com.metaobjects.spring;
+import com.metaobjects.config.NeutralConfig;
import com.metaobjects.loader.LoaderOptions;
import com.metaobjects.loader.MetaDataLoader;
@@ -8,9 +9,13 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
+import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import java.io.IOException;
+import java.io.InputStream;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -30,6 +35,9 @@
* - "file:/path/to/external-metadata.xml"
* auto-attrs: true
* strict: false
+ * # ADR-0058 — this classpath runtime has no project directory, so give it the
+ * # project's .metaobjects/config.json directly (a FILE location, never a bare rule).
+ * physical-naming-config: "classpath:.metaobjects/config.json"
* }
*
* Or use @Bean methods directly:
@@ -56,11 +64,24 @@ public class MetaDataLoaderConfiguration {
private boolean autoAttrs = true;
private boolean strict = false;
private boolean verbose = false;
-
+
+ /**
+ * ADR-0058 — where to read the project's physical-naming rules from, for a runtime
+ * that loads metadata off the classpath and so has no project directory for
+ * {@code SourceResolver.resolvePhysicalNaming(Path)} to resolve against. A Spring
+ * resource location pointing at the {@code .metaobjects/config.json} FILE itself
+ * (never a directory) — e.g. {@code classpath:.metaobjects/config.json} or
+ * {@code file:/opt/app/.metaobjects/config.json}. Parsed with {@link NeutralConfig};
+ * never a bare rule value ({@code naming.columns}/{@code naming.tables}) configured
+ * here directly — ADR-0058 removed every such setter. Absent means the default
+ * ({@code snake_case}/{@code snake_case}), same as a project with no config file.
+ */
+ private String physicalNamingConfig;
+
/**
* Creates MetaDataLoader from application properties.
- *
- * This bean will be automatically discovered and registered by
+ *
+ *
This bean will be automatically discovered and registered by
* {@link MetaDataAutoConfiguration}.
*/
@Bean
@@ -70,12 +91,12 @@ public MetaDataLoader applicationMetaDataLoader() throws Exception {
// Default: look for metadata files in classpath
metadataSources = discoverDefaultMetadataSources();
}
-
+
if (metadataSources.isEmpty()) {
// No metadata sources configured - return null so it's optional
return null;
}
-
+
// Convert string sources to URIs
List sourceURIs = new ArrayList<>();
for (String source : metadataSources) {
@@ -86,7 +107,27 @@ public MetaDataLoader applicationMetaDataLoader() throws Exception {
// Preserve the pre-unification strict=true default (the no-options
// fromUris overload routes through createManual which uses strict=false).
LoaderOptions opts = LoaderOptions.create(false, false, true);
- return MetaDataLoader.fromUris("applicationLoader", sourceURIs, opts);
+ MetaDataLoader loader = MetaDataLoader.fromUris("applicationLoader", sourceURIs, opts);
+ applyPhysicalNaming(loader);
+ return loader;
+ }
+
+ /**
+ * ADR-0058 — reads {@link #physicalNamingConfig} (when set) and sets the project's
+ * physical-naming rules on the loaded root, so this classpath-runtime loader resolves
+ * the SAME rule codegen resolved against the project directory. A caller that never
+ * sets {@code metaobjects.physical-naming-config} gets the default, exactly like a
+ * project directory with no {@code .metaobjects/config.json}.
+ */
+ private void applyPhysicalNaming(MetaDataLoader loader) throws IOException {
+ if (physicalNamingConfig == null || physicalNamingConfig.isBlank()) return;
+ Resource resource = new DefaultResourceLoader().getResource(physicalNamingConfig);
+ String content;
+ try (InputStream in = resource.getInputStream()) {
+ content = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ NeutralConfig cfg = NeutralConfig.parse(content, physicalNamingConfig);
+ loader.getRoot().setPhysicalNaming(cfg.getPhysicalNaming());
}
/**
@@ -151,4 +192,7 @@ private List discoverDefaultMetadataSources() {
public boolean isVerbose() { return verbose; }
public void setVerbose(boolean verbose) { this.verbose = verbose; }
+
+ public String getPhysicalNamingConfig() { return physicalNamingConfig; }
+ public void setPhysicalNamingConfig(String physicalNamingConfig) { this.physicalNamingConfig = physicalNamingConfig; }
}
\ No newline at end of file
diff --git a/server/java/core-spring/src/test/java/com/metaobjects/spring/MetaDataLoaderConfigurationPhysicalNamingTest.java b/server/java/core-spring/src/test/java/com/metaobjects/spring/MetaDataLoaderConfigurationPhysicalNamingTest.java
new file mode 100644
index 000000000..b1e6adfd0
--- /dev/null
+++ b/server/java/core-spring/src/test/java/com/metaobjects/spring/MetaDataLoaderConfigurationPhysicalNamingTest.java
@@ -0,0 +1,60 @@
+package com.metaobjects.spring;
+
+import com.metaobjects.database.PhysicalNaming;
+import com.metaobjects.loader.MetaDataLoader;
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * ADR-0058 — a runtime that loads metadata off the classpath has no project directory for
+ * {@code SourceResolver.resolvePhysicalNaming(Path)} to resolve against, so
+ * {@link MetaDataLoaderConfiguration} takes a Spring resource location naming the project's
+ * {@code .metaobjects/config.json} FILE directly ({@code metaobjects.physical-naming-config}),
+ * parses it with {@link com.metaobjects.config.NeutralConfig}, and sets the rules on the
+ * loaded root — never a bare rule value.
+ */
+public class MetaDataLoaderConfigurationPhysicalNamingTest {
+
+ // ConfigurationPropertiesAutoConfiguration must be explicit: ApplicationContextRunner's
+ // AutoConfigurations.of(...) activates ONLY the listed classes, not Spring Boot's full
+ // auto-configuration chain -- without it @ConfigurationProperties binding never runs
+ // and every property silently reads as unset (metadataSources stays empty too, masked
+ // in the sibling MetaDataLoaderAutoConfigurationTest by its classpath-discovery
+ // fallback matching the same resource by coincidence).
+ private final ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(
+ MetaDataAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class));
+
+ // metadataSources is left UNSET on both runs here (matches
+ // MetaDataLoaderAutoConfigurationTest's classpath-discovery path,
+ // discoverDefaultMetadataSources(), which resolves to proper `file:` URIs via Spring's
+ // own Resource abstraction): explicitly naming `metaobjects.metadata-sources` as a bare
+ // "classpath:" string hits a SEPARATE, pre-existing bug in this example bean -- the
+ // property is handed straight to `URI.create(...)`, which has no "classpath:" scheme
+ // handler, unlike `physicalNamingConfig` below, which this feature resolves through
+ // Spring's `DefaultResourceLoader` correctly. That bug is orthogonal to ADR-0058 and
+ // out of scope here.
+
+ @Test
+ public void physicalNamingConfigPropertySetsTheRootsRules() {
+ runner.withPropertyValues("metaobjects.physical-naming-config=classpath:naming-config/config.json")
+ .run(ctx -> {
+ MetaDataLoader primary = ctx.getBean(MetaDataLoader.class);
+ assertThat(primary.getRoot().getPhysicalNaming().columns())
+ .isEqualTo(PhysicalNaming.of("literal", "snake_case").columns());
+ });
+ }
+
+ @Test
+ public void withNoPhysicalNamingConfigPropertyTheRootKeepsTheDefault() {
+ runner.run(ctx -> {
+ MetaDataLoader primary = ctx.getBean(MetaDataLoader.class);
+ assertThat(primary.getRoot().getPhysicalNaming().columns())
+ .isEqualTo(PhysicalNaming.DEFAULT.columns());
+ });
+ }
+}
diff --git a/server/java/core-spring/src/test/resources/naming-config/config.json b/server/java/core-spring/src/test/resources/naming-config/config.json
new file mode 100644
index 000000000..1e4b8166e
--- /dev/null
+++ b/server/java/core-spring/src/test/resources/naming-config/config.json
@@ -0,0 +1,4 @@
+{
+ "schema_version": 1,
+ "naming": { "columns": "literal" }
+}
diff --git a/server/java/integration-tests-kotlin/pom.xml b/server/java/integration-tests-kotlin/pom.xml
index f24e794e9..1ae1c436f 100644
--- a/server/java/integration-tests-kotlin/pom.xml
+++ b/server/java/integration-tests-kotlin/pom.xml
@@ -86,6 +86,14 @@
${project.version}
test
+
+
+ com.metaobjects
+ metaobjects-om
+ ${project.version}
+ test
+