diff --git a/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java b/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java
index ea105f4ad..53f3546a3 100644
--- a/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java
+++ b/storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java
@@ -30,6 +30,7 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -1717,16 +1718,7 @@ protected void insert(List batch, PreparedQuery query, boolean ignoreAutoGene
if (batch.isEmpty()) {
return;
}
- List transformed = batch.stream()
- .map(this::fireBeforeInsert)
- .toList();
- transformed.stream()
- .map(e -> validateInsert(e, ignoreAutoGenerate))
- .forEach(query::addBatch);
- int[] result = query.executeBatch();
- if (IntStream.of(result).anyMatch(r -> r != 1)) {
- throw new PersistenceException("Batch insert of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
- }
+ List transformed = executeInsertBatch(batch, query, ignoreAutoGenerate);
transformed.forEach(this::fireAfterInsert);
}
@@ -1739,16 +1731,7 @@ private List insertAndFetchIds(List batch, PreparedQuery query, boolean i
if (batch.isEmpty()) {
return List.of();
}
- List transformed = batch.stream()
- .map(this::fireBeforeInsert)
- .toList();
- transformed.stream()
- .map(e -> validateInsert(e, ignoreAutoGenerate))
- .forEach(query::addBatch);
- int[] result = query.executeBatch();
- if (IntStream.of(result).anyMatch(r -> r != 1)) {
- throw new PersistenceException("Batch insert of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
- }
+ List transformed = executeInsertBatch(batch, query, ignoreAutoGenerate);
List ids;
if (isAutoGeneratedPrimaryKey() && !ignoreAutoGenerate) {
try (var stream = query.getGeneratedKeys(model.primaryKeyType())) {
@@ -1761,6 +1744,26 @@ private List insertAndFetchIds(List batch, PreparedQuery query, boolean i
return ids;
}
+ /**
+ * Runs the batch through the prepared insert: fires the before-insert callbacks, validates, executes the
+ * batch and checks that every row was inserted.
+ *
+ * @return the entities as transformed by the before-insert callbacks.
+ */
+ private List executeInsertBatch(List batch, PreparedQuery query, boolean ignoreAutoGenerate) {
+ List transformed = batch.stream()
+ .map(this::fireBeforeInsert)
+ .toList();
+ transformed.stream()
+ .map(e -> validateInsert(e, ignoreAutoGenerate))
+ .forEach(query::addBatch);
+ int[] result = query.executeBatch();
+ if (IntStream.of(result).anyMatch(r -> r != 1)) {
+ throw new PersistenceException("Batch insert of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
+ }
+ return transformed;
+ }
+
/**
* Updates a stream of entities in the database using the default batch size.
*
@@ -1984,22 +1987,7 @@ protected PreparedQuery prepareUpdateQuery(Set> fields) {
}
protected void update(List batch, PreparedQuery query, @Nullable EntityCache cache) {
- if (batch.isEmpty()) {
- return;
- }
- batch.stream().map(this::validateUpdate).forEach(query::addBatch);
- if (cache != null) {
- batch.stream()
- .filter(e -> !model.isDefaultPrimaryKey(e.id()))
- .forEach(e -> cache.remove(e.id()));
- }
- int[] result = query.executeBatch();
- if (query.isVersionAware() && IntStream.of(result).anyMatch(r -> r == 0)) {
- throw new OptimisticLockException("Batch update of %s failed due to optimistic lock. One or more entities may have been modified or deleted by another transaction.".formatted(model.type().getSimpleName()));
- } else if (IntStream.of(result).anyMatch(r -> r != 1)) {
- throw new PersistenceException("Batch update of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
- }
- batch.forEach(this::fireAfterUpdate);
+ updateAndFetchIds(batch, query, cache);
}
protected List updateAndFetchIds(List batch, PreparedQuery query, @Nullable EntityCache cache) {
@@ -2168,6 +2156,124 @@ protected List doUpsertAndFetchIdsBatch(List batch, PreparedQuery query,
throw upsertNotAvailable();
}
+ // Partition keys for the dialects' id-returning upsertAndFetchIds implementations.
+ public sealed interface SeqPartitionKey {}
+ public static final class SeqNoOpKey implements SeqPartitionKey {
+ public static final SeqNoOpKey INSTANCE = new SeqNoOpKey();
+ private SeqNoOpKey() {
+ }
+ }
+ public static final class SeqUpsertKey implements SeqPartitionKey {
+ public static final SeqUpsertKey INSTANCE = new SeqUpsertKey();
+ private SeqUpsertKey() {
+ }
+ }
+ public record SeqUpdateKey(Set> fields) implements SeqPartitionKey {
+ public SeqUpdateKey() {
+ this(Set.of()); // All fields.
+ }
+ }
+
+ /**
+ * The upsert-and-fetch-ids loop for dialects that fetch the ids through a single id-returning query
+ * (such as {@code INSERT ... RETURNING} or {@code MERGE ... OUTPUT}) rather than batched prepared
+ * statements: the entities are partitioned into no-op, upsert and per-shape update batches, and the
+ * resulting ids come back in entity order.
+ *
+ * @param entities the entities to upsert.
+ * @param upsertPartition executes one upsert partition and returns its ids; receives the partition's
+ * chunk and the entity cache in effect for the operation. Dialects with the
+ * standard cache-eviction semantics pass {@link #upsertPartitionAndFetchIds}.
+ * @return the ids of the upserted entities.
+ * @since 1.14
+ */
+ protected List upsertAndFetchIdsPartitioned(
+ Iterable entities,
+ BiFunction, Optional>, List> upsertPartition) {
+ var updateQueries = new HashMap>, PreparedQuery>();
+ try {
+ var result = new ArrayList();
+ var entityCache = entityCache();
+ partitioned(toStream(entities), defaultBatchSize, entity -> {
+ if (isUpsertUpdate(entity)) {
+ var dirty = getDirty(entity, entityCache.orElse(null));
+ if (dirty.isEmpty()) {
+ return SeqNoOpKey.INSTANCE;
+ }
+ return new SeqUpdateKey(dirty.get());
+ } else {
+ return SeqUpsertKey.INSTANCE;
+ }
+ }, getMaxShapes(), new SeqUpdateKey()).forEach(partition -> {
+ switch (partition.key()) {
+ case SeqNoOpKey ignore -> result.addAll(partition.chunk().stream().map(E::id).toList());
+ case SeqUpsertKey ignore -> result.addAll(upsertPartition.apply(partition.chunk(), entityCache));
+ case SeqUpdateKey u -> {
+ List batch = hasEntityCallbacks()
+ ? partition.chunk().stream().map(this::fireBeforeUpdate).toList()
+ : partition.chunk();
+ result.addAll(updateAndFetchIds(batch,
+ updateQueries.computeIfAbsent(u.fields(), this::prepareUpdateQuery),
+ entityCache.orElse(null)));
+ }
+ }
+ });
+ return result;
+ } finally {
+ closeQuietly(updateQueries.values().stream());
+ }
+ }
+
+ /**
+ * Executes one upsert partition through a single id-returning query: fires the before-upsert callbacks,
+ * evicts the entities with non-default primary keys from the cache (the upsert may update them), runs
+ * the query and reports the returned ids to the after-upsert callbacks.
+ *
+ * @param chunk the partition's entities.
+ * @param entityCache the entity cache in effect for the operation.
+ * @param upsertQuery creates the id-returning upsert query for a batch.
+ * @return the ids of the upserted entities.
+ * @since 1.14
+ */
+ protected List upsertPartitionAndFetchIds(List chunk,
+ Optional> entityCache,
+ Function, Query> upsertQuery) {
+ List batch = hasEntityCallbacks()
+ ? chunk.stream().map(this::fireBeforeUpsert).toList()
+ : chunk;
+ entityCache.ifPresent(cache -> batch.stream()
+ .filter(e -> !model.isDefaultPrimaryKey(e.id()))
+ .forEach(e -> cache.remove(e.id())));
+ List ids = upsertQuery.apply(batch).getResultList(model.primaryKeyType());
+ fireAfterUpsert(batch, ids);
+ return ids;
+ }
+
+ /**
+ * Inserts the entity and fetches the generated primary key through the dialect's
+ * {@code INSERT ... RETURNING} clause, for dialects whose driver cannot report sequence-generated
+ * keys through {@code getGeneratedKeys()}.
+ *
+ * @param entity the entity to insert.
+ * @return the primary key of the inserted entity.
+ * @since 1.14
+ */
+ protected ID insertAndFetchIdReturning(E entity) {
+ entity = fireBeforeInsert(entity);
+ validateInsert(entity);
+ assert primaryKeyColumns.size() == 1;
+ var primaryKeyColumn = primaryKeyColumns.getFirst();
+ String pkName = primaryKeyColumn.qualifiedName(ormTemplate.dialect());
+ try (var query = ormTemplate.query(TemplateString.raw("""
+ INSERT INTO \0
+ VALUES \0
+ RETURNING %s""".formatted(pkName), model.type(), entity)).managed().prepare()) {
+ ID id = query.getSingleResult(model.primaryKeyType());
+ fireAfterInsert(entity, id);
+ return id;
+ }
+ }
+
/**
* Removes a stream of entities from the database in batches.
*
diff --git a/storm-core/src/main/java/st/orm/core/repository/impl/MergeEntityRepositoryImpl.java b/storm-core/src/main/java/st/orm/core/repository/impl/MergeEntityRepositoryImpl.java
index f975cea6a..5934a45f4 100644
--- a/storm-core/src/main/java/st/orm/core/repository/impl/MergeEntityRepositoryImpl.java
+++ b/storm-core/src/main/java/st/orm/core/repository/impl/MergeEntityRepositoryImpl.java
@@ -278,17 +278,7 @@ private TemplateString mergeStatement(TemplateString mergeSelect, AtomicBoolean
*/
@Override
protected void doUpsert(E entity) {
- validateUpsert(entity);
- entityCache().ifPresent(cache -> {
- if (!model.isDefaultPrimaryKey(entity.id())) {
- cache.remove(entity.id());
- }
- });
- var versionAware = new AtomicBoolean();
- intercept(sql -> sql.versionAware(versionAware.getPlain()), () -> {
- var query = ormTemplate.query(mergeStatement(mergeSelect(entity), versionAware)).managed();
- query.executeUpdate();
- });
+ doUpsertAndFetchId(entity);
}
/**
@@ -322,19 +312,7 @@ protected PreparedQuery prepareUpsertQuery() {
@Override
protected void doUpsertBatch(List batch, PreparedQuery query,
@Nullable EntityCache cache) {
- if (batch.isEmpty()) {
- return;
- }
- batch.stream().map(this::validateUpsert).forEach(query::addBatch);
- if (cache != null) {
- batch.stream()
- .filter(e -> !model.isDefaultPrimaryKey(e.id()))
- .forEach(e -> cache.remove(e.id()));
- }
- int[] result = query.executeBatch();
- if (IntStream.of(result).anyMatch(r -> r != 0 && r != 1 && r != 2)) {
- throw new PersistenceException(upsertFailureMessage(batch.size()));
- }
+ doUpsertAndFetchIdsBatch(batch, query, cache);
}
@Override
diff --git a/storm-core/src/main/java/st/orm/core/repository/impl/OnConflictEntityRepositoryImpl.java b/storm-core/src/main/java/st/orm/core/repository/impl/OnConflictEntityRepositoryImpl.java
new file mode 100644
index 000000000..16c72fe53
--- /dev/null
+++ b/storm-core/src/main/java/st/orm/core/repository/impl/OnConflictEntityRepositoryImpl.java
@@ -0,0 +1,214 @@
+/*
+ * 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.repository.impl;
+
+import static java.util.function.Predicate.not;
+import static st.orm.core.template.SqlInterceptor.intercept;
+import static st.orm.core.template.TemplateString.combine;
+import static st.orm.core.template.TemplateString.raw;
+import static st.orm.core.template.Templates.table;
+import static st.orm.core.template.impl.StringTemplates.flatten;
+
+import java.math.BigInteger;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+import org.jspecify.annotations.Nullable;
+import st.orm.Data;
+import st.orm.Entity;
+import st.orm.NoResultException;
+import st.orm.NonUniqueResultException;
+import st.orm.PersistenceException;
+import st.orm.core.repository.EntityRepository;
+import st.orm.core.spi.EntityCache;
+import st.orm.core.template.Column;
+import st.orm.core.template.Model;
+import st.orm.core.template.ORMTemplate;
+import st.orm.core.template.PreparedQuery;
+import st.orm.core.template.TemplateString;
+
+/**
+ * Implementation of {@link EntityRepository} for dialects that upsert through the
+ * {@code INSERT ... ON CONFLICT () DO UPDATE SET ...} clause with the {@code EXCLUDED}
+ * pseudo-table, such as PostgreSQL and SQLite.
+ *
+ * @since 1.14
+ */
+public class OnConflictEntityRepositoryImpl, ID>
+ extends EntityRepositoryImpl {
+
+ protected OnConflictEntityRepositoryImpl(ORMTemplate ormTemplate, Model model) {
+ super(ormTemplate, model);
+ }
+
+ private TemplateString getVersionString(Class extends Data> type, Column column) {
+ TemplateString columnName = TemplateString.of(column.qualifiedName(ormTemplate.dialect()));
+ TemplateString updateExpression = switch (column.type()) {
+ case Class> c when Integer.TYPE.isAssignableFrom(c)
+ || Long.TYPE.isAssignableFrom(c)
+ || Integer.class.isAssignableFrom(c)
+ || Long.class.isAssignableFrom(c)
+ || BigInteger.class.isAssignableFrom(c) -> raw("\0.\0 + 1", table(type), columnName);
+ case Class> c when Instant.class.isAssignableFrom(c)
+ || Date.class.isAssignableFrom(c)
+ || Calendar.class.isAssignableFrom(c)
+ || Timestamp.class.isAssignableFrom(c) -> TemplateString.of(ormTemplate.dialect().currentTimestamp());
+ default ->
+ throw new PersistenceException("Unsupported version type: %s.".formatted(column.type().getSimpleName()));
+ };
+ return flatten(raw("\0 = \0", columnName, updateExpression));
+ }
+
+ /**
+ * Constructs the conflict clause for an upsert.
+ *
+ *
This method builds an "ON CONFLICT (<primary_keys>) DO UPDATE SET ..." clause.
+ * For non-primary key columns, it assigns the value from the EXCLUDED pseudo-table.
+ * Version columns are updated using {@link #getVersionString(Class, Column)}.
+ *
+ * @param versionAware a flag that will be set if a version column is encountered.
+ * @return the conflict clause as a TemplateString.
+ */
+ protected TemplateString onConflictClause(AtomicBoolean versionAware) {
+ var dialect = ormTemplate.dialect();
+ // Determine the conflict target from primary key columns.
+ String conflictTarget = model.declaredColumns().stream()
+ .filter(Column::primaryKey)
+ .map(c -> c.qualifiedName(dialect))
+ .reduce("%s, %s"::formatted)
+ .orElseThrow(() -> new PersistenceException("No primary key defined."));
+ // Build the assignment list for non-primary key updatable columns.
+ var assignments = model.declaredColumns().stream()
+ .filter(not(Column::primaryKey))
+ .filter(Column::updatable)
+ .map(column -> {
+ if (column.version()) {
+ versionAware.setPlain(true);
+ return getVersionString(model.type(), column);
+ }
+ return TemplateString.of("%s = EXCLUDED.%s".formatted(column.qualifiedName(dialect), column.qualifiedName(dialect)));
+ })
+ .reduce((left, right) -> combine(left, TemplateString.of(", "), right))
+ .map(st -> combine(TemplateString.of("DO UPDATE SET "), st))
+ .orElse(TemplateString.of("DO NOTHING"));
+ return flatten(combine(TemplateString.of("\nON CONFLICT ("), TemplateString.of(conflictTarget), raw(") \0", assignments)));
+ }
+
+ @Override
+ protected void doUpsert(E entity) {
+ validateUpsert(entity);
+ entityCache().ifPresent(cache -> {
+ if (!model.isDefaultPrimaryKey(entity.id())) {
+ cache.remove(entity.id());
+ }
+ });
+ var versionAware = new AtomicBoolean();
+ intercept(sql -> sql.versionAware(versionAware.getPlain()), () -> {
+ var query = ormTemplate.query(flatten(raw("""
+ INSERT INTO \0
+ VALUES \0\0""", model.type(), entity, onConflictClause(versionAware)))).managed();
+ query.executeUpdate();
+ });
+ }
+
+ /**
+ * Performs the upsert and reads the primary key back through the driver's generated keys. Dialects with
+ * an id-returning path for sequence-generated keys override this method and fall back to this
+ * implementation for the other generation strategies.
+ */
+ @Override
+ protected ID doUpsertAndFetchId(E entity) {
+ validateUpsert(entity);
+ entityCache().ifPresent(cache -> {
+ if (!model.isDefaultPrimaryKey(entity.id())) {
+ cache.remove(entity.id());
+ }
+ });
+ var versionAware = new AtomicBoolean();
+ return intercept(sql -> sql.versionAware(versionAware.getPlain()), () -> {
+ try (var query = ormTemplate.query(flatten(raw("""
+ INSERT INTO \0
+ VALUES \0\0""", model.type(), entity, onConflictClause(versionAware)))).managed().prepare()) {
+ query.executeUpdate();
+ if (isAutoGeneratedPrimaryKey()) {
+ try (var stream = query.getGeneratedKeys(model.primaryKeyType())) {
+ return stream.reduce((ignore1, ignore2) -> {
+ throw new NonUniqueResultException("Expected single result, but found more than one.");
+ }).orElseThrow(() -> new NoResultException("Expected single result, but found none."));
+ }
+ }
+ return entity.id();
+ }
+ });
+ }
+
+ @Override
+ protected PreparedQuery prepareUpsertQuery() {
+ var bindVars = ormTemplate.createBindVars();
+ var versionAware = new AtomicBoolean();
+ return intercept(sql -> sql.versionAware(versionAware.getPlain()), () ->
+ ormTemplate.query(flatten(raw("""
+ INSERT INTO \0
+ VALUES \0\0""", model.type(), bindVars, onConflictClause(versionAware))))
+ .managed().prepare());
+ }
+
+ @Override
+ protected void doUpsertBatch(List batch, PreparedQuery query,
+ @Nullable EntityCache cache) {
+ if (batch.isEmpty()) {
+ return;
+ }
+ batch.stream().map(this::validateUpsert).forEach(query::addBatch);
+ if (cache != null) {
+ batch.stream()
+ .filter(e -> !model.isDefaultPrimaryKey(e.id()))
+ .forEach(e -> cache.remove(e.id()));
+ }
+ int[] result = query.executeBatch();
+ if (IntStream.of(result).anyMatch(r -> r != 0 && r != 1 && r != 2)) {
+ throw new PersistenceException(upsertFailureMessage(batch.size()));
+ }
+ }
+
+ @Override
+ protected List doUpsertAndFetchIdsBatch(List batch, PreparedQuery query,
+ @Nullable EntityCache cache) {
+ if (batch.isEmpty()) {
+ return List.of();
+ }
+ batch.stream().map(this::validateUpsert).forEach(query::addBatch);
+ if (cache != null) {
+ batch.stream()
+ .filter(e -> !model.isDefaultPrimaryKey(e.id()))
+ .forEach(e -> cache.remove(e.id()));
+ }
+ int[] result = query.executeBatch();
+ if (IntStream.of(result).anyMatch(r -> r != 0 && r != 1 && r != 2)) {
+ throw new PersistenceException(upsertFailureMessage(batch.size()));
+ }
+ if (isAutoGeneratedPrimaryKey()) {
+ try (var generatedKeys = query.getGeneratedKeys(model.primaryKeyType())) {
+ return generatedKeys.toList();
+ }
+ }
+ return batch.stream().map(Entity::id).toList();
+ }
+}
diff --git a/storm-core/src/main/java/st/orm/core/spi/AbstractRef.java b/storm-core/src/main/java/st/orm/core/spi/AbstractRef.java
index 5174b0e96..8fd2e93ee 100644
--- a/storm-core/src/main/java/st/orm/core/spi/AbstractRef.java
+++ b/storm-core/src/main/java/st/orm/core/spi/AbstractRef.java
@@ -18,6 +18,7 @@
import java.util.Objects;
import st.orm.Data;
import st.orm.Ref;
+import st.orm.core.template.impl.LazySupplier;
/**
* Abstract implementation of {@link Ref} to have consistent implementations of {@link #hashCode()}
@@ -30,9 +31,14 @@
* as-is.
*
* @param record type.
+ * @param primary key type.
* @since 1.3
*/
-abstract class AbstractRef implements Ref {
+abstract class AbstractRef extends BaseRef {
+
+ AbstractRef(LazySupplier supplier, Class type, ID pk) {
+ super(supplier, type, pk);
+ }
/**
* Lazily computed row identity of the id. Computed outside construction because only map-keyed usage needs the
@@ -72,7 +78,7 @@ public boolean equals(Object obj) {
if (this == obj) {
return true;
}
- if (obj instanceof AbstractRef> other) {
+ if (obj instanceof AbstractRef, ?> other) {
return Objects.equals(type(), other.type())
&& Objects.equals(rowId(), other.rowId());
}
diff --git a/storm-core/src/main/java/st/orm/core/spi/BaseRef.java b/storm-core/src/main/java/st/orm/core/spi/BaseRef.java
new file mode 100644
index 000000000..3da7b75a8
--- /dev/null
+++ b/storm-core/src/main/java/st/orm/core/spi/BaseRef.java
@@ -0,0 +1,124 @@
+/*
+ * 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 static java.util.Objects.requireNonNull;
+
+import org.jspecify.annotations.Nullable;
+import st.orm.Data;
+import st.orm.Entity;
+import st.orm.Ref;
+import st.orm.core.repository.EntityRepository;
+import st.orm.core.template.impl.LazySupplier;
+
+/**
+ * Base implementation for the attached {@link Ref} implementations: holds the record type, the primary key and
+ * the lazy supplier the record is fetched through. Identity semantics are left to the subclasses, which differ
+ * in whether they cache a normalized row identity.
+ *
+ * @param record type.
+ * @param primary key type.
+ * @since 1.14
+ */
+abstract class BaseRef implements Ref {
+ private final LazySupplier supplier;
+ private final Class type;
+ private final ID pk;
+
+ BaseRef(LazySupplier supplier, Class type, ID pk) {
+ this.supplier = requireNonNull(supplier, "supplier");
+ this.type = requireNonNull(type, "type");
+ this.pk = requireNonNull(pk, "pk");
+ }
+
+ /**
+ * The type of the record.
+ *
+ * @return the type of the record.
+ */
+ @Override
+ public Class type() {
+ return type;
+ }
+
+ /**
+ * Returns the record if it has already been fetched, without triggering a database call.
+ *
+ * @return the record if already loaded, or {@code null} if not yet fetched.
+ * @since 1.7
+ */
+ @Nullable
+ @Override
+ public T getOrNull() {
+ return supplier.value().orElse(null);
+ }
+
+ /**
+ * Returns the primary key of the record.
+ *
+ *
This method is provided for convenience. If the type of the id is known, you can cast it to the appropriate
+ * type.
+ *
+ * @return the primary key as an Object.
+ */
+ @Override
+ public ID id() {
+ return pk;
+ }
+
+ /**
+ * Fetches the record from the database if the record has not been fetched yet. The record will be fetched at most
+ * once.
+ *
+ * @return the fetched record.
+ * @since 1.7
+ */
+ @Override
+ public T fetchOrNull() {
+ return supplier.get();
+ }
+
+ /**
+ * Returns whether this ref is attached to a database context and capable of fetching the record on demand.
+ *
+ *
A fetchable ref has access to a database connection and can attempt to retrieve the record when
+ * {@link #fetch()} or {@link #fetchOrNull()} is called. A non-fetchable (detached) ref can only return
+ * data that was already loaded at the time of its creation.
+ *
+ *
Note that this method indicates the capability to fetch, not a guarantee of success. A fetchable
+ * ref may still fail to retrieve a record if it has been deleted from the database or if the connection
+ * encounters an error.
+ *
+ * @return {@code true} if this ref can attempt to fetch from the database, {@code false} if it is detached.
+ * @since 1.7
+ */
+ @Override
+ public boolean isFetchable() {
+ return true;
+ }
+
+ /**
+ * Returns a detached ref with the same identity but without data. The returned ref is not attached to a database
+ * context. To obtain an attached ref that can re-fetch the record, use
+ * {@link EntityRepository#unload(Entity) EntityRepository.unload()} instead.
+ *
+ * @return a detached ref with the same type and primary key but without cached data.
+ */
+ @Override
+ public Ref unload() {
+ return Ref.of(type, pk);
+ }
+}
diff --git a/storm-core/src/main/java/st/orm/core/spi/Providers.java b/storm-core/src/main/java/st/orm/core/spi/Providers.java
index e6314037f..315f89f0a 100644
--- a/storm-core/src/main/java/st/orm/core/spi/Providers.java
+++ b/storm-core/src/main/java/st/orm/core/spi/Providers.java
@@ -311,7 +311,7 @@ public static String getDatabaseProductName(DataSource dataSource) {
}
return DATABASE_PRODUCT_NAMES.computeIfAbsent(new DataSourceIdentity(dataSource, DATA_SOURCE_QUEUE), ignore -> {
try (Connection connection = dataSource.getConnection()) {
- return connection.getMetaData().getDatabaseProductName();
+ return getDatabaseProductName(connection);
} catch (SQLException e) {
throw new PersistenceException("Failed to determine database product name.", e);
}
@@ -358,12 +358,7 @@ public static String getDatabaseProductName(Connection connection) {
* @since 1.11
*/
public static SqlDialect getSqlDialect(DataSource dataSource, StormConfig config) {
- String productName = getDatabaseProductName(dataSource);
- return enabled(SQL_DIALECT_PROVIDERS)
- .filter(p -> p.supports(productName))
- .map(p -> p.getSqlDialect(config))
- .findFirst()
- .orElseThrow();
+ return sqlDialectFor(getDatabaseProductName(dataSource), config);
}
/**
@@ -376,7 +371,10 @@ public static SqlDialect getSqlDialect(DataSource dataSource, StormConfig config
* @since 1.11
*/
public static SqlDialect getSqlDialect(Connection connection, StormConfig config) {
- String productName = getDatabaseProductName(connection);
+ return sqlDialectFor(getDatabaseProductName(connection), config);
+ }
+
+ private static SqlDialect sqlDialectFor(String productName, StormConfig config) {
return enabled(SQL_DIALECT_PROVIDERS)
.filter(p -> p.supports(productName))
.map(p -> p.getSqlDialect(config))
diff --git a/storm-core/src/main/java/st/orm/core/spi/RefImpl.java b/storm-core/src/main/java/st/orm/core/spi/RefImpl.java
index ec520a1bf..2bdb65894 100644
--- a/storm-core/src/main/java/st/orm/core/spi/RefImpl.java
+++ b/storm-core/src/main/java/st/orm/core/spi/RefImpl.java
@@ -15,13 +15,8 @@
*/
package st.orm.core.spi;
-import static java.util.Objects.requireNonNull;
-
-import org.jspecify.annotations.Nullable;
import st.orm.Data;
-import st.orm.Entity;
import st.orm.Ref;
-import st.orm.core.repository.EntityRepository;
import st.orm.core.template.impl.LazySupplier;
/**
@@ -30,92 +25,9 @@
* @param record type.
* @param primary key type.
*/
-final class RefImpl extends AbstractRef {
- private final LazySupplier supplier;
- private final Class type;
- private final ID pk;
+final class RefImpl extends AbstractRef {
RefImpl(LazySupplier supplier, Class type, ID pk) {
- this.supplier = requireNonNull(supplier, "supplier");
- this.type = requireNonNull(type, "type");
- this.pk = requireNonNull(pk, "pk");
- }
-
- /**
- * The type of the record.
- *
- * @return the type of the record.
- */
- @Override
- public Class type() {
- return type;
- }
-
- /**
- * Returns the record if it has already been fetched, without triggering a database call.
- *
- * @return the record if already loaded, or {@code null} if not yet fetched.
- * @since 1.7
- */
- @Nullable
- @Override
- public T getOrNull() {
- return supplier.value().orElse(null);
- }
-
- /**
- * Returns the primary key of the record.
- *
- *
This method is provided for convenience. If the type of the id is known, you can cast it to the appropriate
- * type.
- *
- * @return the primary key as an Object.
- */
- @Override
- public ID id() {
- return pk;
- }
-
- /**
- * Fetches the record from the database if the record has not been fetched yet. The record will be fetched at most
- * once.
- *
- * @return the fetched record.
- * @since 1.7
- */
- @Override
- public T fetchOrNull() {
- return supplier.get();
- }
-
- /**
- * Returns whether this ref is attached to a database context and capable of fetching the record on demand.
- *
- *
A fetchable ref has access to a database connection and can attempt to retrieve the record when
- * {@link #fetch()} or {@link #fetchOrNull()} is called. A non-fetchable (detached) ref can only return
- * data that was already loaded at the time of its creation.
- *
- *
Note that this method indicates the capability to fetch, not a guarantee of success. A fetchable
- * ref may still fail to retrieve a record if it has been deleted from the database or if the connection
- * encounters an error.
- *
- * @return {@code true} if this ref can attempt to fetch from the database, {@code false} if it is detached.
- * @since 1.7
- */
- @Override
- public boolean isFetchable() {
- return true;
- }
-
- /**
- * Returns a detached ref with the same identity but without data. The returned ref is not attached to a database
- * context. To obtain an attached ref that can re-fetch the record, use
- * {@link EntityRepository#unload(Entity) EntityRepository.unload()} instead.
- *
- * @return a detached ref with the same type and primary key but without cached data.
- */
- @Override
- public Ref unload() {
- return Ref.of(type, pk);
+ super(supplier, type, pk);
}
}
diff --git a/storm-core/src/main/java/st/orm/core/spi/ScalarRefImpl.java b/storm-core/src/main/java/st/orm/core/spi/ScalarRefImpl.java
index bd8fd7d33..fc9c3836f 100644
--- a/storm-core/src/main/java/st/orm/core/spi/ScalarRefImpl.java
+++ b/storm-core/src/main/java/st/orm/core/spi/ScalarRefImpl.java
@@ -15,14 +15,9 @@
*/
package st.orm.core.spi;
-import static java.util.Objects.requireNonNull;
-
import java.util.Objects;
-import org.jspecify.annotations.Nullable;
import st.orm.Data;
-import st.orm.Entity;
import st.orm.Ref;
-import st.orm.core.repository.EntityRepository;
import st.orm.core.template.impl.LazySupplier;
/**
@@ -38,84 +33,15 @@
* @param record type.
* @param primary key type.
*/
-final class ScalarRefImpl implements Ref {
- private final LazySupplier supplier;
- private final Class type;
- private final ID pk;
+final class ScalarRefImpl extends BaseRef {
ScalarRefImpl(LazySupplier supplier, Class type, ID pk) {
- this.supplier = requireNonNull(supplier, "supplier");
- this.type = requireNonNull(type, "type");
- this.pk = requireNonNull(pk, "pk");
- }
-
- /**
- * The type of the record.
- *
- * @return the type of the record.
- */
- @Override
- public Class type() {
- return type;
- }
-
- /**
- * Returns the record if it has already been fetched, without triggering a database call.
- *
- * @return the record if already loaded, or {@code null} if not yet fetched.
- */
- @Nullable
- @Override
- public T getOrNull() {
- return supplier.value().orElse(null);
- }
-
- /**
- * Returns the primary key of the record.
- *
- * @return the primary key as an Object.
- */
- @Override
- public ID id() {
- return pk;
- }
-
- /**
- * Fetches the record from the database if the record has not been fetched yet. The record will be fetched at
- * most once.
- *
- * @return the fetched record.
- */
- @Override
- public T fetchOrNull() {
- return supplier.get();
- }
-
- /**
- * Returns whether this ref is attached to a database context and capable of fetching the record on demand.
- *
- * @return {@code true}, this implementation is created attached to a database context.
- */
- @Override
- public boolean isFetchable() {
- return true;
- }
-
- /**
- * Returns a detached ref with the same identity but without data. The returned ref is not attached to a
- * database context. To obtain an attached ref that can re-fetch the record, use
- * {@link EntityRepository#unload(Entity) EntityRepository.unload()} instead.
- *
- * @return a detached ref with the same type and primary key but without cached data.
- */
- @Override
- public Ref unload() {
- return Ref.of(type, pk);
+ super(supplier, type, pk);
}
@Override
public int hashCode() {
- return RowIdentity.hash(type, pk);
+ return RowIdentity.hash(type(), id());
}
@Override
@@ -126,19 +52,19 @@ public boolean equals(Object obj) {
if (obj instanceof ScalarRefImpl, ?> other) {
// Both sides hold their identity as the raw pk of a class that is its own row identity; the type gate guarantees the
// pk classes match.
- return Objects.equals(type, other.type) && pk.equals(other.pk);
+ return Objects.equals(type(), other.type()) && id().equals(other.id());
}
if (obj instanceof Ref> l) {
- return RowIdentity.refEquals(type, pk, l);
+ return RowIdentity.refEquals(type(), id(), l);
}
return false;
}
@Override
public String toString() {
- Class> type = this.type;
+ Class> type = type();
return type == Record.class
- ? "%s".formatted(pk)
- : "%s@%s".formatted(type.getSimpleName(), pk);
+ ? "%s".formatted(id())
+ : "%s@%s".formatted(type.getSimpleName(), id());
}
}
diff --git a/storm-core/src/main/java/st/orm/core/spi/TransactionScope.java b/storm-core/src/main/java/st/orm/core/spi/TransactionScope.java
index 947310183..50970468c 100644
--- a/storm-core/src/main/java/st/orm/core/spi/TransactionScope.java
+++ b/storm-core/src/main/java/st/orm/core/spi/TransactionScope.java
@@ -144,11 +144,7 @@ public static TransactionScope create(Options options, @Nullable TransactionScop
* path are not observed.
*/
public static @Nullable TransactionContext resolveContext(TransactionTemplateProvider provider) {
- var scope = CURRENT.get();
- if (scope != null) {
- return scope.getOrMaterializeContext(provider);
- }
- return provider.getTransactionTemplate().currentContext().orElse(null);
+ return resolveContext(provider, null);
}
/**
diff --git a/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java b/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java
index 21317de46..e38bb10c9 100644
--- a/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java
+++ b/storm-core/src/main/java/st/orm/core/template/QueryBuilder.java
@@ -352,15 +352,6 @@ public final QueryBuilder where(Navigable extends T
return where(path.asMetamodel(), EQUALS, record);
}
- @SuppressWarnings("unchecked")
- private static Metamodel, ?>[] asMetamodels(Navigable extends T, ?>[] paths) {
- Metamodel, ?>[] result = new Metamodel[paths.length];
- for (int i = 0; i < paths.length; i++) {
- result[i] = paths[i].asMetamodel();
- }
- return result;
- }
-
/**
* Adds a WHERE clause that matches the specified ref. The ref can represent any of the related tables in the
* table graph.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/DeleteBuilderImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/DeleteBuilderImpl.java
index 07368bd5c..bd34b81f9 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/DeleteBuilderImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/DeleteBuilderImpl.java
@@ -195,24 +195,7 @@ private TemplateString toTemplateString() {
} else {
template = TemplateString.combine(TemplateString.of("SELECT "), getPrimaryKeyTemplate(true), TemplateString.raw("\nFROM \0", from(fromType, true)));
}
- //noinspection DuplicatedCode
- if (!join.isEmpty()) {
- template = join.stream()
- .reduce(template,
- (acc, join) -> TemplateString.combine(acc, wrap(join)),
- TemplateString::combine);
- }
- if (!where.isEmpty()) {
- if (where.size() == 1) {
- template = TemplateString.combine(template, TemplateString.of("\nWHERE "), wrap(where.getFirst()));
- } else {
- TemplateString whereClause = where.stream()
- .map(w -> TemplateString.combine(TemplateString.of("("), wrap(w), TemplateString.of(")")))
- .reduce((a, b) -> TemplateString.combine(a, TemplateString.of("\n AND "), b))
- .orElseThrow();
- template = TemplateString.combine(template, TemplateString.of("\nWHERE "), whereClause);
- }
- }
+ template = appendJoinsAndWhere(template);
if (!supportsJoin()) {
template = TemplateString.combine(TemplateString.raw("DELETE\nFROM \0\nWHERE (", from(fromType, false)),
getPrimaryKeyTemplate(false), TemplateString.of(") IN ("), wrap(subquery(template, false)), TemplateString.of(")"));
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/ObjectMapperFactory.java b/storm-core/src/main/java/st/orm/core/template/impl/ObjectMapperFactory.java
index 5aba68695..382efed2f 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/ObjectMapperFactory.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/ObjectMapperFactory.java
@@ -37,8 +37,6 @@
import st.orm.PK;
import st.orm.core.spi.Instantiators;
import st.orm.core.spi.Nullability;
-import st.orm.core.spi.ORMReflection;
-import st.orm.core.spi.Providers;
import st.orm.core.spi.RefFactory;
import st.orm.core.template.SqlTemplateException;
import st.orm.mapping.Instantiator;
@@ -48,8 +46,6 @@
*/
public final class ObjectMapperFactory {
- private static final ORMReflection REFLECTION = Providers.getORMReflection();
-
private ObjectMapperFactory() {
}
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/PredicateBuilderFactory.java b/storm-core/src/main/java/st/orm/core/template/impl/PredicateBuilderFactory.java
index 20077fff6..45f1bbc1a 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/PredicateBuilderFactory.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/PredicateBuilderFactory.java
@@ -89,22 +89,4 @@ static PredicateBuilder createWithId(
Iterable o) {
return new PredicateBuilderImpl<>(wrap(new ObjectExpression(path, operator, o)));
}
- /**
- * Creates a new instance of {@link PredicateBuilder} for the specified path, operator, and values with an ID.
- *
- * @param path the metamodel path representing the field to be queried
- * @param operator the operator to be used in the predicate
- * @param o the values to be used in the predicate
- * @param the type of the record
- * @param the type of the result
- * @param the type of the ID
- * @param the type of the values
- * @return a new instance of {@link PredicateBuilder}
- */
- static PredicateBuilder createRefWithId(
- Metamodel, V> path,
- Operator operator,
- Iterable> o) {
- return new PredicateBuilderImpl<>(wrap(new ObjectExpression(path, operator, o)));
- }
}
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/PreparedStatementTemplateImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/PreparedStatementTemplateImpl.java
index 3f2f13e3a..18fe61706 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/PreparedStatementTemplateImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/PreparedStatementTemplateImpl.java
@@ -43,7 +43,6 @@
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -572,24 +571,6 @@ private static void setParameters(PreparedStatement preparedStatement,
}
}
- @FunctionalInterface
- interface SqlRunnable { void run() throws SQLException; }
-
- private static void setObjectOr(PreparedStatement ps,
- AtomicBoolean supportsSetObject,
- SqlRunnable typedSetter,
- SqlRunnable legacyFallback) throws SQLException {
- if (supportsSetObject.get()) {
- try {
- typedSetter.run();
- return;
- } catch (SQLFeatureNotSupportedException e) {
- supportsSetObject.set(false);
- }
- }
- legacyFallback.run();
- }
-
@Override
public @Nullable DataSource dataSource() {
return dataSource;
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
index 884631c19..4cbf375bc 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/QueryBuilderImpl.java
@@ -176,6 +176,31 @@ abstract QueryBuilder copyWith(QueryTemplate queryTemplate,
*/
protected abstract boolean supportsJoin();
+ /**
+ * Appends the registered joins and the WHERE clause, with multiple where-conditions AND-ed together, to the
+ * given template.
+ */
+ protected final TemplateString appendJoinsAndWhere(TemplateString template) {
+ if (!join.isEmpty()) {
+ template = join.stream()
+ .reduce(template,
+ (acc, join) -> TemplateString.combine(acc, wrap(join)),
+ TemplateString::combine);
+ }
+ if (!where.isEmpty()) {
+ if (where.size() == 1) {
+ template = TemplateString.combine(template, TemplateString.of("\nWHERE "), wrap(where.getFirst()));
+ } else {
+ TemplateString whereClause = where.stream()
+ .map(w -> TemplateString.combine(TemplateString.of("("), wrap(w), TemplateString.of(")")))
+ .reduce((a, b) -> TemplateString.combine(a, TemplateString.of("\n AND "), b))
+ .orElseThrow();
+ template = TemplateString.combine(template, TemplateString.of("\nWHERE "), whereClause);
+ }
+ }
+ return template;
+ }
+
/**
* Resolving a reference selects the referenced table's columns into the row that holds the reference, which only a
* statement that selects rows can do.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/QueryModelImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/QueryModelImpl.java
index 12fdee18b..402486185 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/QueryModelImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/QueryModelImpl.java
@@ -607,7 +607,7 @@ private static boolean isPrimitiveCompatible(Object o, Class> clazz) {
* @return the resolved value.
* @throws SqlTemplateException if the value is invalid in this context.
*/
- private Object resolveElements(@Nullable Object value) throws SqlTemplateException {
+ private @Nullable Object resolveElements(@Nullable Object value) throws SqlTemplateException {
return switch (value) {
case TemplateString ignore -> throw new SqlTemplateException("TemplateString is not allowed as a string template value.");
case Stream> ignore -> throw new SqlTemplateException("Stream is not supported as a string template value. Collect the Stream into a List before passing it.");
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java b/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
index cebb9c79d..683deb9ff 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
@@ -1272,9 +1272,12 @@ private static RecordStep recordStep(RecordField field,
* @return the reader for the target's primary key.
* @throws SqlTemplateException if the type declares no primary key.
*/
- private static KeyReader keyReader(RecordType type, FetchPlan fetchPlan) throws SqlTemplateException {
- RecordField pkField = findPkField(type.type()).orElseThrow(() -> new SqlTemplateException(
- "Cannot resolve a reference to %s: the type declares no primary key.".formatted(type.type().getSimpleName())));
+ /**
+ * Returns the flat column offset of the primary key field: the sum of the column counts of all fields
+ * preceding it.
+ */
+ private static int pkColumnOffset(RecordType type, RecordField pkField, FetchPlan fetchPlan)
+ throws SqlTemplateException {
int offset = 0;
for (RecordField field : type.fields()) {
if (field.name().equals(pkField.name())) {
@@ -1282,6 +1285,13 @@ private static KeyReader keyReader(RecordType type, FetchPlan fetchPlan) throws
}
offset += getFieldColumnCount(field, fetchPlan);
}
+ return offset;
+ }
+
+ private static KeyReader keyReader(RecordType type, FetchPlan fetchPlan) throws SqlTemplateException {
+ RecordField pkField = findPkField(type.type()).orElseThrow(() -> new SqlTemplateException(
+ "Cannot resolve a reference to %s: the type declares no primary key.".formatted(type.type().getSimpleName())));
+ int offset = pkColumnOffset(type, pkField, fetchPlan);
int columnCount = getFieldColumnCount(pkField, fetchPlan);
Constructor> keyConstructor = null;
if (columnCount > 1) {
@@ -1401,14 +1411,7 @@ private static PkInfo calculatePkInfo(RecordType type, FetchPlan fetchPlan) thro
return PkInfo.NONE;
}
RecordField pkField = pkFieldOpt.get();
- // Calculate the offset: sum of column counts for all fields before the PK field.
- int offset = 0;
- for (RecordField field : type.fields()) {
- if (field.name().equals(pkField.name())) {
- break;
- }
- offset += getFieldColumnCount(field, fetchPlan);
- }
+ int offset = pkColumnOffset(type, pkField, fetchPlan);
// Calculate how many columns the PK spans.
int pkColumnCount = getFieldColumnCount(pkField, fetchPlan);
// For composite PKs (record types), we need the constructor.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/RecordValidation.java b/storm-core/src/main/java/st/orm/core/template/impl/RecordValidation.java
index 11e3ec87f..eaafac6d8 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/RecordValidation.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/RecordValidation.java
@@ -54,8 +54,6 @@
import st.orm.Ref;
import st.orm.StormConfig;
import st.orm.Version;
-import st.orm.core.spi.ORMReflection;
-import st.orm.core.spi.Providers;
import st.orm.core.spi.TypeDiscovery;
import st.orm.core.template.SqlTemplate;
import st.orm.core.template.SqlTemplate.Parameter;
@@ -70,7 +68,6 @@
@SuppressWarnings("ALL")
final class RecordValidation {
- private static final ORMReflection REFLECTION = Providers.getORMReflection();
private static final Logger LOGGER = LoggerFactory.getLogger("st.orm.validation");
private RecordValidation() {
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
index ba50c0153..32cacdbec 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java
@@ -255,9 +255,7 @@ public List validateAndReport(Iterable> types, boo
*
This is a convenience method for "warn" mode: validation issues are logged but never cause an exception.
*/
public void validateOrWarn() {
- LOGGER.info("Validating Data types for schema compatibility.");
- List> types = TypeDiscovery.getDataTypes();
- reportErrors(validate(types), false, types.size());
+ validateAndReport(false);
}
/**
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SelectBuilderImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/SelectBuilderImpl.java
index d458110fb..ef7e0362b 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SelectBuilderImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SelectBuilderImpl.java
@@ -254,24 +254,7 @@ private TemplateString toTemplateString(TemplateString selectClause, boolean wit
if (hasLock && queryTemplate.dialect().applyLockHintAfterFrom()) {
template = TemplateString.combine(template, TemplateString.of("\n"), forLock);
}
- //noinspection DuplicatedCode
- if (!join.isEmpty()) {
- template = join.stream()
- .reduce(template,
- (acc, join) -> TemplateString.combine(acc, wrap(join)),
- TemplateString::combine);
- }
- if (!where.isEmpty()) {
- if (where.size() == 1) {
- template = TemplateString.combine(template, TemplateString.of("\nWHERE "), wrap(where.getFirst()));
- } else {
- TemplateString whereClause = where.stream()
- .map(w -> TemplateString.combine(TemplateString.of("("), wrap(w), TemplateString.of(")")))
- .reduce((a, b) -> TemplateString.combine(a, TemplateString.of("\n AND "), b))
- .orElseThrow();
- template = TemplateString.combine(template, TemplateString.of("\nWHERE "), whereClause);
- }
- }
+ template = appendJoinsAndWhere(template);
if (!groupBy.isEmpty()) {
TemplateString groupByClause = groupBy.stream()
.reduce((a, b) -> TemplateString.combine(a, TemplateString.of(", "), b))
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SqlInterceptorManager.java b/storm-core/src/main/java/st/orm/core/template/impl/SqlInterceptorManager.java
index 0d0c8f4b7..523b3b505 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SqlInterceptorManager.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SqlInterceptorManager.java
@@ -226,13 +226,7 @@ private SqlInterceptorManager() {
* @param interceptor the interceptor to call for each SQL statement.
*/
public static void registerGlobalInterceptor(UnaryOperator interceptor) {
- LOCK.writeLock().lock();
- try {
- GLOBAL_OPERATORS.add(interceptor);
- globalOperatorCount = GLOBAL_OPERATORS.size();
- } finally {
- LOCK.writeLock().unlock();
- }
+ addGlobalOperator(interceptor);
}
/**
@@ -241,13 +235,7 @@ public static void registerGlobalInterceptor(UnaryOperator interceptor) {
* @param observer the observer to call for each SQL statement.
*/
public static void registerGlobalObserver(Consumer observer) {
- LOCK.writeLock().lock();
- try {
- GLOBAL_OPERATORS.add(observer);
- globalOperatorCount = GLOBAL_OPERATORS.size();
- } finally {
- LOCK.writeLock().unlock();
- }
+ addGlobalOperator(observer);
}
/**
@@ -256,13 +244,7 @@ public static void registerGlobalObserver(Consumer observer) {
* @param observer the observer to unregister.
*/
public static void unregisterGlobalObserver(UnaryOperator observer) {
- LOCK.writeLock().lock();
- try {
- GLOBAL_OPERATORS.remove(observer);
- globalOperatorCount = GLOBAL_OPERATORS.size();
- } finally {
- LOCK.writeLock().unlock();
- }
+ removeGlobalOperator(observer);
}
/**
@@ -271,9 +253,23 @@ public static void unregisterGlobalObserver(UnaryOperator observer) {
* @param observer the observer to unregister.
*/
public static void unregisterGlobalObserver(Consumer observer) {
+ removeGlobalOperator(observer);
+ }
+
+ private static void addGlobalOperator(Object operator) {
+ LOCK.writeLock().lock();
+ try {
+ GLOBAL_OPERATORS.add(operator);
+ globalOperatorCount = GLOBAL_OPERATORS.size();
+ } finally {
+ LOCK.writeLock().unlock();
+ }
+ }
+
+ private static void removeGlobalOperator(Object operator) {
LOCK.writeLock().lock();
try {
- GLOBAL_OPERATORS.remove(observer);
+ GLOBAL_OPERATORS.remove(operator);
globalOperatorCount = GLOBAL_OPERATORS.size();
} finally {
LOCK.writeLock().unlock();
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
index b63237a6c..4f46dde33 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
@@ -474,74 +474,60 @@ Sql process(TemplateString template, boolean applyInterceptors) throws SqlTempla
* that differ only in how far a collection expanded share it.
*/
private Object getShapeKey(BindingContext bindingContext) {
- try {
- var fragments = bindingContext.fragments();
- var elements = bindingContext.elements();
- var shapeGenerator = shapeGenerator();
- var shapeKey = new ArrayList<>(fragments.size() + elements.size());
- for (int i = 0, size = fragments.size(); i < size; i++) {
- shapeKey.add(fragments.get(i));
- if (i < elements.size()) {
- var element = elements.get(i);
- if (element instanceof Wrapped(var wrapped)) {
- for (var e : wrapped) {
- if (!e.synthetic()) {
- var key = getElementProcessor(e.element()).getShapeKey(e.element(), shapeGenerator);
- if (key != null) {
- shapeKey.add(key);
- } else {
- return null;
- }
- }
- }
- } else {
- var key = getElementProcessor(element).getShapeKey(element, shapeGenerator);
- if (key != null) {
- shapeKey.add(key);
- } else {
- return null;
- }
- }
- }
- }
- return shapeKey;
- } catch (SqlTemplateException e) {
- throw new UncheckedSqlTemplateException(e);
- }
+ var shapeGenerator = shapeGenerator();
+ return buildKey(bindingContext,
+ element -> getElementProcessor(element).getShapeKey(element, shapeGenerator));
}
private Object getCompilationKey(BindingContext bindingContext) {
+ return buildKey(bindingContext,
+ element -> getElementProcessor(element).getCompilationKey(element, keyGenerator));
+ }
+
+ /**
+ * The per-element contribution to a template key, or {@code null} when the element does not support keying.
+ */
+ @FunctionalInterface
+ private interface KeyExtractor {
+ @Nullable Object apply(Element element) throws SqlTemplateException;
+ }
+
+ /**
+ * Interleaves the template's fragments with the per-element keys the extractor produces, or returns
+ * {@code null} as soon as any element yields no key.
+ */
+ private @Nullable Object buildKey(BindingContext bindingContext, KeyExtractor extractor) {
try {
var fragments = bindingContext.fragments();
var elements = bindingContext.elements();
// Runs for every processed template; sized for the common case of one key per fragment and element.
- var compilationKey = new ArrayList<>(fragments.size() + elements.size());
+ var templateKey = new ArrayList<>(fragments.size() + elements.size());
for (int i = 0, size = fragments.size(); i < size; i++) {
- compilationKey.add(fragments.get(i));
+ templateKey.add(fragments.get(i));
if (i < elements.size()) {
var element = elements.get(i);
if (element instanceof Wrapped(var wrapped)) {
for (var e : wrapped) {
- if (!e.synthetic()) { // Ignore synthetic elements for the compilation key.
- var key = getElementProcessor(e.element()).getCompilationKey(e.element(), keyGenerator);
+ if (!e.synthetic()) { // Ignore synthetic elements for the key.
+ var key = extractor.apply(e.element());
if (key != null) {
- compilationKey.add(key);
+ templateKey.add(key);
} else {
return null;
}
}
}
} else {
- var key = getElementProcessor(element).getCompilationKey(element, keyGenerator);
+ var key = extractor.apply(element);
if (key != null) {
- compilationKey.add(key);
+ templateKey.add(key);
} else {
return null;
}
}
}
}
- return compilationKey;
+ return templateKey;
} catch (SqlTemplateException e) {
throw new UncheckedSqlTemplateException(e);
}
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/TemplatePreparation.java b/storm-core/src/main/java/st/orm/core/template/impl/TemplatePreparation.java
index 412ba29fe..861dfa7b0 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/TemplatePreparation.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/TemplatePreparation.java
@@ -330,15 +330,7 @@ private Element resolveObjectElement(
) throws SqlTemplateException {
String previous = removeComments(previousFragment, template.dialect()).stripTrailing().toUpperCase();
return switch (operation) {
- case SELECT, DELETE, UNDEFINED -> {
- if (endsWithKeyword(previous, "WHERE")) {
- if (o != null) {
- yield where(o);
- }
- throw new SqlTemplateException("Non-null object expected after WHERE.");
- }
- yield param(o);
- }
+ case SELECT, DELETE, UNDEFINED -> whereOrParam(previous, o);
case INSERT -> {
if (endsWithKeyword(previous, "VALUES")) {
if (o instanceof Data r) {
@@ -346,13 +338,7 @@ private Element resolveObjectElement(
}
throw new SqlTemplateException("Record expected after VALUES.");
}
- if (endsWithKeyword(previous, "WHERE")) {
- if (o != null) {
- yield where(o);
- }
- throw new SqlTemplateException("Non-null object expected after WHERE.");
- }
- yield param(o);
+ yield whereOrParam(previous, o);
}
case UPDATE -> {
if (endsWithKeyword(previous, "SET")) {
@@ -361,17 +347,25 @@ private Element resolveObjectElement(
}
throw new SqlTemplateException("Record expected after SET.");
}
- if (endsWithKeyword(previous, "WHERE")) {
- if (o != null) {
- yield where(o);
- }
- throw new SqlTemplateException("Non-null object expected after WHERE.");
- }
- yield param(o);
+ yield whereOrParam(previous, o);
}
};
}
+ /**
+ * Resolves an object in a position where a where-object is accepted: after WHERE the object becomes a
+ * where-element (and must be non-null), anywhere else it becomes a parameter.
+ */
+ private Element whereOrParam(String previous, @Nullable Object o) throws SqlTemplateException {
+ if (endsWithKeyword(previous, "WHERE")) {
+ if (o != null) {
+ return where(o);
+ }
+ throw new SqlTemplateException("Non-null object expected after WHERE.");
+ }
+ return param(o);
+ }
+
/**
* Resolves an array value into an element.
*
@@ -1170,19 +1164,7 @@ private void deriveAutoJoins(
if (beyondRef && !isReferencedBeyond(referenced.joined(), pkPath)) {
continue;
}
- String fromAlias;
- if (fkName == null) {
- fromAlias = aliasMapper.getAlias(
- table,
- fkPath,
- INNER,
- template.dialect(),
- () -> new SqlTemplateException("Table %s for From not found at path %s."
- .formatted(type.type().getSimpleName(), fkPath))
- );
- } else {
- fromAlias = fkName;
- }
+ String fromAlias = resolveFromAlias(fkName, table, fkPath, type, aliasMapper);
RecordType fieldType = getRecordType(field.type());
boolean effectiveOuterJoin = outerJoin || field.nullable();
String alias = addForeignKeyJoin(table, rootTable, field, fieldType, fromAlias, fkPath,
@@ -1198,24 +1180,36 @@ private void deriveAutoJoins(
if (beyondRef && !isReferencedBeyond(referenced.joined(), pkPath)) {
continue;
}
- String fromAlias;
- if (fkName == null) {
- fromAlias = aliasMapper.getAlias(
- table,
- fkPath,
- INNER,
- template.dialect(),
- () -> new SqlTemplateException("Table %s for From not found at path %s."
- .formatted(type.type().getSimpleName(), fkPath))
- );
- } else {
- fromAlias = fkName;
- }
+ String fromAlias = resolveFromAlias(fkName, table, fkPath, type, aliasMapper);
deriveAutoJoins(getRecordType(field.type()), table, copy, fromAlias, outerJoin, beyondRef, context);
}
}
}
+ /**
+ * Resolves the alias a derived join starts from: the enclosing foreign key's name when the traversal is inside
+ * one, or the alias registered for the table at the foreign key path otherwise.
+ */
+ private String resolveFromAlias(
+ @Nullable String fkName,
+ Class extends Data> table,
+ String fkPath,
+ RecordType type,
+ AliasMapper aliasMapper
+ ) throws SqlTemplateException {
+ if (fkName != null) {
+ return fkName;
+ }
+ return aliasMapper.getAlias(
+ table,
+ fkPath,
+ INNER,
+ template.dialect(),
+ () -> new SqlTemplateException("Table %s for From not found at path %s."
+ .formatted(type.type().getSimpleName(), fkPath))
+ );
+ }
+
/**
* Generates a foreign key join from {@code fromAlias} on {@code table} to the table backing {@code fieldType},
* registers the new alias and foreign key mapping, and appends the join to {@code joins}. The join is marked as an
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/TemplateProcessor.java b/storm-core/src/main/java/st/orm/core/template/impl/TemplateProcessor.java
index 1b91ea1bd..04d078ff1 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/TemplateProcessor.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/TemplateProcessor.java
@@ -1054,19 +1054,7 @@ private final class BindingSession implements TemplateBinder {
* @throws SqlTemplateException if binding fails.
*/
void bindElements(BindingContext context) throws SqlTemplateException {
- for (Element element : context.elements()) {
- if (element instanceof Wrapped(var wrapped)) {
- for (var e : wrapped) {
- if (!e.synthetic()) {
- var hint = nextHint();
- getElementProcessor(e.element()).bind(e.element(), this, hint);
- }
- }
- } else {
- var hint = nextHint();
- getElementProcessor(element).bind(element, this, hint);
- }
- }
+ bindAll(context.elements());
if (hintCursor != bindHints.size()) {
throw new UncheckedSqlTemplateException(new SqlTemplateException(
"Bind hint consumption mismatch. Used %d hints but %d were produced."
@@ -1118,7 +1106,14 @@ private void assertAllHintsConsumed() {
* @throws SqlTemplateException if binding fails.
*/
private void bindPreparedAttached(PreparedTemplate preparedTemplate) throws SqlTemplateException {
- for (var element : preparedTemplate.context().elements()) {
+ bindAll(preparedTemplate.context().elements());
+ }
+
+ /**
+ * Binds the given elements, consuming a bind hint from the shared tape for each non-synthetic element.
+ */
+ private void bindAll(List elements) throws SqlTemplateException {
+ for (Element element : elements) {
if (element instanceof Wrapped(var wrapped)) {
for (var e : wrapped) {
if (!e.synthetic()) {
diff --git a/storm-foundation/src/main/java/st/orm/LoadedRef.java b/storm-foundation/src/main/java/st/orm/LoadedRef.java
new file mode 100644
index 000000000..424457345
--- /dev/null
+++ b/storm-foundation/src/main/java/st/orm/LoadedRef.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Detached {@link Ref} implementation wrapping an already loaded record along with its row identity.
+ *
+ *
The id is stored rather than derived so one implementation serves both entities, whose id comes from the
+ * record itself, and projections, whose row identity is supplied separately. The ref is not attached to a
+ * database context; the wrapped record is all it can return.
+ *
+ * @param record type.
+ * @since 1.14
+ */
+final class LoadedRef extends AbstractRef {
+ private final T value;
+ private final Object id;
+
+ LoadedRef(T value, Object id) {
+ this.value = requireNonNull(value, "value");
+ this.id = id;
+ }
+
+ @Override
+ public Class type() {
+ //noinspection unchecked
+ return (Class) value.getClass();
+ }
+
+ @Override
+ public Object id() {
+ return id;
+ }
+
+ @Override
+ public T getOrNull() {
+ return value;
+ }
+
+ @Override
+ public T fetchOrNull() {
+ return value;
+ }
+
+ @Override
+ public boolean isFetchable() {
+ return false;
+ }
+
+ @Override
+ public Ref unload() {
+ return Ref.of(type(), id());
+ }
+}
diff --git a/storm-foundation/src/main/java/st/orm/Ref.java b/storm-foundation/src/main/java/st/orm/Ref.java
index db51c3642..40db58877 100644
--- a/storm-foundation/src/main/java/st/orm/Ref.java
+++ b/storm-foundation/src/main/java/st/orm/Ref.java
@@ -75,46 +75,8 @@ static Ref of(Class type, ID pk) {
* @return a fully loaded ref instance for the provided entity.
*/
static > Ref of(E entity) {
- class DetachedEntity> extends AbstractRef {
- private final TE entity;
-
- DetachedEntity(TE entity) {
- requireNonNull(entity, "Entity cannot be null.");
- this.entity = entity;
- }
-
- @Override
- public Class type() {
- //noinspection unchecked
- return (Class) entity.getClass();
- }
-
- @Override
- public Object id() {
- return entity.id();
- }
-
- @Override
- public TE getOrNull() {
- return entity;
- }
-
- @Override
- public TE fetchOrNull() {
- return entity;
- }
-
- @Override
- public boolean isFetchable() {
- return false;
- }
-
- @Override
- public Ref unload() {
- return Ref.of(type(), id());
- }
- }
- return new DetachedEntity<>(entity);
+ requireNonNull(entity, "Entity cannot be null.");
+ return new LoadedRef<>(entity, entity.id());
}
/**
@@ -132,47 +94,9 @@ public Ref unload() {
* @return a fully loaded ref instance for the provided projection.
*/
static
, ID> Ref
of(P projection, ID id) {
- class DetachedProjection, TID> extends AbstractRef {
- private final TID id;
- private final TE projection;
-
- DetachedProjection(TID id, TE projection) {
- this.id = requireNonNull(id, "ID cannot be null.");
- this.projection = requireNonNull(projection, "Projection cannot be null.");
- }
-
- @Override
- public Class type() {
- //noinspection unchecked
- return (Class) projection.getClass();
- }
-
- @Override
- public Object id() {
- return id;
- }
-
- @Override
- public TE getOrNull() {
- return projection;
- }
-
- @Override
- public TE fetchOrNull() {
- return projection;
- }
-
- @Override
- public boolean isFetchable() {
- return false;
- }
-
- @Override
- public Ref unload() {
- return Ref.of(type(), id());
- }
- }
- return new DetachedProjection<>(id, projection);
+ requireNonNull(id, "ID cannot be null.");
+ requireNonNull(projection, "Projection cannot be null.");
+ return new LoadedRef<>(projection, id);
}
/**
diff --git a/storm-h2/pom.xml b/storm-h2/pom.xml
index 100e2d5a8..ff2de8c12 100644
--- a/storm-h2/pom.xml
+++ b/storm-h2/pom.xml
@@ -37,7 +37,7 @@
maven-surefire-pluginfalse
@@ -101,6 +101,12 @@
provided
+
+ st.orm
+ storm-test
+ ${project.version}
+ test
+ org.springframework.bootspring-boot-starter-test
diff --git a/storm-h2/src/main/java/st/orm/spi/h2/H2SqlDialect.java b/storm-h2/src/main/java/st/orm/spi/h2/H2SqlDialect.java
index 9cc1cd967..e95e080bb 100644
--- a/storm-h2/src/main/java/st/orm/spi/h2/H2SqlDialect.java
+++ b/storm-h2/src/main/java/st/orm/spi/h2/H2SqlDialect.java
@@ -26,14 +26,12 @@
import java.sql.SQLException;
import java.util.Set;
import java.util.UUID;
-import java.util.regex.Pattern;
import java.util.stream.Stream;
import st.orm.Operator;
import st.orm.StormConfig;
import st.orm.core.spi.DefaultSqlDialect;
-import st.orm.core.template.SqlDialect;
-public class H2SqlDialect extends DefaultSqlDialect implements SqlDialect {
+public class H2SqlDialect extends DefaultSqlDialect {
public H2SqlDialect() {
}
@@ -53,14 +51,6 @@ public String name() {
return "H2";
}
- /**
- * H2 does not support aliasing the target table in DELETE statements.
- */
- @Override
- public boolean supportsDeleteAlias() {
- return false;
- }
-
/**
* H2 supports multi-value tuples in the IN clause.
*/
@@ -69,19 +59,6 @@ public boolean supportsMultiValueTuples() {
return true;
}
- private static final Pattern H2_IDENTIFIER = Pattern.compile("^[A-Za-z][A-Za-z0-9_]*$");
-
- /**
- * Returns the pattern for valid identifiers.
- *
- * @return the pattern for valid identifiers.
- * @since 1.11
- */
- @Override
- public Pattern getValidIdentifierPattern() {
- return H2_IDENTIFIER;
- }
-
private static final Set H2_KEYWORDS = Stream.concat(ANSI_KEYWORDS.stream(), Stream.of(
"AUTOINCREMENT", "CACHED", "EXPLAIN", "IF", "ILIKE", "INDEX", "KEY", "LIMIT",
"MEMORY", "MINUS", "OFFSET", "QUALIFY", "REGEXP", "ROWNUM", "SYSDATE", "SYSTIME",
@@ -111,40 +88,6 @@ public String escape(String name) {
return "\"%s\"".formatted(name.replace("\"", "\"\""));
}
- /**
- * Regex for double-quoted identifiers (handling doubled double quotes as escapes).
- */
- private static final Pattern IDENTIFIER_PATTERN = Pattern.compile(
- "\"(?:\"\"|[^\"])*\""
- );
-
- /**
- * Returns the pattern for identifiers.
- *
- * @return the pattern for identifiers.
- */
- @Override
- public Pattern getIdentifierPattern() {
- return IDENTIFIER_PATTERN;
- }
-
- /**
- * Regex for single-quoted string literals, handling both doubled single quotes and backslash escapes.
- */
- private static final Pattern QUOTE_LITERAL_PATTERN = Pattern.compile(
- "'(?:''|\\\\.|[^'\\\\])*'"
- );
-
- /**
- * Returns the pattern for string literals.
- *
- * @return the pattern for string literals.
- */
- @Override
- public Pattern getQuoteLiteralPattern() {
- return QUOTE_LITERAL_PATTERN;
- }
-
/**
* Returns whether a multi-column comparison renders as a row value tuple, which for H2 is the case for an
* ordering comparison and for a multi-row list.
@@ -229,16 +172,6 @@ public String forShareLockHint() {
return "";
}
- /**
- * Returns the lock hint for a write lock.
- *
- * @return the lock hint for a write lock.
- */
- @Override
- public String forUpdateLockHint() {
- return "FOR UPDATE";
- }
-
/**
* Sets a UUID parameter using {@link PreparedStatement#setObject(int, Object)}, which allows the H2 JDBC
* driver to bind the value as a native UUID type.
diff --git a/storm-h2/src/test/resources/META-INF/services/st.orm.core.spi.ConnectionProvider b/storm-h2/src/test/resources/META-INF/services/st.orm.core.spi.ConnectionProvider
index f0d44d863..b51953f70 100644
--- a/storm-h2/src/test/resources/META-INF/services/st.orm.core.spi.ConnectionProvider
+++ b/storm-h2/src/test/resources/META-INF/services/st.orm.core.spi.ConnectionProvider
@@ -1 +1 @@
-st.orm.spi.h2.testsupport.TestSpringConnectionProvider
+st.orm.test.spring.TestSpringConnectionProvider
diff --git a/storm-jackson2/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java b/storm-jackson2/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
index 0468b5995..cfc8dfd0b 100644
--- a/storm-jackson2/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
+++ b/storm-jackson2/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
@@ -227,7 +227,7 @@ public Object fromDatabase(Object[] values, RefFactory refFactory) throws SqlTem
RefFactory outerRefFactory = REF_FACTORY.get();
REF_FACTORY.set(refFactory);
try {
- return mapper.readValue((String) values[0], typeReference);
+ return mapper.readValue((String) value, typeReference);
} catch (JsonProcessingException e) {
throw new SqlTemplateException(e);
} finally {
diff --git a/storm-jackson3/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java b/storm-jackson3/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
index b6abb0445..2ef9a98bb 100644
--- a/storm-jackson3/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
+++ b/storm-jackson3/src/main/java/st/orm/jackson/spi/JsonORMConverterImpl.java
@@ -227,7 +227,7 @@ public Object fromDatabase(Object[] values, RefFactory refFactory) throws SqlTem
RefFactory outerRefFactory = REF_FACTORY.get();
REF_FACTORY.set(refFactory);
try {
- return mapper.readValue((String) values[0], typeReference);
+ return mapper.readValue((String) value, typeReference);
} catch (JacksonException e) {
throw new SqlTemplateException(e);
} finally {
diff --git a/storm-java21/src/main/java/st/orm/repository/impl/EntityRepositoryImpl.java b/storm-java21/src/main/java/st/orm/repository/impl/EntityRepositoryImpl.java
index 4778a2261..f759e85b7 100644
--- a/storm-java21/src/main/java/st/orm/repository/impl/EntityRepositoryImpl.java
+++ b/storm-java21/src/main/java/st/orm/repository/impl/EntityRepositoryImpl.java
@@ -27,8 +27,6 @@
import st.orm.Page;
import st.orm.Pageable;
import st.orm.Ref;
-import st.orm.Scrollable;
-import st.orm.Window;
import st.orm.repository.EntityRepository;
import st.orm.template.Model;
import st.orm.template.ORMTemplate;
@@ -248,11 +246,6 @@ public Page> pageRef(Pageable pageable) {
return core.pageRef(pageable);
}
- @Override
- public Window scroll(Scrollable scrollable) {
- return select().scroll(scrollable);
- }
-
@Override
public List findAll() {
return core.findAll();
diff --git a/storm-java21/src/main/java/st/orm/repository/impl/ProjectionRepositoryImpl.java b/storm-java21/src/main/java/st/orm/repository/impl/ProjectionRepositoryImpl.java
index b926bc0db..233b67fa1 100644
--- a/storm-java21/src/main/java/st/orm/repository/impl/ProjectionRepositoryImpl.java
+++ b/storm-java21/src/main/java/st/orm/repository/impl/ProjectionRepositoryImpl.java
@@ -27,8 +27,6 @@
import st.orm.Pageable;
import st.orm.Projection;
import st.orm.Ref;
-import st.orm.Scrollable;
-import st.orm.Window;
import st.orm.repository.ProjectionRepository;
import st.orm.template.Model;
import st.orm.template.ORMTemplate;
@@ -133,13 +131,6 @@ public Page> pageRef(Pageable pageable) {
return core.pageRef(pageable);
}
- // Window methods.
-
- @Override
- public Window