1.14.0 #548
zantvoort
announced in
Announcements
1.14.0
#548
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A quality release: a smaller, more coherent API, and SQL that is correct on every dialect Storm supports rather than on the permissive ones, held there by a Technology Compatibility Kit that every dialect runs.
Requires Java 21+ and Kotlin 2.0+.
Added
widen()andnarrow(rootType)make the two directions explicit,widen()admitting short-form references on a query that joins nothing andnarrowrestoring the root for the operations defined relative to it,resultGroupedByandscroll. A path on an entity the query does not carry fails when the query is built, naming the entity, the root, and the paths that pin the table where it appears more than once.groupBy(User_.city)emitsGROUP BY c.idon PostgreSQL, MySQL, SQLite and H2, and every column of the referenced key on SQL Server and Oracle, which do not resolve functional dependency from a key. The relationship, the key beyond it, the root's own key and a reference all state the same identity; any other path groups by its value. Selecting an entity selects the tables its foreign keys reach, so a grouping covers those keys too, and since every foreign key is to-one this cannot move a row into a different group.storm-dialect-tckmodule holds 146 conformance tests over entity repository, polymorphic, schema validation and multi-column expression behavior, and all seven dialects run all four of them; the seven per-dialect copies ofEntityRepositoryTestfall from 13,931 lines to 2,498, and the other three per-dialect files are gone. Fourteen behaviors used to run on H2 alone, the most permissive dialect in the set, among them the key chains where a key is a reference the mapper has to flatten rather than a column. Where dialects legitimately differ they state a capability once (supportsSequences,supportsFetchWithSequences,supportsBatchUpsertAndFetchWithSequences,supportsRowValueIn,supportsRowValueComparison) and pin their own statement text, with a drift guard that fails naming any statement a dialect can reach but has not pinned. A dialect that cannot do something demonstrates that it refuses cleanly rather than being silently exempt. Consolidating surfaced coverage gaps that had gone unnoticed: MariaDB ran 10 of 16 multi-column expression tests, SQL Server skipped 12 sequence tests it passes, and Oracle's schema init masked failures behindcontinue-on-error. The module is build-time only and is not published.'?', which is valid SQL, so the value binds to the position of the next placeholder and every parameter after it shifts; where the leftover count balances, the statement runs against the wrong arguments and returns results that look ordinary. A statement that binds more positional parameters than it exposes placeholders is now refused when it is built, naming the cause and the SQL, and the Kotlin compiler plugin reports it in the editor where it is written. The check reads the compiled SQL once per cached statement shape and only walks it when the SQL contains a quote at all; more placeholders than parameters stays legal, since that is bind vars.st.orm.sql.slowatWARN, carrying the statement, the rows, and how the execution compares to what its shape typically costs (typically 6.0 ms, 306x) and binds (parameters 32 (typically 3)), which says whether to look at the parameters or at the query.storm.sql-log.slow.threshold=200msin Spring,sqlLogSlowThresholdin Ktor,storm.sql_log.slow.thresholdon a plain JVM. Values render atTRACEonly, lines are rate-limited per shape, and the threshold derives from the performance log when it has none of its own, so the derived default names the statement behind a warning instead of adding warnings of its own.SlowStatementLog.threshold(Duration)andlimit(int)land on the next execution, and the performance log reads its settings per unit of work. Where the actuator is on the class path, thestormendpoint reads and sets both halves (GET/POST /actuator/storm), Storm's control surface rather than the SQL log's. Onlyenabledstays a startup decision, since it installs the request filter and the entry-point proxies.@StormTestand@DataStormTestrun on the database the application deploys on (Run @StormTest against a real database with Testcontainers #502):database = POSTGRESQL(orMYSQL,MARIADB,MSSQL_SERVER,ORACLE) starts a Testcontainers-managed container once per JVM and gives each test class a freshly created database inside it, withimageoverriding the pinned default. The database's Testcontainers 2 module and JDBC driver stay out ofstorm-test's dependencies, and a test that names a container database fails naming both when one is missing.TestDatabase.POSTGRESQL.container()exposes the shared container, andcreateDatabase()a database of your own.@StormTestruns each test inside a database transaction that is rolled back afterwards (Per-test rollback for @StormTest #387), so tests no longer observe each other's writes. Transaction blocks demarcate with savepoints inside it;rollback = falseopts a class out.OrmTemplateFactoryin the Spring Boot starters (feat: property-driven template composition for Spring Boot and Ktor #520): one bean that composes a fully integratedORMTemplatewherever the application defines its own template beans. Failure translation followsstorm.exception-translation.enabledper data source, observations resolve their conventions from each data source's JDBC URL and report the template's name asstorm.database, and a customize block applies application-specific composition without touching the integration SPI.storm.observations.semanticConventions = otelselects the OpenTelemetry conventions per database, and acustomizeslot applies composition after the integration is wired, inherited per named database unless it sets its own. Observer composition is shared with the starters throughQueryObserversin storm-micrometer, so the two stacks cannot drift.JtaTransactionManager(Run Storm-initiated transactions through a JTA transaction manager #504) where no resource-bound manager claims the data source, alongsideJdbcTransactionManagerand aJpaTransactionManagerbacked by it. An option a manager refuses is reported against the option passed to the block.manualCommitConnections()(Support DataSources that hand out manual-commit connections #450): a template declares that itsDataSourcehands out connections with auto-commit already disabled, and the non-transactional paths manage auto-commit accordingly. A connection that arrives with auto-commit disabled without the declaration fails naming the two possible causes and the fix.andandorcombinators root at their operands' least common root (feat: root the and/or combinators at the operands' least common root #519), so a cross-root conjunction is one clause rather than an escalation towhereBuilder { }. On a narrow builder the combination is unsatisfiable and is rejected at the call site, at compile time; on a widened builder it solves to the common supertype and defers to the query-build validation, exactly like consecutivewhere()clauses.StormConfig.sqlShapingKeys()(Derive the shape-affecting config keys instead of hand-maintaining TEMPLATE_SHAPE_KEYS #405) exposes the configuration keys whose values affect the generated SQL, and the template cache key includes exactly these.select { }anddelete { }blocks add the remaining clause forms (Bring the select { } block up to parity with the chained builder #397):where(records),where(path, records),where(path, operator, values),whereRef(path, refs)and the descending order template.whereRefworks insidewhereBuilder { }too.narrow,widenandtypedIdstay outside the block by design.Changed
storm-coreisprovidedon the library modules andruntimeon the Spring Boot starters, so completion and imports offer exactly oneEntityRepository, oneORMTemplate, oneQueryBuilder: the facade's. A project that follows the installation guide (implementationon the facade,runtimeOnly("st.orm:storm-core")) is unaffected; one that relied on the engine arriving transitively adds the runtime dependency explicitly. Creating a template without the engine on the runtime classpath names the missingst.orm:storm-coredependency instead of surfacing as aNoClassDefFoundErrorat the first statement. On the module path the facades requirestorm-foundationtransitively;storm.corestays non-transitive.st.orm.spiin storm-foundation (refactor: keep the engine off the application compile classpath #527):ExceptionMapper,ExceptionContext,SqlOperation,QueryObserver,QueryContext,StatementOrigin,SqlCommenterand the cursor codecs;SqlTemplateExceptionjoinsPersistenceExceptioninst.orm. Implementations update their imports, andMETA-INF/services/st.orm.core.spi.CursorCodecProviderrenames tost.orm.spi.CursorCodecProvider. Transaction bridging,RefFactoryand the dialect surface stay engine SPI instorm-core.QueryObserver.onTransactionreceives thest.orm.TransactionOptionsan application passes totransaction(options) { }, and@StormTestinjectsSchemaValidationrather than the engine's validator, keeping test classpaths engine-free.typed(pkType)is renamedtypedId(pkType)(A join widens the query: replace the Any variants with root widening #371): it types the erased primary-key parameter, wherenarrowtypes the root.fetch(...)comes right afterselect(), before any join, enforced at compile time. Kotlin'sselect { }anddelete { }blocks are widened from the start, return the widened builder, and carry the chained builder's clause vocabulary.storm.sql-log.performance.*for the performance log (st.orm.sql.perf), which says what a unit of work cost the database, andstorm.sql-log.slow.*for the slow statement log (st.orm.sql.slow), which names the execution that cost too much. The old flat keys are removed rather than mapped, so a configuration still onstorm.sql-log.enabledbinds to nothing and starts with the performance log off and no message. The Ktor DSL, the HOCON keys, the system properties and the Spring types follow:StormPerformanceLogFilterandStormPerformanceLogEntryPointPostProcessor. Database time is measured to the statement's return everywhere, so the summary,SqlCapture.durationand the slow line agree.jakarta.annotation-apiis no longer a dependency. JSpecify itself is optional: compilers and analysis tools read the annotations from bytecode. Kotlin callers get realT/T?types where the Java surface used to be platform types, and the generated nullable metamodel chain compiles on Kotlin 2.1+ (Kotlin 2.1+ consumers cannot compile generated metamodels against the null-marked API #458).Projection<ID>'s type argument is a checked contract instead of a phantom parameter (Projection's ID type parameter is phantom and carries two meanings #419):IDis the projection's row identity type, which for a foreign-key-typed primary key is the referenced table's key. Record validation rejects a declaration the record contradicts, and the rule that a foreign key must not be an auto-generated primary key applies to entities only.st.orm.core.template.implandst.orm.core.repository.implare exported to Storm's own modules only, and storm-kotlin'simplpackages areinternalthroughout, including theFlowoperators that collided with their kotlinx.coroutines namesakes. All five Kotlin modules compile in explicit API mode; the coroutine-aware SQL log recording the Ktor plugin shares is the one exception, behind@InternalStormApi.REQUIREDblock inside aNOT_SUPPORTEDorNEVERblock opens a transaction of its own,MANDATORYandNEVERare checked against the block the enclosing code declares, and data source consistency is checked per physical transaction, so an audit write can go to a second database from inside a transaction on the first.StormTransactionObservationConvention(Transaction observations bypass the Micrometer convention, and the Ktor delegating observer drops them entirely #400), the way query observations always did, and the Ktor plugin forwards them. Query observations carry the shape identity as the low-cardinalitystorm.shapekey value, cached per compiled template; the identity uses the full 64 bits it is declared with and renders as sixteen hex digits.<Type>NullableMetamodelchain variant, and KSP sources components from the primary constructor, contributes abstract properties only for sealed interfaces, and escapes keyword-named properties. The Java processor registers with Gradle as an aggregating incremental processor, so attaching it no longer forces full recompilation, and the Gradle plugin wires it into every source set (Gradle plugin: test source sets get no metamodel, and the javadoc task misses the preview flag #415), so entities in test sources get a metamodel.@DataStormTestslice imports every auto-configuration the starters register in production (@DataStormTest slice: repositories are unproxied and standard verification tools are missing #401), verified by a parity test, and providesJdbcTemplate,JdbcClientand Testcontainers service connection support.whereandwhereRefclauses acceptNavigablepaths, so navigation-only nodes reached through aRefcompile in both languages; Java'swhere(path, record)bounds the record byData. Kotlin'sfindRefBydrops two unused type parameters,findAllRefBy(field, values)accepts any value type, andTemplatesgains the namedparam(name, Calendar, TemporalType)variant. The two-argumentselectFrom(fromType, selectType)renders the select list from the FROM table, soselectTypemay be any record shape over those columns.SqlLogcarries the diagnostics API only (Qualify the JPMS exports of the impl packages and move SqlLog's rendering internals out of the public class #395): summary rendering lives inSqlLogRendererand call-site capture inCallSiteCapture, both internal. The hydration shape is gone from summary rows; the per-type report belongs tostorm analyze(storm analyze: one command for entity graph cost and the design budget #503).storm-java21/storm-kotlin, the two Spring Boot starters,storm-jackson2/storm-jackson3and the two metamodel processors, ship the same fully qualified names, one half per class path. Each half bans its twin through a maven-enforcer rule.@GenerateMetamodelmoves tost.orm(@GenerateMetamodel is dead: both processors look for st.orm.GenerateMetamodel but the annotation lives in st.orm.core.template #378), where both processors can find it: the annotation processor module must not depend on storm-foundation.Fixed
getResultCount()counted the rows a select returned instead of executing a count query (getResultCount() streams and counts the full result set while its javadoc promises a count query #379). It now emits a realCOUNTquery and page totals are inferred from it.Petwhile grouping byPet_.owner, is refused when the query is built, on every dialect rather than only the ones that would notice.JpaTransactionManager(Storm-initiated transactions fail when a JpaTransactionManager is on the classpath #383). Matching is JPA-first, ambiguity fails fast naming the candidates, and the auto-configuration orders after the JDBC and Hibernate managers on Spring Boot 3 and 4.withConfig, and discovery silently picked the first candidate (withConfig discards an explicitly set dialect, and getSqlDialect(StormConfig) resolves first-match #404). Resolution is lazy and reports the candidates by name.ClassValue, a class-loader-keyed cache and weakDataSourcekeys replace the strong static maps.WHEREwas detected as top-level (SQL lexing edge cases: keyword detection without word boundaries, nested WHERE defeating the unsafe-statement check #391), and unknown interpolation-safety modes passed silently (Interpolation safety check: bypassed by one explicit t(), no else on the mode switch, mode name mismatch #392); the compiler plugin marker is now required.Metamodel.KeyDelegate.isNullable()always returned false through the documented factory (Metamodel.KeyDelegate.isNullable() is always false through the documented factory #403).Metamodel.key()wraps anything that is not already aKey, and the delegate's check tested forKey, so every factory-built delegate reported non-nullable; nullability now derives from the underlying field.@Jsonfields with the runtime rather than the declared type, so a polymorphic value missed the discriminator that reading the column expects.RefFactorywas not restored after nested JSON deserialization (JSON converters: the RefFactory ThreadLocal is not reentrant, nested deserialization produces detached refs #385): three converters left theThreadLocalpointing at the nested factory. A kotlinx@Contextual Reftarget must itself be@Serializable.Refobjects failed to deserialize (Ref with a compound primary key serializes but cannot deserialize #390); the plain-object primary key path is gated onfindRecordType.INFORMATION_SCHEMA.TABLESstrategy gated onsequencesDiscovered.@StormTestshared one database across test classes (StormExtension: parallel-execution store collision, database-name collision, abstract-base breakage #386): a class-keyed store and ananoTime-derived database name isolate them, and the annotation is@Inherited.SqlCapturelost statements across Kotlin coroutine boundaries and skipped an action that returns no row (SqlCapture cannot capture from Kotlin coroutines, and Kotlin lacks a blocking sqlLog #388, fix: SqlCapture captures an action that returns no row #490).runbecomesrecord, storm-kotlin-test gains a blockingrecording { }scope, and the Ktor test scope wires into the Setup phase so it captures for real.application.conf(Ktor configuration gaps: named databases inherit almost nothing, sqlLog mutates JVM globals and has no config-file support #399); the plugin closed pools it did not create and failed on eager dependency-injection resolution (Ktor plugin lifecycle: closes user-supplied pools, requires HikariCP to shut down, eager DI resolution can abort startup #398).whereAny/whereAllbuilder methods and Kotlin KDoc called Java names such asgetResultList(). ArequireColumnguard replaces the phantom references.--devargument and hid fetch failures (CLI: unknown commands fall into the setup wizard, --dev leaks its argument, failures report success #414).NullPointerException(refactor: keep the engine off the application compile classpath #527) instead of an error naming the cause.Removed
whereAny,whereAnyRef,whereAnyBuilder,havingAny,groupByAny,orderByAny,orderByDescendingAny,andAnyandorAny(A join widens the query: replace the Any variants with root widening #371): a join widens the query, soand/orinherit the query root andwiden()covers the query that joins nothing.QueryTemplate.dialect()from the Java API (refactor: keep the engine off the application compile classpath #527): which dialect executes a query is engine configuration. It remains on the engine's own template surface.connectionProvider(...)andtransactionTemplateProvider(...)from the facade builders (refactor: keep the engine off the application compile classpath #527). Bridging a transaction manager is framework-level composition, available asSpringOrmTemplate.builder(dataSource, beanFactory)andspringOrmTemplateBuilder(dataSource, beanFactory), or on the engine builder directly.QueryBuilder.append(page(Pageable) runs a count query even when the fetched page determines the total #428, Remove QueryBuilder.append: raw trailing fragments bypass builder invariants #429) andhasOrderBy()(refactor: keep the engine off the application compile classpath #527) from the public API.jakarta.annotation-apias a dependency (Adopt JSpecify on the public API surface #418): the public API is JSpecify null-marked instead.storm.sql-log.*keys (Slow statement log: report individual slow executions under st.orm.sql.slow #517), replaced by theperformanceandslowsections rather than mapped.Upgrading
runtimeOnly("st.orm:storm-core"), or<scope>runtime</scope>in Maven. The Spring Boot starters and the Gradle plugin already do this for you.st.orm.core.spiimports tost.orm.spiin anyExceptionMapper,QueryObserver,SqlCommenterorCursorCodecProviderimplementation, and rename theCursorCodecProviderservice file.Anyclause variants:whereAny(...)and friends become plainwhere(...)after a join, orwiden().where(...)on a query that joins nothing.typed(pkType)becomestypedId(pkType), andfetch(...)moves to directly afterselect().storm.sql-log.enabledbecomesstorm.sql-log.performance.enabled, and the thresholds, limit, call sites, line width and entry points move underperformance. The old keys bind to nothing and log no warning.@StormTestnow rolls back each test, androllback = falseopts a class out.implpackage: the Kotlinimplsurfaces areinternaland the coreimplpackages are exported to Storm's own modules only.Full changelog: v1.13.1...v1.14.0
This discussion was created from the release 1.14.0.
All reactions