Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ GROUP BY c.id, c.name, c.population

- The public API is JSpecify null-marked: every Java package carries `@NullMarked`, nullable type uses carry `org.jspecify.annotations.Nullable`, and the jakarta annotations are gone from Storm's signatures, so `jakarta.annotation-api` is no longer a Storm dependency. JSpecify itself is optional: the compilers and analysis tools read the annotations from bytecode by name. Kotlin callers get real `T`/`T?` types where the Java surface used to be platform types, checked strictly by Kotlin 2.1+. Type parameters that admit nullable arguments declare it — `V extends @Nullable Object` on `TypedMetamodel`, `AbstractMetamodel` and `AbstractKeyMetamodel`, `@Nullable` elements on `Instantiator`'s argument arrays, `@Nullable` on `Metamodel.getValue`, `T extends @Nullable Object` on `SqlCapture.execute` and `executeThrowing` — so Kotlin 2.1+ consumers compile the generated nullable metamodel chain. Generated metamodel sources adapt to the class path, referencing no annotation library when JSpecify is absent.
- `@StormTest` runs each test inside a database transaction that is rolled back afterwards, so tests no longer observe each other's writes and count assertions can be exact regardless of execution order. Storm's transaction blocks demarcate with savepoints inside it. `rollback = false` opts a class out for tests that need real commits.
- `@StormTest` and `@DataStormTest` run on the database the application deploys on: `database = POSTGRESQL` (or `MYSQL`, `MARIADB`, `MSSQL_SERVER`, `ORACLE`) starts a Testcontainers-managed container of a pinned default image, or of the image named by `image`, once per JVM and shares it across the test classes of the run; each test class, or Spring context, receives a freshly created database inside the container, so scripts run against an empty database as they do on H2 and classes never observe each other's tables. Testcontainers stays out of `storm-test`'s dependencies: a test that names a container database needs the database's Testcontainers module and JDBC driver on its classpath and fails with a message naming both when one is missing, while tests on H2 pull in nothing new. `TestDatabase.POSTGRESQL.container()` exposes the shared container, and `createDatabase()` on it a database of your own, for setups outside the annotations.
- `Projection<ID>`'s type argument is a checked contract instead of a phantom parameter. `ID` is the projection's row identity type; for a foreign-key-typed primary key that is the referenced table's key rather than the component value, which is why the interface declares no id accessor and `Ref.of(projection, id)` takes the id explicitly. Record validation rejects a declaration the record contradicts. The rule that a foreign key must not be an auto-generated primary key applies to entities only now.
- The implementation is sealed. `st.orm.core.template.impl` and `st.orm.core.repository.impl` are exported to Storm's own modules only; every declaration under storm-kotlin's `st.orm.template.impl` and `st.orm.repository.impl` is `internal`, including the `Flow` operators that collided with their kotlinx.coroutines namesakes and the top-level predicate factories that polluted completion. All five Kotlin modules compile in explicit API mode. The coroutine-aware SQL log recording the Ktor plugin shares is the one deliberate exception, published as `st.orm.template.recordSqlLog` behind `@InternalStormApi`. On the class path nothing changes; an application on the module path that reached into these packages no longer compiles.

Expand Down
4 changes: 2 additions & 2 deletions docs/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Beyond the data model, Storm provides dedicated tooling for AI-assisted workflow

- **Skills** guide AI tools through specific tasks (entity creation, queries, repositories, migrations) with framework-aware conventions and rules.
- **A locally running MCP server** gives AI tools access to your live database schema: table definitions, column types, constraints, and foreign keys. Optionally, the AI can also query individual records (read-only) when sample data would improve type decisions. The AI can inspect your actual database structure to generate entities that match, or validate entities it just created.
- **Built-in verification** through `ORMTemplate.validateSchema()` and `SqlCapture` lets the AI validate its own work. After generating entities, the AI can validate them against the database. After writing queries, it can capture and inspect the actual SQL. Both checks run in an isolated in-memory database through `@StormTest`, so verification happens before anything touches production. For dialect-specific code, `@StormTest` supports a static `dataSource()` factory method on the test class, allowing integration with Testcontainers to test against the actual target database.
- **Built-in verification** through `ORMTemplate.validateSchema()` and `SqlCapture` lets the AI validate its own work. After generating entities, the AI can validate them against the database. After writing queries, it can capture and inspect the actual SQL. Both checks run in an isolated in-memory database through `@StormTest`, so verification happens before anything touches production. For dialect-specific code, `@StormTest(database = POSTGRESQL)` runs the same test against the actual target database in a Testcontainers-managed container.

---

Expand Down Expand Up @@ -299,7 +299,7 @@ The workflow:
3. **Fix (if needed).** If the test fails, the error messages tell the AI exactly what is wrong. It fixes the entities, queries, or migration and re-runs the test.
4. **Clean up.** Once the test passes, the AI deletes the temporary test file (and any temporary SQL scripts it created). The verified code stays; the scaffolding goes.

This works because `@StormTest` spins up an H2 in-memory database by default, executes the setup scripts, and tears everything down after the test. No external database, no persistent state, no side effects. When the code under test uses dialect-specific SQL, define a static `dataSource()` factory method on the test class to provide a Testcontainers-backed `DataSource` for the target database instead of H2.
This works because `@StormTest` spins up an H2 in-memory database by default, executes the setup scripts, and tears everything down after the test. No external database, no persistent state, no side effects. When the code under test uses dialect-specific SQL, set `database = POSTGRESQL` (or `MYSQL`, `MARIADB`, `MSSQL_SERVER`, `ORACLE`) on the annotation to run the same test against the target database in a Testcontainers-managed container instead of H2.

You can also ask the AI to keep the test as a permanent regression test. The choice is yours, and the AI should ask.

Expand Down
15 changes: 9 additions & 6 deletions docs/dialects.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,21 +237,24 @@ For basic testing without upsert support, H2 works without any dialect dependenc

## Integration Testing with Real Databases

While H2 is excellent for fast unit tests, it does not support all database-specific features (JSONB, arrays, database-specific functions). For thorough testing, you should also run integration tests against your production database. Each dialect module includes a `docker-compose.yml` file that starts the corresponding database in a container, making integration testing straightforward. For example, to test with PostgreSQL:
While H2 is excellent for fast unit tests, it does not support all database-specific features (JSONB, arrays, database-specific functions), and it accepts SQL the production database rejects. For thorough testing, you should also run integration tests against your production database. The `database` attribute of [`@StormTest`](testing.md#testing-against-the-database-you-deploy-on) and [`@DataStormTest`](spring-integration.md#testing-with-datastormtest) runs a test class on that database in a Testcontainers-managed container, started once per run and shared across test classes:

```bash
cd storm-postgresql
docker-compose up -d
mvn test -pl storm-postgresql
```java
@StormTest(database = POSTGRESQL, scripts = {"/schema.sql", "/data.sql"})
class OwnerRepositoryTest {
// the same test, running against PostgreSQL with the storm-postgresql dialect
}
```

Storm's own dialect modules are tested the same way. Each of them also includes a `docker-compose.yml` file that starts the corresponding database, for working against a long-running local instance.

## Tips

1. **Always include the dialect** for production databases; without it you lose the database-specific features
2. **Use H2 or SQLite** for unit tests; add `storm-h2` or `storm-sqlite` for upsert support
3. **Dialect is runtime-only**; it doesn't affect your compile-time code or entity definitions
4. **One dialect per application**; Storm auto-detects the right dialect from your connection URL
5. **Test with both**: Use H2/SQLite for fast unit tests and the production dialect for integration tests
5. **Test with both**: Use H2/SQLite for fast unit tests and the production dialect for integration tests, through `@StormTest(database = ...)`

---

Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ The Spring Boot starters include `storm-micrometer`; Ktor applications add it ex

| Module | Provides |
|--------|----------|
| `storm-test` | `@StormTest` JUnit 5 extension and `SqlCapture`, framework-free |
| `storm-test` | `@StormTest` JUnit 5 extension and `SqlCapture`, framework-free; runs on H2 or, with the database's Testcontainers module added, on PostgreSQL, MySQL, MariaDB, SQL Server or Oracle |
| `storm-kotlin-test` | The suspending `recording` extension, carrying `SqlCapture` across coroutines (test scope) |
| `storm-spring-boot-test-autoconfigure` | The `@DataStormTest` Spring Boot test slice (test scope) |

Expand Down
2 changes: 1 addition & 1 deletion docs/ktor-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ class UserRouteTest {
}
```

Both approaches use H2 in-memory databases by default. For testing against a real database (e.g., PostgreSQL with Testcontainers), provide a custom DataSource. See [Testing](testing.md) for the full testing guide.
Both approaches use H2 in-memory databases by default. To run a `@StormTest` class against the database the application deploys on, set `database = TestDatabase.POSTGRESQL` (or the database you use) on the annotation; the injected `DataSource` then points at a Testcontainers-managed container, and the Ktor test needs no other change. `testStormApplication` takes `url`, `username` and `password` for the same purpose; a database provisioned through `TestDatabase.POSTGRESQL.container().createDatabase()` supplies all three. See [Testing](testing.md#testing-against-the-database-you-deploy-on) for the full testing guide.

---

Expand Down
27 changes: 26 additions & 1 deletion docs/spring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,32 @@ class VisitRepositoryTest(
}
```

To run against a real database instead, disable the replacement with `spring.test.database.replace=none` and hand the slice a Testcontainers-managed database through `@ServiceConnection`:
To run the same slice on the database the application deploys on, name it with the `database` attribute. The slice starts a Testcontainers-managed container of that database once per JVM, shared by every test class that asks for it, gives each Spring context a fresh database inside the container, and points `spring.datasource.*` at it. Nothing else changes; the same `schema.sql`, `data.sql`, Flyway or Liquibase setup that initializes the embedded database initializes the container database:

```kotlin
@DataStormTest(database = TestDatabase.POSTGRESQL)
class VisitRepositoryPostgresTest(
@Autowired private val visitRepository: VisitRepository,
) {

@Test
fun `finds all visits`() {
visitRepository.count() shouldBe 14
}
}
```

The attribute is the one [`@StormTest`](testing.md#testing-against-the-database-you-deploy-on) has, with the same databases (`POSTGRESQL`, `MYSQL`, `MARIADB`, `MSSQL_SERVER`, `ORACLE` from `st.orm.test.TestDatabase`), the same pinned default images and `image` override, and the same dependency requirements: the Testcontainers module for the database and its JDBC driver on the test classpath, and for SQL Server the license acceptance file. Both annotations share their containers within a JVM.

What the slice sets, and why:

- `spring.datasource.url`, `spring.datasource.username` and `spring.datasource.password` point at the provisioned database, ahead of anything the application's configuration files say.
- `spring.test.database.replace=none`, so neither Boot's embedded replacement nor the slice's own Boot 4 fallback swaps the container database out again.
- `spring.sql.init.mode=always`, unless the application configures `spring.sql.init.mode` itself. Boot runs `schema.sql` and `data.sql` for embedded databases only; the container database is as disposable as the embedded one, so they run there too.

The database is created when the context is created and dropped when it closes. Test classes with the same configuration share one context, and with it one database; classes that name a different database or image get a context and a database of their own.

A container the slice does not manage, for example one configured beyond what the attribute offers, still works the way it did: disable the replacement with `spring.test.database.replace=none` and hand the slice the database through `@ServiceConnection`:

```kotlin
@DataStormTest(properties = ["spring.test.database.replace=none"])
Expand Down
Loading
Loading