Skip to content

Commit 61d4db7

Browse files
dmealingclaude
andcommitted
Merge omdb-spring-boot-starter: Spring Boot 3 starter + OMDB autoconfiguration
- Fix dead Boot 3 autoconfiguration registration (AutoConfiguration.imports; drop legacy spring.factories); strip OSGi remnants from core-spring. - DatabaseDriverResolver (dialect override + JDBC product auto-detect). - ObjectManager autoconfiguration: builds a transaction-aware ObjectManagerDB from a Spring DataSource (SpringObjectManagerDB + SpringObjectConnections), gated by @ConditionalOnBean(DataSource)/@ConditionalOnMissingBean; + metaobjects.omdb.* properties; metaobjects.metadata-sources now autoconfigured. - New thin metaobjects-spring-boot-starter module (metadata + omdb + core-spring). - Virtual-thread audit documented (OMDB does not pin carriers); jOOQ recorded as a closed non-goal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents 48a571d + 89943cf commit 61d4db7

22 files changed

Lines changed: 1599 additions & 22 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,6 @@ See `spec/roadmap.md` for current and planned work across the H1-H10 project ser
505505
## Open questions
506506

507507
- [TECHNICAL] Field-type → Drizzle-column-type mapping table (needed for complete TS codegen coverage).
508-
- [TECHNICAL] ObjectManagerDB further modernization (jOOQ migration, Spring Boot 3 starter, async via virtual threads). FR-003 Plan 4 (2026-05-27) closed the three engine-debt anti-patterns identified at the time — atomic mapping cache off shared `MetaObject`, JDBC codec registry replacing the type ladders (ADR-0002), and `inTransaction` template method. The remaining items above are larger architectural moves rather than near-term cleanups.
508+
- [TECHNICAL] ObjectManagerDB further modernization. FR-003 Plan 4 (2026-05-27) closed the three engine-debt anti-patterns. The Spring Boot 3 starter + OMDB autoconfiguration + virtual-thread audit shipped 2026-05-30 (`metaobjects-spring-boot-starter`). **jOOQ migration is a closed non-goal**: jOOQ's OSS edition excludes Oracle/SQL Server/DB2 (commercial license required), which would paywall OMDB's commercial-DB drivers in a public OSS project, and jOOQ generates code *from* a schema — the inverse of MetaObjects' metadata-is-the-spine model.
509509
- [TECHNICAL] Payload `origin.*` resolution in `codegen-spring` (Day-1 deferral — see `server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/KNOWN_GAPS.md`). Kotlin's `KotlinPayloadGenerator` is the cross-port reference.
510510
- [TECHNICAL] WARN envelope-shape assertion on cross-port `expected-warnings.json` (closed 2026-05-27 — runners now assert envelope shape on warnings; legacy string-list path retired).

docs/superpowers/plans/2026-05-30-omdb-spring-boot-starter.md

Lines changed: 935 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# OMDB Spring Boot 3 Starter + Autoconfiguration — Design
2+
3+
_Date: 2026-05-29_
4+
5+
## Problem
6+
7+
The Java port's Spring integration (`core-spring`) registers its autoconfiguration
8+
**only** through the legacy Boot 2.x `META-INF/spring.factories`
9+
`EnableAutoConfiguration` key. **Spring Boot 3 no longer honors that key**
10+
autoconfiguration classes must be listed in
11+
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`.
12+
The project builds on **Java 21** and **Spring Boot 3.5.6**, so today a consumer who
13+
adds the dependency and expects `@SpringBootApplication` to pick MetaObjects up gets
14+
**nothing** auto-wired; they must manually `@Import(MetaDataAutoConfiguration.class)`.
15+
The existing integration test masks this because it wires the config via
16+
`@ContextConfiguration(classes = MetaDataAutoConfiguration.class)` rather than
17+
exercising Boot's autoconfiguration *discovery*.
18+
19+
Separately, `core-spring` only wires the **metadata loader** (`MetaDataLoaderRegistry`,
20+
a primary `MetaDataLoader`, `MetaDataService`). There is **no** autoconfiguration for
21+
the OMDB persistence layer: a Spring app that wants an `ObjectManagerDB` must hand-build
22+
it (`new ObjectManagerDB()` + `setDatabaseDriver(...)` + `setDataSource(...)`).
23+
24+
This work modernizes the Spring integration to Boot 3, ships an idiomatic
25+
one-dependency **starter**, adds **OMDB persistence autoconfiguration**, and hardens
26+
OMDB for Java 21 virtual threads. It deliberately does **not** adopt jOOQ (see
27+
Non-Goals).
28+
29+
## Scope
30+
31+
In scope:
32+
1. **Boot 3 registration fix**`AutoConfiguration.imports`; delete the dead
33+
`spring.factories` entry.
34+
2. **OMDB persistence autoconfiguration** — build an `ObjectManagerDB` from a Spring
35+
`DataSource` + the primary `MetaDataLoader`, transaction-integrated via the existing
36+
`SpringObjectConnections`.
37+
3. **Thin `metaobjects-spring-boot-starter`** module — one-dependency onboarding.
38+
4. **Virtual-thread hardening (cheap)** — audit OMDB's blocking JDBC paths for
39+
carrier-thread pinning (`synchronized` across JDBC calls) and convert to
40+
`ReentrantLock` where found. No async API, no forced toggle.
41+
5. **Tidy** — strip stale "OSGi-compatible" language from the autoconfig javadoc
42+
(OSGi was removed in 7.1.0); refresh the `core-spring` README.
43+
44+
Out of scope / Non-Goals:
45+
- **jOOQ migration.** jOOQ's free Open Source Edition supports only open-source
46+
databases; Oracle / SQL Server / DB2 require a *paid* license. OMDB ships drivers for
47+
Postgres / MySQL / MSSQL / Oracle / Derby, so a jOOQ migration would paywall or drop
48+
commercial-DB support in a public OSS project. jOOQ also generates code *from* a DB
49+
schema, the inverse of MetaObjects' metadata-is-the-spine model, and would rip out the
50+
driver layer just cleaned up in FR-003 Plan 4. Recorded as a closed open-question.
51+
- Forcing `spring.threads.virtual.enabled` on consumers.
52+
- Any change to the OMDB SQL-generation or driver dialect logic beyond the pinning audit.
53+
54+
## Architecture
55+
56+
### Module structure
57+
58+
- **`core-spring`** remains the **autoconfigure** module. It **already** compile-depends
59+
on `omdb` (for `SpringObjectConnections`/`ObjectConnectionDB`), so no dependency change
60+
is needed; the OMDB beans are gated at runtime by `@ConditionalOnBean(DataSource)` (plus
61+
a defensive `@ConditionalOnClass(ObjectManagerDB)`), so they only activate when the
62+
consumer actually has a `DataSource`.
63+
- **New `metaobjects-spring-boot-starter`** — a thin module (no Java code) whose pom
64+
depends on `metadata` + `omdb` + `core-spring`. Adding this single dependency gives a
65+
consumer the full metadata-loader + OMDB-persistence wiring.
66+
- `omdb` / `om` are unchanged except for the §"Virtual-thread hardening" pinning audit.
67+
68+
Net change: one new module; `core-spring` gains one optional dependency.
69+
70+
### Autoconfiguration classes
71+
72+
Two independent `@AutoConfiguration` classes (loader and persistence stay decoupled):
73+
74+
**`MetaDataAutoConfiguration`** (existing — behavior unchanged):
75+
- `MetaDataLoaderRegistry` (`@Primary`), primary `MetaDataLoader`, `MetaDataService`.
76+
- Driven by the existing `metaobjects.*` `@ConfigurationProperties`
77+
(`MetaDataLoaderConfiguration`).
78+
- Only the registration mechanism and stale OSGi javadoc change.
79+
80+
**`ObjectManagerAutoConfiguration`** (new):
81+
- Produces an `ObjectManagerDB` bean from the Spring `DataSource` + the primary
82+
`MetaDataLoader`.
83+
- Connections flow through the existing `SpringObjectConnections` so Spring-managed
84+
transactions / `@Transactional` participate correctly (FR-003 Spring-tx groundwork).
85+
- Conditions:
86+
- `@ConditionalOnClass(ObjectManagerDB.class)` — only when `omdb` is on the classpath.
87+
- `@ConditionalOnBean(DataSource.class)` — only when a DataSource exists.
88+
- `@ConditionalOnMissingBean(ObjectManagerDB.class)` — consumer override always wins.
89+
- New `metaobjects.omdb.*` `@ConfigurationProperties`: `dialect`,
90+
`enforce-transaction` (maps to `ObjectManagerDB.setEnforceTransaction`).
91+
92+
### Driver selection
93+
94+
`DatabaseDriver` is chosen by **auto-detection with an explicit override**:
95+
- If `metaobjects.omdb.dialect` is set (`postgres|mysql|mssql|oracle|derby`), use the
96+
matching driver.
97+
- Otherwise open one connection and map
98+
`DatabaseMetaData.getDatabaseProductName()` to the driver
99+
(`PostgreSQL``PostgresDriver`, `MySQL``MySQLDriver`,
100+
`Microsoft SQL Server``MSSQLDriver`, `Oracle``OracleDriver`,
101+
`Apache Derby``DerbyDriver`). Unknown product name → a clear
102+
`MetaDataException` instructing the user to set `metaobjects.omdb.dialect`.
103+
104+
Rationale: zero-config for the common case, explicit escape hatch otherwise. The
105+
product-name→driver mapping lives in a small, separately-tested helper.
106+
107+
> **Implementation note:** confirm during planning whether `ObjectManagerDB` needs the
108+
> `MetaDataLoader`/registry wired beyond `DataSource` + `DatabaseDriver` (the integration
109+
> tests set only driver + datasource). If the OM requires the loader for object-class
110+
> binding, wire the primary `MetaDataLoader` in the same bean method.
111+
112+
## Virtual-thread hardening
113+
114+
OMDB stays a synchronous JDBC API. On Java 21, blocking calls run fine on virtual
115+
threads **provided** no `synchronized` monitor is held across a blocking JDBC call
116+
(that pins the carrier thread). Audit the OMDB blocking paths — the FR-003 Plan 4
117+
atomic-mapping-cache locking and the `synchronized getDatabaseDriver()` are the prime
118+
suspects — and convert any `synchronized`-around-JDBC to `java.util.concurrent.locks.ReentrantLock`.
119+
If the audit finds no JDBC-spanning monitor, document that and make no change (no churn).
120+
The starter does not set `spring.threads.virtual.enabled`; it only guarantees OMDB is
121+
pinning-safe when a consumer enables it, and documents the option.
122+
123+
## Boot 3 registration fix + tidy
124+
125+
- Add `core-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
126+
listing `MetaDataAutoConfiguration` and `ObjectManagerAutoConfiguration`.
127+
- Delete `core-spring/src/main/resources/META-INF/spring.factories`.
128+
- Remove "OSGi-compatible" / OSGi references from `MetaDataAutoConfiguration` javadoc and
129+
any sibling that still mentions it. Refresh the `core-spring` README: document the
130+
one-dependency starter and the `metaobjects.omdb.*` properties.
131+
132+
## Testing
133+
134+
- **Autoconfiguration discovery** (`ApplicationContextRunner`, the gap the current
135+
`@ContextConfiguration` test masks):
136+
- With a DataSource + OMDB present → the `ObjectManagerDB` bean is created.
137+
- With no DataSource → `ObjectManagerAutoConfiguration` backs off (no bean, no error).
138+
- With a consumer-defined `ObjectManagerDB` → autoconfig backs off (`@ConditionalOnMissingBean`).
139+
- The loader autoconfig still produces its beans through real discovery.
140+
- **Driver selection** — unit-test the product-name→driver mapping and the
141+
`metaobjects.omdb.dialect` override; unknown product name → clear error.
142+
- **Transaction-binding slice** — an embedded-Derby test (Derby is a real OMDB driver,
143+
auto-detected from product name; core-spring has no Testcontainers, and full persist/read
144+
is already covered by the `integration-tests` module against Postgres/Derby). With a
145+
Derby `DataSource` + `DataSourceTransactionManager`, assert the *autoconfigured*
146+
`ObjectManagerDB`'s `getConnection()` returns the SAME physical connection Spring bound
147+
to the active transaction, and that `close()` on it is a no-op (mirrors the existing
148+
`SpringObjectConnectionTest`). This proves the new Spring-tx wiring without duplicating
149+
OMDB persistence coverage.
150+
- Existing `core-spring` tests stay green.
151+
152+
## Sequencing
153+
154+
1. Boot 3 registration fix + OSGi tidy (loader autoconfig only) + discovery test —
155+
smallest standalone fix; verifies the bug is closed.
156+
2. `ObjectManagerAutoConfiguration` + driver-selection helper + `metaobjects.omdb.*`
157+
properties + slice tests.
158+
3. `metaobjects-spring-boot-starter` thin module + README.
159+
4. Virtual-thread pinning audit (+ fix if found).
160+
5. Record jOOQ as a closed non-goal in CLAUDE.md open questions.
161+
162+
## Open questions
163+
164+
- Whether `ObjectManagerDB` requires the `MetaDataLoader` wired beyond driver +
165+
datasource for object-class binding (resolve in planning by reading the OM API).
166+
- Boot 2.x support is dropped (Boot 2 is EOL); the starter targets Boot 3.5+ only.

server/java/core-spring/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
This module provides comprehensive Spring Framework integration for MetaObjects, including auto-configuration, dependency injection, and Spring Boot support.
44

5+
## Spring Boot starter
6+
7+
Add the single starter dependency:
8+
9+
<dependency>
10+
<groupId>com.metaobjects</groupId>
11+
<artifactId>metaobjects-spring-boot-starter</artifactId>
12+
<version>7.1.1-SNAPSHOT</version>
13+
</dependency>
14+
15+
On Spring Boot 3, this auto-configures the metadata-loader registry and — when a
16+
`DataSource` is present — a transaction-aware `ObjectManagerDB`.
17+
18+
Properties:
19+
20+
| Property | Default | Description |
21+
|---|---|---|
22+
| `metaobjects.metadata-sources` | (none) | Loader source URIs/resources. |
23+
| `metaobjects.omdb.dialect` | (auto) | `postgres`/`mysql`/`mssql`/`oracle`/`derby`. Auto-detected from the DataSource when blank. |
24+
| `metaobjects.omdb.enforce-transaction` | `false` | Require an active transaction for writes. |
25+
26+
Define your own `ObjectManagerDB` bean to opt out of the autoconfigured one.
27+
528
## 🚀 **Quick Start**
629

730
### **Add Dependency**
@@ -416,10 +439,23 @@ Optional<MetaObject> user = metaDataService.findMetaObjectByNameOptional("User")
416439
- Thread-safe for concurrent access
417440

418441
### **Memory Management**
419-
- Uses WeakHashMap patterns for OSGi compatibility
442+
- Uses WeakHashMap patterns to avoid leaking classloaders
420443
- Automatic cleanup when Spring context shuts down
421444
- Minimal memory overhead for Spring integration
422445

446+
## Virtual threads (Java 21)
447+
448+
OMDB is a synchronous JDBC API and is safe to run on JVM virtual threads. An audit of
449+
the OMDB blocking paths found no `synchronized` monitor held across a JDBC call (the only
450+
synchronized methods guard in-memory state — the driver lazy-init, the event-listener
451+
list, and per-object attribute maps), so OMDB does not pin carrier threads.
452+
453+
To run a Spring Boot 3.2+ application's request handling on virtual threads, set:
454+
455+
spring.threads.virtual.enabled=true
456+
457+
The starter does not enable this for you — it is the application's choice.
458+
423459
---
424460

425461
**This Spring integration provides seamless MetaObjects functionality within Spring applications while maintaining all the framework's powerful features.**

server/java/core-spring/pom.xml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@
6969
<version>6.2.11</version>
7070
<scope>test</scope>
7171
</dependency>
72+
<dependency>
73+
<groupId>org.springframework.boot</groupId>
74+
<artifactId>spring-boot-test</artifactId>
75+
<version>3.5.6</version>
76+
<scope>test</scope>
77+
</dependency>
7278
<dependency>
7379
<groupId>com.metaobjects</groupId>
7480
<artifactId>metaobjects-metadata</artifactId>
@@ -83,6 +89,32 @@
8389
<version>2.2.224</version>
8490
<scope>test</scope>
8591
</dependency>
92+
<!-- Embedded Derby for OMDB autoconfiguration tests -->
93+
<dependency>
94+
<groupId>org.apache.derby</groupId>
95+
<artifactId>derby</artifactId>
96+
<version>${derby.version}</version>
97+
<scope>test</scope>
98+
</dependency>
99+
<dependency>
100+
<groupId>org.apache.derby</groupId>
101+
<artifactId>derbyshared</artifactId>
102+
<version>${derby.version}</version>
103+
<scope>test</scope>
104+
</dependency>
105+
<dependency>
106+
<groupId>org.apache.derby</groupId>
107+
<artifactId>derbytools</artifactId>
108+
<version>${derby.version}</version>
109+
<scope>test</scope>
110+
</dependency>
111+
<!-- AssertJ for ApplicationContextRunner assertions (not transitive here) -->
112+
<dependency>
113+
<groupId>org.assertj</groupId>
114+
<artifactId>assertj-core</artifactId>
115+
<version>3.26.3</version>
116+
<scope>test</scope>
117+
</dependency>
86118
</dependencies>
87119

88120
<build>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.metaobjects.spring;
2+
3+
import com.metaobjects.manager.db.DatabaseDriver;
4+
import com.metaobjects.manager.db.driver.DerbyDriver;
5+
import com.metaobjects.manager.db.driver.MSSQLDriver;
6+
import com.metaobjects.manager.db.driver.MySQLDriver;
7+
import com.metaobjects.manager.db.driver.OracleDriver;
8+
import com.metaobjects.manager.db.driver.PostgresDriver;
9+
10+
import javax.sql.DataSource;
11+
import java.sql.Connection;
12+
import java.sql.SQLException;
13+
14+
/**
15+
* Resolves the OMDB {@link DatabaseDriver} for a Spring {@link DataSource}: an explicit
16+
* {@code metaobjects.omdb.dialect} wins; otherwise the JDBC
17+
* {@code DatabaseMetaData.getDatabaseProductName()} is mapped to a driver.
18+
*/
19+
final class DatabaseDriverResolver {
20+
21+
private DatabaseDriverResolver() {}
22+
23+
static DatabaseDriver resolve(String dialect, DataSource dataSource) {
24+
if (dialect != null && !dialect.isBlank()) {
25+
return forDialect(dialect.trim().toLowerCase());
26+
}
27+
return forProduct(detectProduct(dataSource));
28+
}
29+
30+
static DatabaseDriver forDialect(String dialect) {
31+
switch (dialect) {
32+
case "postgres":
33+
case "postgresql":
34+
return new PostgresDriver();
35+
case "mysql":
36+
return new MySQLDriver();
37+
case "mssql":
38+
case "sqlserver":
39+
return new MSSQLDriver();
40+
case "oracle":
41+
return new OracleDriver();
42+
case "derby":
43+
return new DerbyDriver();
44+
default:
45+
throw new IllegalArgumentException(
46+
"Unknown metaobjects.omdb.dialect '" + dialect +
47+
"'. Use one of: postgres, mysql, mssql, oracle, derby.");
48+
}
49+
}
50+
51+
static DatabaseDriver forProduct(String product) {
52+
String p = product == null ? "" : product.toLowerCase();
53+
if (p.contains("postgresql")) return new PostgresDriver();
54+
if (p.contains("mysql")) return new MySQLDriver();
55+
if (p.contains("sql server")) return new MSSQLDriver();
56+
if (p.contains("oracle")) return new OracleDriver();
57+
if (p.contains("derby")) return new DerbyDriver();
58+
throw new IllegalStateException(
59+
"Could not auto-detect an OMDB driver for database product '" + product +
60+
"'. Set metaobjects.omdb.dialect explicitly (postgres|mysql|mssql|oracle|derby).");
61+
}
62+
63+
private static String detectProduct(DataSource dataSource) {
64+
try (Connection c = dataSource.getConnection()) {
65+
return c.getMetaData().getDatabaseProductName();
66+
} catch (SQLException e) {
67+
throw new IllegalStateException(
68+
"Failed to read DatabaseMetaData for OMDB driver auto-detection: " + e.getMessage(), e);
69+
}
70+
}
71+
}

server/java/core-spring/src/main/java/com/metaobjects/spring/MetaDataAutoConfiguration.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,20 @@
44
import com.metaobjects.registry.MetaDataLoaderRegistry;
55
import com.metaobjects.registry.ServiceRegistryFactory;
66
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.autoconfigure.AutoConfiguration;
78
import org.springframework.context.annotation.Bean;
8-
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.context.annotation.Import;
910
import org.springframework.context.annotation.Primary;
1011

1112
import java.util.List;
1213

1314
/**
14-
* Spring Auto-Configuration for MetaObjects OSGi-compatible registry.
15-
*
15+
* Spring Boot auto-configuration for the MetaObjects metadata-loader registry.
16+
*
1617
* <p>This configuration automatically:</p>
1718
* <ul>
18-
* <li>Creates OSGi-compatible MetaDataLoaderRegistry using ServiceRegistryFactory</li>
1919
* <li>Auto-discovers MetaDataLoader beans from Spring context</li>
2020
* <li>Provides backward-compatible @Autowired injection</li>
21-
* <li>Works in both OSGi and non-OSGi environments</li>
2221
* </ul>
2322
*
2423
* <p><strong>Usage in Spring Controllers:</strong></p>
@@ -42,18 +41,19 @@
4241
*
4342
* @since 6.0.0
4443
*/
45-
@Configuration
44+
@AutoConfiguration
45+
@Import(MetaDataLoaderConfiguration.class)
4646
public class MetaDataAutoConfiguration {
4747

4848
/**
49-
* Creates OSGi-compatible MetaDataLoaderRegistry with auto-discovery
49+
* Creates the MetaDataLoaderRegistry with auto-discovery
5050
* of MetaDataLoader beans from Spring context.
5151
*/
5252
@Bean
5353
@Primary
5454
public MetaDataLoaderRegistry metaDataLoaderRegistry() {
5555

56-
// Create OSGi-compatible registry (auto-detects environment)
56+
// Create the loader registry
5757
MetaDataLoaderRegistry registry = new MetaDataLoaderRegistry(
5858
ServiceRegistryFactory.getDefault()
5959
);

0 commit comments

Comments
 (0)