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 @@ -37,6 +37,7 @@ Framework integration points are now instance-scoped (#198), and the Ktor integr
- SQL failures raised by Storm translate to Spring's `DataAccessException` hierarchy in Spring applications: `SpringExceptionMapper` (storm-spring) translates on vendor error codes with `SQLException` subclass and SQL state fallback, auto-configured by both starters (`storm.exception-translation.enabled=false` to disable) and applied by the `SpringOrmTemplate.of` / `springOrmTemplate` compositions. Failures without a `SQLException` cause keep Storm's own exceptions.
- The Spring Boot starters bind query observations automatically: with an `ObservationRegistry` bean present (Actuator provides one), every query executed by the auto-configured template reports as a `storm.query` Micrometer Observation via the shared `StormObservationAutoConfiguration`. The starters ship storm-micrometer; override the convention with an `ObservationConvention` bean, replace the binding with a `QueryObserver` bean, or disable via `management.observations.enable.storm.query=false`.
- `OtelDatabaseObservationConvention` (storm-micrometer): opt-in OpenTelemetry database client semantic conventions on query observations — `db.system.name` (derived from the JDBC URL), `db.operation.name`, and `db.query.text` (placeholders only) alongside the `storm.*` key values, so Storm queries surface in the database tooling of OTLP-capable observability backends. Activated with `storm.observations.semantic-conventions=otel` in the Spring Boot starters (database product detected from the DataSource) or by registering the convention in the Ktor dependency container.
- Trace context in SQL statements: the `SqlCommenter` hook (template builder, all language stacks) appends per-execution comment content to statements after all processing and caching; `TraceContextSqlCommenter` (storm-micrometer) renders the current span as a sqlcommenter-style W3C `traceparent` comment, correlating database-side diagnostics such as slow query logs with traces. Opt-in everywhere — `storm.tracing.sql-comments=true` in the Spring Boot starters (with a `Tracer` present), the `sqlCommenter` slot in the Ktor plugin — since a per-execution comment defeats prepared statement caching. Hostile content is rejected (comment terminator, semicolons), and the emitted comment is padded so MySQL/MariaDB executable-comment and optimizer-hint markers are never interpreted.
- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass.
- `@DataStormTest` test slice in the new storm-spring-boot-test-autoconfigure module, the counterpart of `@DataJpaTest`: starts only the `DataSource`, Storm's auto-configuration, and SQL initialization (plus Flyway/Liquibase when present), keeps regular components out, replaces the data source with an embedded database on Spring Boot 3 (opt out via `spring.test.database.replace=none` for Testcontainers `@ServiceConnection` setups), and rolls back a per-test transaction. One module serves both starters and both Spring Boot 3 and 4: the slice imports the starters' identically named auto-configuration classes, lists relocated platform classes as optional imports, and registers its type exclusion through a context customizer instead of the relocated `@TypeExcludeFilters`.

Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ storm:
enabled: true
observations:
semantic-conventions: storm
tracing:
sql-comments: false
```

The Spring Boot Starter binds these properties and builds a `StormConfig` that is passed to the `ORMTemplate` factory. Values not set in YAML fall back to system properties and then to built-in defaults. See [Spring Integration](spring-integration.md#configuration-via-applicationyml) for details.
Expand Down
8 changes: 8 additions & 0 deletions docs/ktor-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,14 @@ dependencies {
OtelDatabaseObservationConvention.fromJdbcUrl(jdbcUrl)
}
}
```

With tracing in place, the `sqlCommenter` slot appends the current trace context to every statement as a sqlcommenter-style comment, correlating database-side diagnostics such as slow query logs back to the trace. Opt-in: a per-execution comment defeats prepared statement caching.

```kotlin
install(Storm) {
sqlCommenter = TraceContextSqlCommenter(tracer)
}
```
What each observation produces depends on the handlers attached to the registry: timing metrics, tracing spans, or both. Storm spans nest under the current trace context, so with context propagation in place (for example the OpenTelemetry agent, or micrometer-tracing with a `TracingObservationHandler`) queries appear under the active request span.

Expand Down
4 changes: 4 additions & 0 deletions docs/spring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@ storm:
enabled: true
observations:
semantic-conventions: storm
tracing:
sql-comments: false
```

The `schema-mode` property controls startup schema validation: `fail` (default) blocks startup if any entity definitions do not match the database schema, `warn` logs mismatches without blocking startup, and `none` skips validation. The `strict` property controls whether warnings (type narrowing, nullability mismatches) are treated as errors. See the [Configuration](configuration.md#schema-validation) guide for details.
Expand Down Expand Up @@ -632,6 +634,8 @@ storm:

Customization follows the usual back-off rules: contribute an `ObservationConvention<StormQueryObservationContext>` bean to override the naming and key values (it wins over the property), define your own `QueryObserver` bean to replace the binding entirely, or disable the observation at the registry level with `management.observations.enable.storm.query=false`.

With tracing in place, `storm.tracing.sql-comments: true` additionally appends the current trace context to every statement as a sqlcommenter-style comment (`/*traceparent='00-…'*/`), so database-side diagnostics — MariaDB's slow query log, `pg_stat_activity` — correlate directly back to the trace that issued the statement. This is deliberately opt-in: a per-execution comment changes the statement text on every call, which defeats driver-side and server-side prepared statement caching. Enable it where the correlation is worth that trade-off.

## Testing with @DataStormTest

`@DataStormTest` (from `storm-spring-boot-test-autoconfigure`, test scope) is the Storm test slice, the counterpart of `@DataJpaTest`: it starts only the `DataSource`, Storm's auto-configuration (template, repository scanning, transaction integration, schema validation, exception translation), and SQL initialization, plus Flyway or Liquibase when present. It complements [`@StormTest`](testing.md), which tests data logic without a Spring context: use `@StormTest` for fast query-level tests, and the slice when the test should see what production Spring code sees — injected repository beans, translated exceptions, Spring-managed transactions, and your `storm.*` configuration. Regular `@Component`, `@Service`, and `@Controller` beans stay out of the context. Each test method runs in a transaction that is rolled back afterwards, so tests cannot see each other's writes.
Expand Down
48 changes: 48 additions & 0 deletions storm-core/src/main/java/st/orm/core/spi/SqlCommenter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 - 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package st.orm.core.spi;

import java.util.Optional;

/**
* Contributes a comment appended to SQL statements at execution time.
*
* <p>The comment is appended after all statement processing and caching, immediately before the statement is
* prepared, so per-execution content such as the current trace context reaches the database without affecting
* Storm's template cache. Database-side diagnostics — slow query logs, statement views — then carry the
* comment, correlating captured statements back to the execution that issued them.</p>
*
* <p>Commenters are configured per ORM template via the template builder; they are deliberately not discovered
* through the {@code ServiceLoader} mechanism. Note that a per-execution comment changes the statement text on
* every call, which defeats driver-side and server-side prepared statement caching; enable selectively.</p>
*
* @see st.orm.core.template.ORMTemplate.Builder#sqlCommenter(SqlCommenter)
* @since 1.13
*/
@FunctionalInterface
public interface SqlCommenter {

/**
* Returns the comment content for the statement that is about to execute, without comment delimiters,
* or empty when no comment applies. The content must not contain the comment terminator sequence
* (asterisk followed by slash) or semicolons; following the sqlcommenter convention, values are
* URL-encoded, which escapes both. The framework pads the emitted comment with spaces, so leading
* executable-comment and optimizer-hint markers are never interpreted.
*
* @return the comment content, such as {@code traceparent='00-4bf92f35-00f067aa-01'}.
*/
Optional<String> comment();
}
19 changes: 17 additions & 2 deletions storm-core/src/main/java/st/orm/core/template/ORMTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import st.orm.core.spi.ConnectionProvider;
import st.orm.core.spi.ExceptionMapper;
import st.orm.core.spi.QueryObserver;
import st.orm.core.spi.SqlCommenter;
import st.orm.core.spi.TransactionTemplateProvider;
import st.orm.core.template.impl.PreparedStatementTemplateImpl;
import st.orm.mapping.TemplateDecorator;
Expand Down Expand Up @@ -433,6 +434,7 @@ final class Builder {
private @Nullable TransactionTemplateProvider transactionTemplateProvider;
private @Nullable ExceptionMapper exceptionMapper;
private @Nullable QueryObserver queryObserver;
private @Nullable SqlCommenter sqlCommenter;

private Builder(@Nullable DataSource dataSource, @Nullable Connection connection) {
this.dataSource = dataSource;
Expand Down Expand Up @@ -510,6 +512,19 @@ public Builder queryObserver(@Nonnull QueryObserver queryObserver) {
return this;
}

/**
* Sets the SQL commenter that appends per-execution comment content to statements, such as the
* current trace context. Note that per-execution content defeats prepared statement caching.
*
* @param sqlCommenter the SQL commenter; must not be {@code null}.
* @return this builder.
* @since 1.13
*/
public Builder sqlCommenter(@Nonnull SqlCommenter sqlCommenter) {
this.sqlCommenter = requireNonNull(sqlCommenter, "sqlCommenter");
return this;
}

/**
* Builds the ORM template.
*
Expand All @@ -521,15 +536,15 @@ public ORMTemplate build() {
PreparedStatementTemplateImpl template;
if (dataSource != null) {
template = new PreparedStatementTemplateImpl(dataSource, config, connectionProvider,
transactionTemplateProvider, exceptionMapper, queryObserver);
transactionTemplateProvider, exceptionMapper, queryObserver, sqlCommenter);
} else {
if (connectionProvider != null) {
throw new PersistenceException(
"A connection provider cannot be configured for a connection backed template.");
}
assert connection != null;
template = new PreparedStatementTemplateImpl(connection, config,
transactionTemplateProvider, exceptionMapper, queryObserver);
transactionTemplateProvider, exceptionMapper, queryObserver, sqlCommenter);
}
if (decorator != null) {
var decorated = decorator.apply(template);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import st.orm.core.spi.QueryObserver;
import st.orm.core.spi.RefFactory;
import st.orm.core.spi.RefFactoryImpl;
import st.orm.core.spi.SqlCommenter;
import st.orm.core.spi.SqlDialectProvider;
import st.orm.core.spi.TransactionContext;
import st.orm.core.spi.TransactionScope;
Expand Down Expand Up @@ -98,7 +99,36 @@ PreparedStatement process(@Nonnull Sql sql,
record IntegrationStrategies(@Nullable ConnectionProvider connectionProvider,
@Nonnull TransactionTemplateProvider transactionTemplateProvider,
@Nonnull ExceptionMapper exceptionMapper,
@Nonnull QueryObserver queryObserver) {
@Nonnull QueryObserver queryObserver,
@Nullable SqlCommenter sqlCommenter) {
}

/**
* Appends the commenter's content to the statement, after all statement processing and caching. A
* commenter must never be able to alter the statement: content containing the comment terminator is
* rejected (the only escape from a block comment), and so are semicolons (inert to the SQL parser inside
* a comment, but naive statement splitters in drivers and proxies split on them; sqlcommenter values
* URL-encode them instead). The content is padded with spaces, which keeps MySQL and MariaDB from
* interpreting leading {@code !} or {@code +} as an executable comment or optimizer hint.
*/
private static String applySqlCommenter(@Nullable SqlCommenter sqlCommenter, @Nonnull String statement) {
if (sqlCommenter == null) {
return statement;
}
var content = sqlCommenter.comment().orElse(null);
if (content == null || content.isBlank()) {
return statement;
}
if (content.contains("*/")) {
throw new PersistenceException(
"SQL comment content must not contain the comment terminator '*/': %s".formatted(content));
}
if (content.indexOf(';') >= 0) {
throw new PersistenceException(
"SQL comment content must not contain semicolons; URL-encode values instead: %s"
.formatted(content));
}
return statement + " /* " + content + " */";
}

private final TemplateProcessor templateProcessor;
Expand Down Expand Up @@ -133,11 +163,29 @@ public PreparedStatementTemplateImpl(@Nonnull DataSource dataSource,
@Nullable TransactionTemplateProvider transactionTemplateProvider,
@Nullable ExceptionMapper exceptionMapper,
@Nullable QueryObserver queryObserver) {
this(dataSource, config, connectionProvider, transactionTemplateProvider, exceptionMapper, queryObserver,
null);
}

/**
* Creates a data source backed template with instance-scoped integration strategies and an optional
* SQL commenter.
*
* @since 1.13
*/
public PreparedStatementTemplateImpl(@Nonnull DataSource dataSource,
@Nonnull StormConfig config,
@Nullable ConnectionProvider connectionProvider,
@Nullable TransactionTemplateProvider transactionTemplateProvider,
@Nullable ExceptionMapper exceptionMapper,
@Nullable QueryObserver queryObserver,
@Nullable SqlCommenter sqlCommenter) {
this(new IntegrationStrategies(
requireNonNullElseGet(connectionProvider, Providers::getConnectionProvider),
requireNonNullElseGet(transactionTemplateProvider, Providers::getTransactionTemplateProvider),
requireNonNullElseGet(exceptionMapper, ExceptionMapper::defaultMapper),
requireNonNullElseGet(queryObserver, QueryObserver::noop)),
requireNonNullElseGet(queryObserver, QueryObserver::noop),
sqlCommenter),
dataSource, config);
}

Expand Down Expand Up @@ -182,11 +230,27 @@ public PreparedStatementTemplateImpl(@Nonnull Connection connection,
@Nullable TransactionTemplateProvider transactionTemplateProvider,
@Nullable ExceptionMapper exceptionMapper,
@Nullable QueryObserver queryObserver) {
this(connection, config, transactionTemplateProvider, exceptionMapper, queryObserver, null);
}

/**
* Creates a connection backed template with instance-scoped integration strategies and an optional
* SQL commenter.
*
* @since 1.13
*/
public PreparedStatementTemplateImpl(@Nonnull Connection connection,
@Nonnull StormConfig config,
@Nullable TransactionTemplateProvider transactionTemplateProvider,
@Nullable ExceptionMapper exceptionMapper,
@Nullable QueryObserver queryObserver,
@Nullable SqlCommenter sqlCommenter) {
this(new IntegrationStrategies(
null,
requireNonNullElseGet(transactionTemplateProvider, Providers::getTransactionTemplateProvider),
requireNonNullElseGet(exceptionMapper, ExceptionMapper::defaultMapper),
requireNonNullElseGet(queryObserver, QueryObserver::noop)),
requireNonNullElseGet(queryObserver, QueryObserver::noop),
sqlCommenter),
connection, config);
}

Expand Down Expand Up @@ -240,7 +304,7 @@ private static TemplateProcessor createDataSourceProcessor(@Nonnull DataSource d
throw new PersistenceException("%s Use Query.unsafe() to allow this operation.".formatted(warning));
});
}
var statement = sql.statement();
var statement = applySqlCommenter(strategies.sqlCommenter(), sql.statement());
var parameters = sql.parameters();
var bindVariables = sql.bindVariables().orElse(null);
var generatedKeys = sql.generatedKeys();
Expand Down Expand Up @@ -297,7 +361,7 @@ private static TemplateProcessor createConnectionProcessor(@Nonnull Connection c
throw new PersistenceException("%s Use Query.unsafe() to allow this operation.".formatted(warning));
});
}
var statement = sql.statement();
var statement = applySqlCommenter(strategies.sqlCommenter(), sql.statement());
var parameters = sql.parameters();
var bindVariables = sql.bindVariables().orElse(null);
var generatedKeys = sql.generatedKeys();
Expand Down
13 changes: 13 additions & 0 deletions storm-java21/src/main/java/st/orm/template/ORMTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,19 @@ public Builder queryObserver(@Nonnull st.orm.core.spi.QueryObserver queryObserve
return this;
}

/**
* Sets the SQL commenter that appends per-execution comment content to statements, such as the
* current trace context. Note that per-execution content defeats prepared statement caching.
*
* @param sqlCommenter the SQL commenter; must not be {@code null}.
* @return this builder.
* @since 1.13
*/
public Builder sqlCommenter(@Nonnull st.orm.core.spi.SqlCommenter sqlCommenter) {
core.sqlCommenter(sqlCommenter);
return this;
}

/**
* Builds the ORM template.
*
Expand Down
Loading
Loading