diff --git a/.gitignore b/.gitignore
index 400a274124e..2e62c6aec0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ src/main/docs/resources/img/micronaut-logo-white.svg
# OpenCode files
.sisyphus/
+.kotlin
diff --git a/data-connection/build.gradle b/data-connection/build.gradle
index 32e09067c5a..7aee37d8d0b 100644
--- a/data-connection/build.gradle
+++ b/data-connection/build.gradle
@@ -16,5 +16,6 @@ dependencies {
implementation mn.micronaut.core.reactive
testImplementation(mnTest.micronaut.test.junit5)
+ testImplementation(mnTest.junit.jupiter.params)
compileOnly(mn.kotlinx.coroutines.reactor)
}
diff --git a/data-connection/src/main/java/io/micronaut/data/connection/ConnectionCapabilities.java b/data-connection/src/main/java/io/micronaut/data/connection/ConnectionCapabilities.java
index d2092f8f977..cc5dde9041b 100644
--- a/data-connection/src/main/java/io/micronaut/data/connection/ConnectionCapabilities.java
+++ b/data-connection/src/main/java/io/micronaut/data/connection/ConnectionCapabilities.java
@@ -20,11 +20,23 @@
import io.micronaut.core.order.Ordered;
import java.sql.Connection;
+import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Supplier;
/**
- * Defines the capabilities of a {@link Connection}.
+ * Defines the capabilities of a database connection.
+ *
+ * The primary API is {@link #supports(Capability, Supplier)}, which accepts a supplier of
+ * the database product name so that capability detection works for both JDBC
+ * ({@link Connection}) and R2DBC ({@code io.r2dbc.spi.Connection}) connections.
+ * Convenience overloads such as {@link #supports(Capability, Connection)} delegate to the
+ * supplier-based method.
+ *
+ * When the database product name cannot be determined (e.g., the supplier throws or returns
+ * an unrecognised value), implementations should default to returning {@code true} (capability
+ * is supported) so that the calling code can fall back to its own default behaviour safely.
*
* You can provide your own implementation via Java SPI by registering
* {@code io.micronaut.data.connection.ConnectionCapabilities} in
@@ -59,6 +71,25 @@ enum Capability {
*/
ConnectionCapabilities INSTANCE = loadInstance();
+ /**
+ * Determines whether the database supports the requested capability.
+ *
+ * Implementations receive a {@link Supplier} rather than a direct connection object so
+ * that this method works uniformly with JDBC ({@link Connection}) and R2DBC
+ * ({@code io.r2dbc.spi.Connection}) connections. The supplier is called lazily; if
+ * determining the product name is expensive, implementations may cache the result.
+ *
+ * If the supplier throws or returns an unrecognised product name, implementations should
+ * return {@code true} (capability supported) so that callers can fall back gracefully to
+ * their own default behaviour.
+ *
+ * @param capability The capability to evaluate
+ * @param databaseProductNameSupplier supplier of the database product name; may throw
+ * {@link RuntimeException} if the metadata cannot be retrieved
+ * @return {@code true} if the connection supports the capability; {@code false} otherwise
+ */
+ boolean supports(Capability capability, Supplier databaseProductNameSupplier);
+
/**
* Determines whether the given JDBC connection supports the requested capability.
*
@@ -66,7 +97,15 @@ enum Capability {
* @param connection The JDBC connection
* @return {@code true} if the connection supports the capability; {@code false} otherwise
*/
- boolean supports(Capability capability, Connection connection);
+ default boolean supports(Capability capability, Connection connection) {
+ return supports(capability, () -> {
+ try {
+ return connection.getMetaData().getDatabaseProductName();
+ } catch (SQLException e) {
+ return "Unknown";
+ }
+ });
+ }
private static ConnectionCapabilities loadInstance() {
List providers = new ArrayList<>();
diff --git a/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionCapabilities.java b/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionCapabilities.java
index 11e0595e4aa..051e84e82dc 100644
--- a/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionCapabilities.java
+++ b/data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionCapabilities.java
@@ -16,7 +16,9 @@
package io.micronaut.data.connection;
import io.micronaut.core.annotation.Internal;
+
import java.sql.Connection;
+import java.util.function.Supplier;
/**
* Internal fallback {@link ConnectionCapabilities} implementation that assumes all capabilities are supported.
@@ -25,9 +27,13 @@
*/
@Internal
final class DefaultConnectionCapabilities implements ConnectionCapabilities {
+ @Override
+ public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) {
+ return true;
+ }
@Override
- public boolean supports(ConnectionCapabilities.Capability capability, Connection connection) {
+ public boolean supports(Capability capability, Connection connection) {
return true;
}
}
diff --git a/data-connection/src/test/java/example/CustomConnectionCapabilities.java b/data-connection/src/test/java/example/CustomConnectionCapabilities.java
index 169f1c22867..2fb22594359 100644
--- a/data-connection/src/test/java/example/CustomConnectionCapabilities.java
+++ b/data-connection/src/test/java/example/CustomConnectionCapabilities.java
@@ -1,13 +1,12 @@
package example;
import io.micronaut.data.connection.ConnectionCapabilities;
-
-import java.sql.Connection;
+import java.util.function.Supplier;
public class CustomConnectionCapabilities implements ConnectionCapabilities {
@Override
- public boolean supports(ConnectionCapabilities.Capability capability, Connection connection) {
+ public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) {
return true;
}
}
diff --git a/data-connection/src/test/java/io/micronaut/data/connection/DefaultConnectionCapabilitiesTest.java b/data-connection/src/test/java/io/micronaut/data/connection/DefaultConnectionCapabilitiesTest.java
new file mode 100644
index 00000000000..033819f2df5
--- /dev/null
+++ b/data-connection/src/test/java/io/micronaut/data/connection/DefaultConnectionCapabilitiesTest.java
@@ -0,0 +1,62 @@
+package io.micronaut.data.connection;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.lang.reflect.Proxy;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DefaultConnectionCapabilitiesTest {
+
+ private final DefaultConnectionCapabilities capabilities = new DefaultConnectionCapabilities();
+
+ @DisabledInNativeImage
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "MySQL",
+ "MariaDB",
+ "PostgreSQL",
+ "H2",
+ "SQLite",
+ "Oracle",
+ "Microsoft SQL Server"
+ })
+ void supportsReadOnlyForDifferentDatabaseProducts(String databaseProductName) {
+ assertTrue(capabilities.supports(ConnectionCapabilities.Capability.READ_ONLY, connection(databaseProductName)));
+ }
+
+ private Connection connection(String databaseProductName) {
+ DatabaseMetaData metaData = (DatabaseMetaData) Proxy.newProxyInstance(
+ DatabaseMetaData.class.getClassLoader(),
+ new Class>[]{DatabaseMetaData.class},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "getDatabaseProductName" -> databaseProductName;
+ case "unwrap" -> null;
+ case "isWrapperFor" -> false;
+ default -> throw new UnsupportedOperationException(method.getName());
+ }
+ );
+ return connection(metaData);
+ }
+
+ private Connection connection(DatabaseMetaData metaData) {
+ return (Connection) Proxy.newProxyInstance(
+ Connection.class.getClassLoader(),
+ new Class>[]{Connection.class},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "getMetaData" -> metaData;
+ case "unwrap" -> null;
+ case "isWrapperFor" -> false;
+ default -> throw new UnsupportedOperationException(method.getName());
+ }
+ );
+ }
+}
diff --git a/data-model/build.gradle b/data-model/build.gradle
index b4ce64bc02f..ae33c74e6d7 100644
--- a/data-model/build.gradle
+++ b/data-model/build.gradle
@@ -25,4 +25,5 @@ dependencies {
testImplementation mn.micronaut.inject.java.test
testImplementation mnSerde.micronaut.serde.jackson
testImplementation mn.micronaut.jackson.databind
+ testImplementation(mnTest.micronaut.test.junit5)
}
diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/Dialect.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/Dialect.java
index 782b6a58295..9661cdd6352 100644
--- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/Dialect.java
+++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/Dialect.java
@@ -60,15 +60,19 @@ public enum Dialect {
/**
* Postgres 9.5 or later.
*/
- POSTGRES(true, false, ALL_TYPES, false, true, true, true),
+ POSTGRES(true, false, ALL_TYPES, false, true, true, true, true),
/**
* SQL server 2012 or above.
*/
- SQL_SERVER(false, false, ALL_TYPES),
+ SQL_SERVER(false, false, ALL_TYPES, false, false, false, false, true),
/**
* Oracle 12c or above.
*/
- ORACLE(true, true, ALL_TYPES, true, true, true, true),
+ ORACLE(true, true, ALL_TYPES, true, true, true, true, true),
+ /**
+ * SQLite.
+ */
+ SQLITE(false, false, ALL_TYPES, false, true, true, true, false),
/**
* Ansi compliant SQL.
*/
@@ -84,6 +88,7 @@ public enum Dialect {
private final boolean supportsUpdateReturning;
private final boolean supportsInsertReturning;
private final boolean supportsDeleteReturning;
+ private final boolean supportsReadOnly;
/**
* Allows customization of batch support.
@@ -93,7 +98,7 @@ public enum Dialect {
* @param joinTypesSupported EnumSet of supported join types.
*/
Dialect(boolean supportsBatch, boolean stringUUID, EnumSet joinTypesSupported) {
- this(supportsBatch, stringUUID, joinTypesSupported, false, false, false, false);
+ this(supportsBatch, stringUUID, joinTypesSupported, false, false, false, false, true);
}
/**
@@ -106,6 +111,7 @@ public enum Dialect {
* @param supportsUpdateReturning Whether the dialect supports UPDATE ... RETURNING clause.
* @param supportsInsertReturning Whether the dialect supports INSERT ... RETURNING clause.
* @param supportsDeleteReturning Whether the dialect supports DELETE ... RETURNING clause.
+ * @param supportsReadOnly Whether the dialect supports invoking {@link java.sql.Connection#setReadOnly(boolean)}
* @since 4.2.0
*/
Dialect(boolean supportsBatch,
@@ -114,7 +120,8 @@ public enum Dialect {
boolean supportsJsonEntity,
boolean supportsUpdateReturning,
boolean supportsInsertReturning,
- boolean supportsDeleteReturning) {
+ boolean supportsDeleteReturning,
+ boolean supportsReadOnly) {
this.supportsBatch = supportsBatch;
this.stringUUID = stringUUID;
this.joinTypesSupported = joinTypesSupported;
@@ -122,6 +129,7 @@ public enum Dialect {
this.supportsUpdateReturning = supportsUpdateReturning;
this.supportsInsertReturning = supportsInsertReturning;
this.supportsDeleteReturning = supportsDeleteReturning;
+ this.supportsReadOnly = supportsReadOnly;
}
/**
@@ -210,4 +218,14 @@ public boolean supportsInsertReturning() {
public boolean supportsDeleteReturning() {
return supportsDeleteReturning;
}
+
+ /**
+ * Whether the dialect supports read only. e.g. invoking {@link java.sql.Connection#setReadOnly(boolean)}.
+ *
+ * @return true if it does support
+ * @since 5.0.0
+ */
+ public boolean supportsReadOnly() {
+ return supportsReadOnly;
+ }
}
diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java
index ac0cdfe1776..a260a32c966 100644
--- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java
+++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java
@@ -988,6 +988,11 @@ private String addGeneratedStatementToColumn(GeneratedValue.Type type, DataType
column += " AUTO_INCREMENT";
}
break;
+ case SQLITE:
+ if (type == UUID) {
+ column += " NOT NULL DEFAULT (lower(hex(randomblob(4))||'-'||hex(randomblob(2))||'-'||'4'||substr(hex(randomblob(2)),2)||'-'||substr('89ab',abs(random())%4+1,1)||substr(hex(randomblob(2)),2)||'-'||hex(randomblob(6))))";
+ }
+ break;
default:
if (type == UUID) {
column += " NOT NULL DEFAULT random_uuid()";
@@ -1272,9 +1277,18 @@ public String[] getPropertyPath() {
});
}
- builder = INSERT_INTO + getTableName(entity) +
- " (" + String.join(",", columns) + CLOSE_BRACKET + " " +
- "VALUES (" + String.join(String.valueOf(COMMA), values) + CLOSE_BRACKET;
+ if (columns.isEmpty()) {
+ // MySQL/MariaDB do not support DEFAULT VALUES syntax
+ if (dialect == Dialect.MYSQL) {
+ builder = INSERT_INTO + getTableName(entity) + " () VALUES ()";
+ } else {
+ builder = INSERT_INTO + getTableName(entity) + " DEFAULT VALUES";
+ }
+ } else {
+ builder = INSERT_INTO + getTableName(entity) +
+ " (" + String.join(",", columns) + CLOSE_BRACKET + " " +
+ "VALUES (" + String.join(String.valueOf(COMMA), values) + CLOSE_BRACKET;
+ }
if (definition.returning()) {
if (dialect == Dialect.ORACLE) {
diff --git a/data-model/src/main/java/io/micronaut/data/model/schema/sql/SqlColumnMapping.java b/data-model/src/main/java/io/micronaut/data/model/schema/sql/SqlColumnMapping.java
index fb923c7e2ae..6605d734c85 100644
--- a/data-model/src/main/java/io/micronaut/data/model/schema/sql/SqlColumnMapping.java
+++ b/data-model/src/main/java/io/micronaut/data/model/schema/sql/SqlColumnMapping.java
@@ -209,6 +209,8 @@ public String getSqlType(Dialect dialect) {
case UUID -> {
if (dialect == Dialect.ORACLE || dialect == Dialect.MYSQL) {
yield varcharType(36);
+ } else if (dialect == Dialect.SQLITE) {
+ yield "TEXT";
} else if (dialect == Dialect.SQL_SERVER) {
yield "UNIQUEIDENTIFIER";
} else {
@@ -250,6 +252,11 @@ public String getSqlType(Dialect dialect) {
case LONG -> {
if (dialect == Dialect.ORACLE) {
yield "NUMBER(19)";
+
+ // In SQLite, INTEGER PRIMARY KEY has special behavior (rowid, auto-generation).
+ } else if (dialect == Dialect.SQLITE && isAutoGenerated()) {
+ yield "INTEGER";
+
} else {
yield "BIGINT";
}
diff --git a/data-model/src/test/java/io/micronaut/data/model/query/builder/sql/DialectTest.java b/data-model/src/test/java/io/micronaut/data/model/query/builder/sql/DialectTest.java
new file mode 100644
index 00000000000..e5f9d1a8dea
--- /dev/null
+++ b/data-model/src/test/java/io/micronaut/data/model/query/builder/sql/DialectTest.java
@@ -0,0 +1,129 @@
+package io.micronaut.data.model.query.builder.sql;
+
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.model.DataType;
+import org.junit.jupiter.api.Test;
+
+import java.util.EnumSet;
+
+import static io.micronaut.data.annotation.Join.Type.ALL_TYPES;
+import static io.micronaut.data.annotation.Join.Type.DEFAULT;
+import static io.micronaut.data.annotation.Join.Type.FETCH;
+import static io.micronaut.data.annotation.Join.Type.INNER;
+import static io.micronaut.data.annotation.Join.Type.LEFT;
+import static io.micronaut.data.annotation.Join.Type.LEFT_FETCH;
+import static io.micronaut.data.annotation.Join.Type.RIGHT;
+import static io.micronaut.data.annotation.Join.Type.RIGHT_FETCH;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DialectTest {
+
+ private static final EnumSet LIMITED_JOIN_TYPES = EnumSet.of(
+ DEFAULT,
+ LEFT,
+ LEFT_FETCH,
+ RIGHT,
+ RIGHT_FETCH,
+ FETCH,
+ INNER
+ );
+
+ @Test
+ void mapsDataTypesPerDialect() {
+ for (Dialect dialect : Dialect.values()) {
+ DataType expectedUuidType = dialect == Dialect.MYSQL || dialect == Dialect.ORACLE ? DataType.STRING : DataType.UUID;
+
+ assertEquals(expectedUuidType, dialect.getDataType(DataType.UUID));
+ assertEquals(expectedUuidType == DataType.STRING, dialect.requiresStringUUID(DataType.UUID));
+ assertEquals(dialect == Dialect.ORACLE ? DataType.DURATION : DataType.STRING, dialect.getDataType(DataType.DURATION));
+ assertEquals(dialect == Dialect.ORACLE ? DataType.PERIOD : DataType.STRING, dialect.getDataType(DataType.PERIOD));
+ assertEquals(DataType.BOOLEAN, dialect.getDataType(DataType.BOOLEAN));
+ }
+ }
+
+ @Test
+ void supportsExpectedJoinTypes() {
+ assertSupportedJoinTypes(Dialect.H2, LIMITED_JOIN_TYPES);
+ assertSupportedJoinTypes(Dialect.MYSQL, LIMITED_JOIN_TYPES);
+ assertSupportedJoinTypes(Dialect.POSTGRES, ALL_TYPES);
+ assertSupportedJoinTypes(Dialect.SQL_SERVER, ALL_TYPES);
+ assertSupportedJoinTypes(Dialect.ORACLE, ALL_TYPES);
+ assertSupportedJoinTypes(Dialect.SQLITE, ALL_TYPES);
+ assertSupportedJoinTypes(Dialect.ANSI, ALL_TYPES);
+ }
+
+ @Test
+ void allowBatchMatchesExpectedDialects() {
+ assertTrue(Dialect.H2.allowBatch());
+ assertTrue(Dialect.MYSQL.allowBatch());
+ assertTrue(Dialect.POSTGRES.allowBatch());
+ assertFalse(Dialect.SQL_SERVER.allowBatch());
+ assertTrue(Dialect.ORACLE.allowBatch());
+ assertFalse(Dialect.SQLITE.allowBatch());
+ assertTrue(Dialect.ANSI.allowBatch());
+ }
+
+ @Test
+ void supportsJsonEntityMatchesExpectedDialects() {
+ assertFalse(Dialect.H2.supportsJsonEntity());
+ assertFalse(Dialect.MYSQL.supportsJsonEntity());
+ assertFalse(Dialect.POSTGRES.supportsJsonEntity());
+ assertFalse(Dialect.SQL_SERVER.supportsJsonEntity());
+ assertTrue(Dialect.ORACLE.supportsJsonEntity());
+ assertFalse(Dialect.SQLITE.supportsJsonEntity());
+ assertFalse(Dialect.ANSI.supportsJsonEntity());
+ }
+
+ @Test
+ void supportsUpdateReturningMatchesExpectedDialects() {
+ assertFalse(Dialect.H2.supportsUpdateReturning());
+ assertFalse(Dialect.MYSQL.supportsUpdateReturning());
+ assertTrue(Dialect.POSTGRES.supportsUpdateReturning());
+ assertFalse(Dialect.SQL_SERVER.supportsUpdateReturning());
+ assertTrue(Dialect.ORACLE.supportsUpdateReturning());
+ assertTrue(Dialect.SQLITE.supportsUpdateReturning());
+ assertFalse(Dialect.ANSI.supportsUpdateReturning());
+ }
+
+ @Test
+ void supportsInsertReturningMatchesExpectedDialects() {
+ assertFalse(Dialect.H2.supportsInsertReturning());
+ assertFalse(Dialect.MYSQL.supportsInsertReturning());
+ assertTrue(Dialect.POSTGRES.supportsInsertReturning());
+ assertFalse(Dialect.SQL_SERVER.supportsInsertReturning());
+ assertTrue(Dialect.ORACLE.supportsInsertReturning());
+ assertTrue(Dialect.SQLITE.supportsInsertReturning());
+ assertFalse(Dialect.ANSI.supportsInsertReturning());
+ }
+
+ @Test
+ void supportsDeleteReturningMatchesExpectedDialects() {
+ assertFalse(Dialect.H2.supportsDeleteReturning());
+ assertFalse(Dialect.MYSQL.supportsDeleteReturning());
+ assertTrue(Dialect.POSTGRES.supportsDeleteReturning());
+ assertFalse(Dialect.SQL_SERVER.supportsDeleteReturning());
+ assertTrue(Dialect.ORACLE.supportsDeleteReturning());
+ assertTrue(Dialect.SQLITE.supportsDeleteReturning());
+ assertFalse(Dialect.ANSI.supportsDeleteReturning());
+ }
+
+ @Test
+ void supportsReadOnlyMatchesExpectedDialects() {
+ assertTrue(Dialect.H2.supportsReadOnly());
+ assertTrue(Dialect.MYSQL.supportsReadOnly());
+ assertTrue(Dialect.POSTGRES.supportsReadOnly());
+ assertTrue(Dialect.SQL_SERVER.supportsReadOnly());
+ assertTrue(Dialect.ORACLE.supportsReadOnly());
+ assertFalse(Dialect.SQLITE.supportsReadOnly());
+ assertTrue(Dialect.ANSI.supportsReadOnly());
+ }
+
+ private static void assertSupportedJoinTypes(Dialect dialect, EnumSet expectedJoinTypes) {
+ for (Join.Type joinType : Join.Type.values()) {
+ assertEquals(expectedJoinTypes.contains(joinType), dialect.supportsJoinType(joinType),
+ () -> dialect + " join support mismatch for " + joinType);
+ }
+ }
+}
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java
index d88c5435f22..618580b8ff9 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java
@@ -258,9 +258,23 @@ protected static boolean shouldPersistChildOnPersist(RuntimePersistentEntity childPersistentEntity,
+ @Nullable RuntimeAssociation association,
+ Object child) {
+ if (association != null && SqlQueryBuilder.isForeignKeyWithJoinTable(association)) {
+ return childPersistentEntity.getIdentity().getProperty().get(child) == null;
+ }
+ return shouldPersistChildOnPersist(childPersistentEntity, child);
+ }
+
/**
* Build a veto predicate for batch persist of many children.
- * For join-table associations, veto any child with a non-null id (existing). For direct FKs, veto when id present and generated.
+ * For join-table associations, veto any child with a non-null id (existing).
+ * For direct FKs, use the same persistability decision as the single-child cascade path.
*/
protected static Predicate batchPersistVeto(io.micronaut.data.model.runtime.RuntimePersistentEntity childPersistentEntity,
@Nullable RuntimeAssociation association,
@@ -269,13 +283,7 @@ protected static Predicate batchPersistVeto(io.micronaut.data.model.runt
if (association != null && SqlQueryBuilder.isForeignKeyWithJoinTable(association)) {
return val -> alreadyPersisted.contains(val) || identity.getProperty().get(val) != null;
}
- return val -> {
- if (alreadyPersisted.contains(val)) {
- return true;
- }
- Object idVal = identity.getProperty().get(val);
- return idVal != null && identity.isGenerated() && !(identity instanceof Association);
- };
+ return val -> alreadyPersisted.contains(val) || !shouldPersistChildOnPersist(childPersistentEntity, association, val);
}
/**
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/ReactiveCascadeOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/ReactiveCascadeOperations.java
index 5e760ac0913..b47399404e2 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/ReactiveCascadeOperations.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/ReactiveCascadeOperations.java
@@ -184,9 +184,10 @@ public Mono cascadeEntity(Ctx ctx,
LOG.debug("Cascading many PERSIST for '{}' association: '{}'", persistentEntity.getName(), cascadeOp.ctx.associations);
}
+ RuntimeAssociation association = (RuntimeAssociation) cascadeOp.ctx.getAssociation();
Flux childrenFlux = Flux.empty();
for (Object child : cascadeManyOp.children) {
- if (ctx.persisted.contains(child) || childPersistentEntity.getIdentity().getProperty().get(child) != null) {
+ if (ctx.persisted.contains(child) || !shouldPersistChildOnPersist(childPersistentEntity, association, child)) {
childrenFlux = childrenFlux.concatWith(Mono.just(child));
continue;
}
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/SyncCascadeOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/SyncCascadeOperations.java
index 0d3e2e329e4..94b79b2ef89 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/SyncCascadeOperations.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/SyncCascadeOperations.java
@@ -139,13 +139,13 @@ public T cascadeEntity(Ctx ctx,
entities = helper.persistBatch(ctx, sourceChildren, childPersistentEntity, veto);
} else {
entities = CollectionUtils.iterableToList(cascadeManyOp.children);
+ RuntimeAssociation association = (RuntimeAssociation) cascadeManyOp.ctx.getAssociation();
for (ListIterator iterator = entities.listIterator(); iterator.hasNext(); ) {
Object child = iterator.next();
if (ctx.persisted.contains(child)) {
continue;
}
- RuntimePersistentProperty identity = childPersistentEntity.getIdentity();
- if (identity.getProperty().get(child) != null) {
+ if (!shouldPersistChildOnPersist(childPersistentEntity, association, child)) {
continue;
}
Object persisted = helper.persistOne(ctx, child, childPersistentEntity);
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java
index 19df4057602..ca7dfed39c6 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java
@@ -574,9 +574,10 @@ protected boolean isSupportsBatchInsert(PersistentEntity persistentEntity, SqlSt
* @return true if supported
*/
protected boolean isSupportsBatchInsert(PersistentEntity persistentEntity, Dialect dialect) {
- // Oracle and MySql doesn't support a batch with returning generated ID: "DML Returning cannot be batched"
+ if (!dialect.allowBatch()) {
+ return false;
+ }
return switch (dialect) {
- case SQL_SERVER -> false;
case MYSQL, ORACLE -> {
if (persistentEntity.hasIdentity()) {
// Oracle and MySql doesn't support a batch with returning generated ID: "DML Returning cannot be batched"
diff --git a/doc-examples/jdbc-sqlite/build.gradle b/doc-examples/jdbc-sqlite/build.gradle
index 5cceee1231f..e4c4505011c 100644
--- a/doc-examples/jdbc-sqlite/build.gradle
+++ b/doc-examples/jdbc-sqlite/build.gradle
@@ -13,5 +13,6 @@ dependencies {
implementation(projects.micronautDataJdbc)
implementation(mnSql.micronaut.jdbc.hikari)
runtimeOnly(mnLogging.logback.classic)
- testRuntimeOnly(mnSql.sqlite.jdbc)
+ testRuntimeOnly(mnSql.micronaut.jdbc.sqlite)
+ testImplementation(mnTest.junit.jupiter.params)
}
diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/BookRepository.java b/doc-examples/jdbc-sqlite/src/main/java/example/BookRepository.java
index 3fe85e889cb..e88ad9ff38f 100644
--- a/doc-examples/jdbc-sqlite/src/main/java/example/BookRepository.java
+++ b/doc-examples/jdbc-sqlite/src/main/java/example/BookRepository.java
@@ -4,8 +4,6 @@
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
-// There is no SQLite dialect yet; H2 is used as a substitute for DDL generation.
-// See: https://github.com/micronaut-projects/micronaut-data/pull/3820
-@JdbcRepository(dialect = Dialect.H2)
+@JdbcRepository(dialect = Dialect.SQLITE)
public interface BookRepository extends CrudRepository {
}
diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/Person.java b/doc-examples/jdbc-sqlite/src/main/java/example/Person.java
new file mode 100644
index 00000000000..656a5770914
--- /dev/null
+++ b/doc-examples/jdbc-sqlite/src/main/java/example/Person.java
@@ -0,0 +1,11 @@
+package example;
+
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+@MappedEntity
+public record Person(@Id @GeneratedValue @Nullable Long id, @NonNull String name, @NonNull Integer age) {
+}
diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/PersonRepository.java b/doc-examples/jdbc-sqlite/src/main/java/example/PersonRepository.java
new file mode 100644
index 00000000000..a73b0364ded
--- /dev/null
+++ b/doc-examples/jdbc-sqlite/src/main/java/example/PersonRepository.java
@@ -0,0 +1,9 @@
+package example;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface PersonRepository extends CrudRepository {
+}
diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java
deleted file mode 100644
index 10b706f291f..00000000000
--- a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2017-2025 original 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 example;
-
-import io.micronaut.data.connection.ConnectionCapabilities;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * {@link ConnectionCapabilities} implementation used by the SQLite JDBC example.
- */
-public final class SqliteConnectionCapabilities implements ConnectionCapabilities {
- private static final Logger LOG = LoggerFactory.getLogger(SqliteConnectionCapabilities.class);
- public static final String SQ_LITE = "SQLite";
-
- /**
- * Connection capabilities implementation for the SQLite JDBC example.
- *
- * SQLite connections do not support toggling read-only mode through JDBC, so
- * {@link ConnectionCapabilities.Capability#READ_ONLY} is reported as unsupported for SQLite URLs.
- * Other capabilities are treated as supported.
- */
- @Override
- public boolean supports(ConnectionCapabilities.Capability capability, Connection connection) {
- if (capability == ConnectionCapabilities.Capability.READ_ONLY) {
- try {
- return !connection.getMetaData().getDatabaseProductName().equalsIgnoreCase(SQ_LITE);
- } catch (SQLException e) {
- LOG.trace("Could not get metadata from connection", e);
- }
- }
- return true;
- }
-}
diff --git a/doc-examples/jdbc-sqlite/src/main/resources/META-INF/services/io.micronaut.data.connection.ConnectionCapabilities b/doc-examples/jdbc-sqlite/src/main/resources/META-INF/services/io.micronaut.data.connection.ConnectionCapabilities
deleted file mode 100644
index 56ee8efc1ad..00000000000
--- a/doc-examples/jdbc-sqlite/src/main/resources/META-INF/services/io.micronaut.data.connection.ConnectionCapabilities
+++ /dev/null
@@ -1 +0,0 @@
-example.SqliteConnectionCapabilities
diff --git a/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java b/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java
index 870e344f3b0..3afa8de3fd3 100644
--- a/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java
+++ b/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java
@@ -1,44 +1,18 @@
package example;
import io.micronaut.context.annotation.Property;
-import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
-import jakarta.inject.Inject;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import javax.sql.DataSource;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@Property(name = "datasources.default.url", value = "jdbc:sqlite:file:mydb?mode=memory&cache=shared")
@Property(name = "datasources.default.driver-class-name", value = "org.sqlite.JDBC")
-// There is no SQLite dialect yet; H2 is used as a substitute.
-// See: https://github.com/micronaut-projects/micronaut-data/pull/3820
-@Property(name = "datasources.default.dialect", value = "H2")
-@Property(name = "datasources.default.schema-generate", value = "NONE")
+@Property(name = "datasources.default.dialect", value = "SQLITE")
+@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP")
@MicronautTest(transactional = false)
class BookRepositoryTest {
- @Inject
- DataSource dataSource;
-
- @BeforeEach
- void setupSchema() throws SQLException {
- try (Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection();
- Statement statement = connection.createStatement()) {
- statement.execute("""
- CREATE TABLE IF NOT EXISTS book (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- title TEXT NOT NULL
- )
- """);
- }
- }
-
@Test
void sqliteConnectionCapabilitiesDoesNotApplyReadOnlyForSqliteConnections(BookRepository repository) {
assertDoesNotThrow(() -> repository.findAll().iterator().hasNext());
diff --git a/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java
new file mode 100644
index 00000000000..863dea93bf9
--- /dev/null
+++ b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java
@@ -0,0 +1,29 @@
+package example;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Property(name = "datasources.default.url", value = "jdbc:sqlite:file:mydb_person?mode=memory&cache=shared")
+@Property(name = "datasources.default.driver-class-name", value = "org.sqlite.JDBC")
+@Property(name = "datasources.default.dialect", value = "SQLITE")
+@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP")
+@MicronautTest(transactional = false)
+class PersonRepositoryTest {
+
+ @Test
+ void batchInsertSQLite(PersonRepository personRepository) {
+ long count = personRepository.count();
+ personRepository.save(new Person(null, "Sergio", 43));
+ assertEquals(1 + count, personRepository.count());
+ count = personRepository.count();
+ personRepository.saveAll(List.of(
+ new Person(null, "John Ternus", 51),
+ new Person(null, "Tim Cook", 65)
+ ));
+ assertEquals(2 + count, personRepository.count());
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8e409149ad8..224a88ab05c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,14 +2,14 @@
micronaut = "5.0.0-M25"
micronaut-platform = "5.0.0-M2"
micronaut-docs = "3.0.0"
-micronaut-gradle-plugin = "4.6.2"
+micronaut-gradle-plugin = "5.0.0-M1"
micronaut-testresources = "4.0.0-M1"
micronaut-azure = "5.13.0"
micronaut-reactor = "4.0.0-M2"
micronaut-rxjava3 = "4.0.0-M1"
micronaut-r2dbc = "7.0.1-M2"
-micronaut-sql = "7.0.1-M2"
+micronaut-sql = "7.0.1-M3"
micronaut-serde = "3.0.0-M7"
micronaut-spring = "6.0.0-M2"
micronaut-test = "5.0.0-M8"
diff --git a/settings.gradle b/settings.gradle
index a466897fb0e..dac3c0dfe31 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -145,6 +145,8 @@ include 'doc-examples:jooq-r2dbc-postgres'
include 'test-annotation-processor-java'
+include 'test-suite-data-jdbc-sqlite'
+
// benchmarks
include 'benchmarks:benchmark-micronaut-data-jpa'
include 'benchmarks:benchmark-micronaut-data-jdbc'
diff --git a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
index e16c1275782..0c213b7fdc8 100644
--- a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
+++ b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
@@ -65,8 +65,13 @@ As seen in the configuration above you should also configure the dialect. Althou
|api:data.model.query.builder.sql.Dialect#ORACLE[]
|Oracle 12c or above
+|api:data.model.query.builder.sql.Dialect#SQLITE[]
+|SQLite
+| io.micronaut.sql:micronaut-sql-sqlite
|===
+IMPORTANT: To use SQLite, add the dependency `io.micronaut.sql:micronaut-sql-sqlite`, which adds the https://github.com/xerial/sqlite-jdbc[SQLite JDBC Driver] and configures a custom api:data.connection.ConnectionCapabilities[] implementation.
+
IMPORTANT: The dialect setting in configuration does *not* replace the need to ensure the correct dialect is set at the repository. If the dialect is H2 in configuration, the repository should have `@JdbcRepository(dialect = Dialect.H2)` / `@R2dbcRepository(dialect = Dialect.H2)`. Because repositories are computed at compile time, the configuration value is not known at that time.
=== Tracing calls from Java to Oracle Database sessions
diff --git a/test-suite-data-jdbc-sqlite/build.gradle.kts b/test-suite-data-jdbc-sqlite/build.gradle.kts
new file mode 100644
index 00000000000..d92e6e59528
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/build.gradle.kts
@@ -0,0 +1,56 @@
+plugins {
+ `java-library`
+}
+
+dependencies {
+ // DI
+ testAnnotationProcessor(mn.micronaut.inject.java)
+
+ // SQLite
+ testRuntimeOnly(mnSql.micronaut.jdbc.sqlite)
+
+ // CONNECTION POOL
+// testRuntimeOnly(mnSql.micronaut.jdbc.hikari)
+ testRuntimeOnly(mnSql.micronaut.jdbc.tomcat)
+
+ // MULTITENANCY
+ testImplementation(mnMultitenancy.micronaut.multitenancy)
+
+ // REACTIVE
+ testImplementation(mnReactor.micronaut.reactor)
+ testImplementation(mnRxjava3.micronaut.rxjava3)
+
+ // TEST
+ testRuntimeOnly(mnTest.junit.jupiter.engine)
+ testRuntimeOnly(mnTest.junit.platform.launcher)
+ testImplementation(mnTest.micronaut.test.spock)
+ testImplementation(mnTest.micronaut.test.junit5)
+
+ // PERSISTENCE API
+ testImplementation(mnSql.jakarta.persistence.api)
+ testImplementation(libs.managed.javax.persistence.api)
+ testImplementation(libs.managed.jakarta.data.api)
+
+ // DATA
+ testAnnotationProcessor(projects.micronautDataProcessor)
+ testImplementation(projects.micronautDataJdbc)
+ testImplementation(projects.micronautDataTck)
+
+ // HTTP Client
+ testImplementation(mn.micronaut.http.client)
+
+ // Validation
+ testImplementation(mnValidation.micronaut.validation)
+ testAnnotationProcessor(mnValidation.micronaut.validation.processor)
+
+ // Serialization
+ testAnnotationProcessor(mnSerde.micronaut.serde.processor)
+ testImplementation(mnSerde.micronaut.serde.jackson)
+
+ // LOGGING
+ runtimeOnly(mnLogging.logback.classic)
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcMultitenancyTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcMultitenancyTest.java
new file mode 100644
index 00000000000..386eb43e594
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcMultitenancyTest.java
@@ -0,0 +1,70 @@
+package io.micronaut.data.jdbc;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.BeanContext;
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.jdbc.config.DataJdbcConfiguration;
+import io.micronaut.data.jdbc.operations.JdbcSchemaHandler;
+import io.micronaut.data.tck.tests.AbstractMultitenancySpec;
+import io.micronaut.inject.qualifiers.Qualifiers;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+abstract class AbstractJdbcMultitenancyTest extends AbstractMultitenancySpec {
+
+ @Override
+ public String sourcePrefix() {
+ return "datasources";
+ }
+
+ @Override
+ public long countDataSources(ApplicationContext context) {
+ return context.getBeansOfType(DataSource.class).size();
+ }
+
+ @Override
+ protected long getDataSourceBooksCount(BeanContext beanContext, String ds) {
+ return getBooksCount(beanContext.getBean(DataSource.class, Qualifiers.byName(ds)));
+ }
+
+ @Override
+ protected long getSchemaBooksCount(BeanContext beanContext, String schemaName) {
+ DataJdbcConfiguration conf = beanContext.getBean(DataJdbcConfiguration.class);
+ JdbcSchemaHandler schemaHandler = beanContext.getBean(JdbcSchemaHandler.class);
+ DataSource dataSource = beanContext.getBean(DataSource.class);
+ if (dataSource instanceof DelegatingDataSource delegatingDataSource) {
+ dataSource = delegatingDataSource.getTargetDataSource();
+ }
+ try (Connection connection = dataSource.getConnection()) {
+ schemaHandler.useSchema(connection, conf.getDialect(), schemaName);
+ try (PreparedStatement ps = connection.prepareStatement("select count(*) from book")) {
+ try (ResultSet resultSet = ps.executeQuery()) {
+ resultSet.next();
+ return resultSet.getLong(1);
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private long getBooksCount(DataSource ds) {
+ if (ds instanceof DelegatingDataSource delegatingDataSource) {
+ ds = delegatingDataSource.getTargetDataSource();
+ }
+ try (Connection connection = ds.getConnection()) {
+ try (PreparedStatement ps = connection.prepareStatement("select count(*) from book")) {
+ try (ResultSet resultSet = ps.executeQuery()) {
+ resultSet.next();
+ return resultSet.getLong(1);
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcTransactionTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcTransactionTest.java
new file mode 100644
index 00000000000..bc76243d08b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcTransactionTest.java
@@ -0,0 +1,50 @@
+package io.micronaut.data.jdbc;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.connection.ConnectionOperations;
+import io.micronaut.data.connection.jdbc.operations.DefaultDataSourceConnectionOperations;
+import io.micronaut.data.tck.tests.AbstractTransactionSpec;
+import io.micronaut.transaction.TransactionOperations;
+import io.micronaut.transaction.jdbc.DataSourceTransactionManager;
+
+import java.sql.Connection;
+
+abstract class AbstractJdbcTransactionTest extends AbstractTransactionSpec {
+
+ @Override
+ protected TransactionOperations getTransactionOperations() {
+ return applicationContext().getBean(DataSourceTransactionManager.class);
+ }
+
+ @Override
+ protected ConnectionOperations getConnectionOperations() {
+ return applicationContext().getBean(DefaultDataSourceConnectionOperations.class);
+ }
+
+ @Override
+ protected Runnable getNoTxCheck() {
+ DefaultDataSourceConnectionOperations connectionOperations = applicationContext().getBean(DefaultDataSourceConnectionOperations.class);
+ return () -> {
+ var status = connectionOperations.findConnectionStatus();
+ if (status.isEmpty()) {
+ return;
+ }
+ Connection connection = (Connection) status.get().getConnection();
+ try {
+ org.junit.jupiter.api.Assertions.assertTrue(connection.getAutoCommit());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+ private ApplicationContext applicationContext() {
+ try {
+ var field = AbstractTransactionSpec.class.getDeclaredField("context");
+ field.setAccessible(true);
+ return (ApplicationContext) field.get(this);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Unable to access AbstractTransactionSpec context", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractManualSchemaTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractManualSchemaTest.java
new file mode 100644
index 00000000000..ea7f2e1d5a2
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractManualSchemaTest.java
@@ -0,0 +1,152 @@
+package io.micronaut.data.jdbc;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.tck.entities.Patient;
+import io.micronaut.data.tck.repositories.PatientRepository;
+import io.micronaut.inject.qualifiers.Qualifiers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * This is the base test when need to create schema manually and test some features for jdbc. This was created to test getting auto generated ids when id is not first column,
+ * but can be used for other purposes.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class AbstractManualSchemaTest {
+
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ protected final ApplicationContext context = ApplicationContext.run(new java.util.HashMap<>(getProperties()));
+
+ protected final DataSource dataSource = DelegatingDataSource.unwrapDataSource(
+ context.getBean(DataSource.class, Qualifiers.byName("default"))
+ );
+
+ protected abstract PatientRepository getPatientRepository();
+
+ protected abstract java.util.Map getProperties();
+
+ protected List createStatements() {
+ return Arrays.asList("CREATE TABLE patient (name TEXT,id INTEGER PRIMARY KEY,history TEXT,doctor_notes TEXT,appointments TEXT);");
+ }
+
+ protected List dropStatements() {
+ return Arrays.asList("DROP TABLE patient");
+ }
+
+ protected String insertStatement() {
+ return "INSERT INTO patient (name, history, doctor_notes) VALUES (?, ?, ?)";
+ }
+
+ protected void createSchema() {
+ try {
+ var conn = dataSource.getConnection();
+ createStatements().forEach(st -> {
+ try {
+ conn.prepareStatement(st).executeUpdate();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (Exception e) {
+ log.warn("Error creating schema manually: {}", e.getMessage());
+ }
+ }
+
+ protected void dropSchema() {
+ try {
+ var conn = dataSource.getConnection();
+ dropStatements().forEach(st -> {
+ try {
+ conn.prepareStatement(st).executeUpdate();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (Exception e) {
+ log.warn("Error dropping schema manually: {}", e.getMessage());
+ }
+ }
+
+ private void insertRecord(String name, String history, String doctorNotes) {
+ try {
+ var conn = dataSource.getConnection();
+ var insertStmt = conn.prepareStatement(insertStatement());
+ insertStmt.setString(1, name);
+ insertStmt.setString(2, history);
+ insertStmt.setString(3, doctorNotes);
+ int inserted = insertStmt.executeUpdate();
+ assertEquals(1, inserted);
+ } catch (Exception e) {
+ log.warn("Error inserting record manually: {}", e.getMessage());
+ }
+ }
+
+ @AfterAll
+ void closeContext() {
+ context.close();
+ }
+
+ @Test
+ void testSaveAndLoadRecordWhenIdNotFirstFieldInTheTable() {
+ createSchema();
+ try {
+ Patient patient = new Patient();
+ patient.setName("Patient1");
+ patient.setHistory("Enter some details");
+ getPatientRepository().save(patient);
+
+ var optPatient = getPatientRepository().findById(patient.getId());
+
+ assertTrue(optPatient.isPresent());
+ assertEquals(patient.getId(), optPatient.get().getId());
+ } finally {
+ dropSchema();
+ }
+ }
+
+ @Test
+ @Disabled("FORMAT JSON")
+ void testManualInsertAndDtoRetrieval() {
+ createSchema();
+ try {
+ String name = "pt1";
+ String history = "flu";
+ String doctorNotes = "mild";
+ List appointments = List.of("Dr1 April 2022", "Dr2 June 2022");
+ insertRecord(name, history, doctorNotes);
+ getPatientRepository().updateAppointmentsByName(name, appointments);
+
+ var patientDtos = getPatientRepository().findAllByNameWithQuery(name);
+
+ assertEquals(1, patientDtos.size());
+ assertEquals(name, patientDtos.get(0).getName());
+ assertEquals(history, patientDtos.get(0).getHistory());
+ assertEquals(doctorNotes, patientDtos.get(0).getDoctorNotes());
+ assertEquals(appointments, patientDtos.get(0).getAppointments());
+
+ var optPatientDto = getPatientRepository().findByNameWithQuery(name);
+
+ assertTrue(optPatientDto.isPresent());
+ var patientDto = optPatientDto.get();
+ assertEquals(name, patientDto.getName());
+ assertEquals(history, patientDto.getHistory());
+ assertEquals(doctorNotes, patientDto.getDoctorNotes());
+ assertEquals(appointments, patientDto.getAppointments());
+ } finally {
+ dropSchema();
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/CallableStatementTupleMapperTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/CallableStatementTupleMapperTest.java
new file mode 100644
index 00000000000..b0c7d8c66e0
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/CallableStatementTupleMapperTest.java
@@ -0,0 +1,61 @@
+package io.micronaut.data.jdbc;
+
+import io.micronaut.data.jdbc.mapper.CallableStatementTupleMapper;
+import jakarta.persistence.Tuple;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Proxy;
+import java.sql.CallableStatement;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CallableStatementTupleMapperTest {
+
+ @Test
+ void mapsAllRegisteredOutParametersIntoTupleOrder() {
+ CallableStatement callableStatement = (CallableStatement) Proxy.newProxyInstance(
+ CallableStatement.class.getClassLoader(),
+ new Class[]{CallableStatement.class},
+ (proxy, method, args) -> {
+ if ("getObject".equals(method.getName()) && args != null && args.length == 1) {
+ if (Integer.valueOf(4).equals(args[0])) {
+ return "Oracle DTO Title";
+ }
+ if (Integer.valueOf(5).equals(args[0])) {
+ return 777;
+ }
+ }
+ Class> returnType = method.getReturnType();
+ if (returnType == boolean.class) {
+ return false;
+ }
+ if (returnType == byte.class || returnType == short.class || returnType == int.class || returnType == long.class) {
+ return 0;
+ }
+ if (returnType == float.class || returnType == double.class) {
+ return 0.0;
+ }
+ if (returnType == char.class) {
+ return '\0';
+ }
+ return null;
+ }
+ );
+ LinkedHashMap columnIndexesByName = new LinkedHashMap<>();
+ columnIndexesByName.put("title", 4);
+ columnIndexesByName.put("total_pages", 5);
+ var mapper = new CallableStatementTupleMapper(
+ io.micronaut.core.convert.ConversionService.SHARED,
+ columnIndexesByName
+ );
+
+ Tuple tuple = mapper.map(callableStatement, Tuple.class);
+
+ assertArrayEquals(new Object[]{"Oracle DTO Title", 777}, tuple.toArray());
+ assertEquals("Oracle DTO Title", tuple.get("title", String.class));
+ assertEquals(777, tuple.get("total_pages", Integer.class));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/SchemaCreateDropTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/SchemaCreateDropTest.java
new file mode 100644
index 00000000000..9c267c43afa
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/SchemaCreateDropTest.java
@@ -0,0 +1,39 @@
+package io.micronaut.data.jdbc;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.repositories.BookRepository;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class SchemaCreateDropTest {
+
+ protected final ApplicationContext context = ApplicationContext.run(new java.util.HashMap<>(getProperties()));
+
+ protected abstract BookRepository getBookRepository();
+
+ protected abstract java.util.Map getProperties();
+
+ @AfterAll
+ void closeContext() {
+ context.close();
+ }
+
+ @Test
+ void bookIsCreated() {
+ Book book = new Book();
+ book.setTitle("title");
+ getBookRepository().save(book);
+
+ assertEquals(1, getBookRepository().count());
+ }
+
+ @Test
+ void bookWasDropped() {
+ assertEquals(0, getBookRepository().count());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/AbstractSQLiteRepositoryBehaviorTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/AbstractSQLiteRepositoryBehaviorTest.java
new file mode 100644
index 00000000000..e4fb2b577fe
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/AbstractSQLiteRepositoryBehaviorTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.Sort;
+import io.micronaut.data.tck.entities.AuthorBooksDto;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.BookDto;
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.data.tck.entities.Student;
+import io.micronaut.data.tck.entities.embedded.BookEntity;
+import io.micronaut.data.tck.entities.embedded.BookState;
+import io.micronaut.data.tck.entities.embedded.ResourceEntity;
+import io.micronaut.inject.qualifiers.Qualifiers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import static io.micronaut.data.tck.repositories.PersonRepository.Specifications.findNameSubqueryEq;
+import static io.micronaut.data.tck.repositories.PersonRepository.Specifications.findNameSubqueryIn;
+import static io.micronaut.data.tck.repositories.PersonRepository.Specifications.nameEqualsCaseInsensitive;
+import static io.micronaut.data.tck.repositories.PersonRepository.Specifications.subqueriesWithJoinReferencingOuter;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class AbstractSQLiteRepositoryBehaviorTest implements SQLiteTestingPropertyProvider {
+
+ private ApplicationContext context;
+
+ @BeforeAll
+ void setupContext() {
+ context = ApplicationContext.run(new HashMap<>(getProperties()));
+ }
+
+ @AfterEach
+ void cleanup() {
+ cleanupData();
+ }
+
+ @AfterAll
+ void closeContext() {
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Override
+ public Map getProperties() {
+ return SQLiteTestingPropertyProvider.super.getProperties();
+ }
+
+ @Test
+ void testSubqueryWithJoin() {
+ saveSampleBooks();
+
+ List books = context.getBean(SQLiteBookRepository.class).findAll(subqueriesWithJoinReferencingOuter());
+
+ assertEquals(6, books.size());
+ }
+
+ @Test
+ void testSubqueryIn() {
+ savePersons(List.of("Jeff", "James"));
+
+ var person = context.getBean(SQLitePersonRepository.class).findOne(findNameSubqueryIn("James"));
+
+ assertNotNull(person);
+ }
+
+ @Test
+ void testSubqueryEq() {
+ savePersons(List.of("Jeff", "James"));
+
+ var person = context.getBean(SQLitePersonRepository.class).findOne(findNameSubqueryEq("James"));
+
+ assertNotNull(person);
+ }
+
+ @Test
+ void testCriteriaLowerSelect() {
+ savePersons(List.of("Jeff", "James"));
+
+ var person = context.getBean(SQLitePersonRepository.class).findOne(nameEqualsCaseInsensitive("james"));
+
+ assertTrue(person.isPresent());
+ }
+
+ @Test
+ void testManualJoiningOnManyEndedAssociation() {
+ saveSampleBooks();
+
+ var author = context.getBean(SQLiteBookService.class).findByName("Stephen King");
+
+ assertNotNull(author);
+ assertEquals("Stephen King", author.getName());
+ assertEquals(2, author.getBooks().size());
+ assertTrue(author.getBooks().stream().anyMatch(book -> "The Stand".equals(book.getTitle())));
+ assertTrue(author.getBooks().stream().anyMatch(book -> "Pet Cemetery".equals(book.getTitle())));
+ }
+
+ @Test
+ void testSqlMappingFunction() {
+ saveSampleBooks();
+
+ var authorRepository = context.getBean(SQLiteAuthorRepository.class);
+
+ var book = authorRepository.testReadSingleProperty("The Stand", 700);
+ assertNotNull(book);
+ assertEquals("Stephen King", book.getAuthor().getName());
+
+ book = authorRepository.testReadAssociatedEntity("The Stand", 700);
+ assertNotNull(book);
+ assertEquals("Stephen King", book.getAuthor().getName());
+ assertNotNull(book.getAuthor().getId());
+
+ book = authorRepository.testReadDTO("The Stand", 700);
+ assertNotNull(book);
+ assertEquals("Stephen King", book.getAuthor().getName());
+ }
+
+ @Test
+ void findByEmbeddedEntityField() {
+ var bookEntityRepository = context.getBean(SQLiteBookEntityRepository.class);
+ BookEntity bookEntity = new BookEntity(1L, new ResourceEntity<>("1984", BookState.BORROWED));
+
+ bookEntityRepository.save(bookEntity);
+ var result = bookEntityRepository.findAllByResourceState(BookState.BORROWED);
+
+ assertFalse(result.isEmpty());
+ bookEntityRepository.deleteAll();
+ }
+
+ @Test
+ void testJoinPaginationXxx() {
+ Student denis = new Student("Denis");
+ Student josh = new Student("Josh");
+ Student kevin = new Student("Kevin");
+ Book book1 = new Book();
+ book1.setTitle("The Stand");
+ book1.setStudents(new HashSet<>(List.of(denis, josh)));
+ Book book2 = new Book();
+ book2.setTitle("Pet Cemetery");
+ book2.setStudents(new HashSet<>(List.of(kevin)));
+ Book book3 = new Book();
+ book3.setTitle("Along Came a Spider");
+ book3.setStudents(new HashSet<>(List.of(kevin, josh)));
+ var bookRepository = context.getBean(SQLiteBookRepository.class);
+ bookRepository.save(book1);
+ bookRepository.save(book2);
+ bookRepository.save(book3);
+ List names = List.of(denis.getName(), josh.getName());
+
+ io.micronaut.data.model.Page page = bookRepository.findAllByStudentsNameIn(
+ names,
+ Pageable.from(0, 10, Sort.of(Sort.Order.asc("title")))
+ );
+
+ assertEquals(page.getTotalSize(), page.getContent().size());
+ assertEquals(2, page.getTotalSize());
+ assertEquals(List.of("Along Came a Spider", "The Stand"), sortedTitles(page.getContent()));
+ assertEquals(List.of("Josh", "Kevin"), sortedStudentNames(page.getContent().get(0)));
+ assertEquals(List.of("Denis", "Josh"), sortedStudentNames(page.getContent().get(1)));
+
+ Pageable pageable = Pageable.from(0, 1, Sort.of(Sort.Order.asc("title")));
+ page = bookRepository.findAllByStudentsNameIn(names, pageable);
+
+ assertEquals(2, page.getTotalSize());
+ assertEquals(1, page.getContent().size());
+ assertEquals("Along Came a Spider", page.getContent().get(0).getTitle());
+ assertEquals(List.of("Josh", "Kevin"), sortedStudentNames(page.getContent().get(0)));
+
+ pageable = pageable.next();
+ page = bookRepository.findAllByStudentsNameIn(names, pageable);
+
+ assertEquals(2, page.getTotalSize());
+ assertEquals(1, page.getContent().size());
+ assertEquals("The Stand", page.getContent().get(0).getTitle());
+ assertEquals(List.of("Denis", "Josh"), sortedStudentNames(page.getContent().get(0)));
+
+ pageable = pageable.next();
+ page = bookRepository.findAllByStudentsNameIn(names, pageable);
+
+ assertEquals(2, page.getTotalSize());
+ assertEquals(0, page.getContent().size());
+
+ pageable = pageable.previous();
+ page = bookRepository.findAllByStudentsNameIn(names, pageable);
+
+ assertEquals(2, page.getTotalSize());
+ assertEquals(1, page.getContent().size());
+ assertEquals("The Stand", page.getContent().get(0).getTitle());
+ assertEquals(List.of("Denis", "Josh"), sortedStudentNames(page.getContent().get(0)));
+ }
+
+ private void savePersons(List names) {
+ List people = new ArrayList<>();
+ for (String name : names) {
+ Person person = new Person();
+ person.setName(name);
+ people.add(person);
+ }
+ context.getBean(SQLitePersonRepository.class).saveAll(people);
+ }
+
+ private void saveSampleBooks() {
+ context.getBean(SQLiteBookRepository.class).saveAuthorBooks(List.of(
+ new AuthorBooksDto("Stephen King", List.of(
+ new BookDto("The Stand", 1000),
+ new BookDto("Pet Cemetery", 400)
+ )),
+ new AuthorBooksDto("James Patterson", List.of(
+ new BookDto("Along Came a Spider", 300),
+ new BookDto("Double Cross", 300)
+ )),
+ new AuthorBooksDto("Don Winslow", List.of(
+ new BookDto("The Power of the Dog", 600),
+ new BookDto("The Border", 700)
+ ))
+ ));
+ }
+
+ private void cleanupData() {
+ var studentRepository = context.getBean(SQLiteStudentRepository.class);
+ var bookRepository = context.getBean(SQLiteBookRepository.class);
+ var authorRepository = context.getBean(SQLiteAuthorRepository.class);
+ studentRepository.deleteAll();
+ try (Connection connection = dataSource().getConnection();
+ Statement statement = connection.createStatement()) {
+ statement.executeUpdate("DELETE FROM \"book_student\"");
+ } catch (SQLException e) {
+ throw new IllegalStateException("Failed to clean up book_student rows", e);
+ }
+ bookRepository.deleteAll();
+ authorRepository.deleteAll();
+ context.getBean(SQLitePersonRepository.class).deleteAll();
+ context.getBean(SQLiteIntervalRepository.class).deleteAll();
+ }
+
+ private DataSource dataSource() {
+ DataSource dataSource = context.getBean(DataSource.class, Qualifiers.byName("default"));
+ return DelegatingDataSource.unwrapDataSource(dataSource);
+ }
+
+ private List sortedTitles(List books) {
+ return books.stream()
+ .map(Book::getTitle)
+ .sorted()
+ .toList();
+ }
+
+ private List sortedStudentNames(Book book) {
+ List names = new ArrayList<>();
+ for (Student student : book.getStudents()) {
+ names.add(student.getName());
+ }
+ names.sort(Comparator.naturalOrder());
+ return names;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntity.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntity.java
new file mode 100644
index 00000000000..b10ee964ab9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntity.java
@@ -0,0 +1,17 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+
+import java.util.List;
+
+@MappedEntity
+public record CascadeEntity(
+ @Id @GeneratedValue Long id,
+ @Relation(value = Relation.Kind.ONE_TO_MANY, cascade = Relation.Cascade.ALL, mappedBy = "entity")
+ List subEntityAs,
+ @Relation(value = Relation.Kind.ONE_TO_MANY, cascade = Relation.Cascade.ALL, mappedBy = "entity")
+ List subEntityBs
+){}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntityRepository.java
new file mode 100644
index 00000000000..ea8fc194e44
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntityRepository.java
@@ -0,0 +1,11 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+@Join("subEntityAs")
+@Join("subEntityBs")
+interface CascadeEntityRepository extends CrudRepository {}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityA.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityA.java
new file mode 100644
index 00000000000..5ac315dde17
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityA.java
@@ -0,0 +1,16 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import org.jspecify.annotations.Nullable;
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+
+@MappedEntity
+public record CascadeSubEntityA(
+ @Id @GeneratedValue Long id,
+ @Nullable Integer data,
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ @Nullable
+ CascadeEntity entity
+){}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityB.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityB.java
new file mode 100644
index 00000000000..c897f665fb8
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityB.java
@@ -0,0 +1,16 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import org.jspecify.annotations.Nullable;
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+
+@MappedEntity
+public record CascadeSubEntityB(
+ @Id @GeneratedValue Long id,
+ @Nullable Integer data,
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ @Nullable
+ CascadeEntity entity
+){}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepository.java
new file mode 100644
index 00000000000..8bc8396942b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepository.java
@@ -0,0 +1,22 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import org.jspecify.annotations.NonNull;
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Challenge;
+
+import jakarta.validation.constraints.NotNull;
+import java.util.Optional;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface ChallengeRepository extends CrudRepository {
+
+ @Override
+ @Join(value = "authentication")
+ @Join(value = "authentication.device")
+ @Join(value = "authentication.device.user")
+ @NonNull
+ Optional findById(@NonNull @NotNull Long id);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepositoryTest.java
new file mode 100644
index 00000000000..ed286a16cc9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepositoryTest.java
@@ -0,0 +1,20 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+@MicronautTest(rollback = false)
+@SQLiteDBProperties
+class ChallengeRepositoryTest {
+
+ @Inject
+ ChallengeRepository repository;
+
+ @Test
+ void queryWithMultipleJoinsIsSuccessful() {
+ assertDoesNotThrow(() -> repository.findById(1L));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/EscapeIdentifiersTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/EscapeIdentifiersTest.java
new file mode 100644
index 00000000000..bff5f380db9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/EscapeIdentifiersTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest(rollback = false)
+@SQLiteDBProperties
+class EscapeIdentifiersTest {
+
+ @Inject
+ SQLiteTableRatingsRepository repository;
+
+ @AfterEach
+ void cleanup() {
+ repository.deleteAll();
+ }
+
+ @Test
+ void testSaveOne() {
+ TableRatings ratings = new TableRatings(10);
+ repository.save(ratings);
+
+ assertNotNull(ratings.getId());
+ assertTrue(repository.findById(ratings.getId()).isPresent());
+ assertTrue(repository.existsById(ratings.getId()));
+ assertEquals(1, repository.count());
+ assertEquals(1, repository.findAll().size());
+ }
+
+ @Test
+ void testSaveMany() {
+ TableRatings p1 = repository.save(new TableRatings(20));
+ TableRatings p2 = repository.save(new TableRatings(30));
+ List ratings = List.of(p1, p2);
+
+ assertTrue(ratings.stream().allMatch(r -> r.getId() != null));
+ assertTrue(ratings.stream().allMatch(r -> repository.findById(r.getId()).isPresent()));
+ assertEquals(2, repository.findAll().size());
+ assertEquals(2, repository.count());
+ }
+
+ @Test
+ void testDeleteById() {
+ repository.save(new TableRatings(20));
+ repository.save(new TableRatings(30));
+ TableRatings rating = repository.findByRating(20);
+
+ assertNotNull(rating);
+ assertEquals(20, rating.getRating());
+ assertTrue(repository.findById(rating.getId()).isPresent());
+
+ repository.deleteById(rating.getId());
+
+ assertTrue(repository.findById(rating.getId()).isEmpty());
+ assertEquals(1, repository.count());
+ }
+
+ @Test
+ void testUpdateOne() {
+ repository.save(new TableRatings(10));
+ TableRatings ratings = repository.findByRating(10);
+
+ assertNotNull(ratings);
+
+ repository.updateRating(ratings.getId(), 15);
+
+ assertNull(repository.findByRating(10));
+ assertNotNull(repository.findByRating(15));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/JpaTransientPropertyTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/JpaTransientPropertyTest.java
new file mode 100644
index 00000000000..920d9a219aa
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/JpaTransientPropertyTest.java
@@ -0,0 +1,23 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+@MicronautTest
+@SQLiteDBProperties
+class JpaTransientPropertyTest {
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @Test
+ void testJpaSpecificationExecutorWithTransientProperties() {
+ PredicateSpecification spec = (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("title"), "Random title");
+ assertDoesNotThrow(() -> bookRepository.findAll(spec));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/LocalDateTimeTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/LocalDateTimeTest.java
new file mode 100644
index 00000000000..78f14d9731f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/LocalDateTimeTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.BasicTypes;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@MicronautTest
+@SQLiteDBProperties
+class LocalDateTimeTest {
+
+ @Inject
+ SQLiteBasicTypesRepository repository;
+
+ @Test
+ void testLocalDateTimeDst() throws Exception {
+ ZoneId zoneId = ZoneId.of("Europe/Berlin");
+ ZonedDateTime dstChange = LocalDateTime.of(2020, 3, 29, 2, 0).atZone(zoneId);
+ BasicTypes basicTypes = new BasicTypes();
+ basicTypes.setZonedDateTime(dstChange);
+ repository.save(basicTypes);
+
+ assertEquals(dstChange, repository.findById(1L).orElseThrow().getZonedDateTime().withZoneSameInstant(zoneId));
+ }
+
+ @Test
+ void testLocalDateTimeUtc() throws Exception {
+ ZoneId utc = ZoneId.of("UTC");
+ ZonedDateTime dstChange = LocalDateTime.of(2020, 3, 29, 2, 0).atZone(utc);
+ BasicTypes basicTypes = new BasicTypes();
+ basicTypes.setZonedDateTime(dstChange);
+ repository.save(basicTypes);
+
+ assertEquals(dstChange, repository.findById(basicTypes.getMyId()).orElseThrow().getZonedDateTime().withZoneSameInstant(utc));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/MultipleDataSourceTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/MultipleDataSourceTest.java
new file mode 100644
index 00000000000..c278cc61b5d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/MultipleDataSourceTest.java
@@ -0,0 +1,225 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.event.ApplicationEventPublisher;
+import io.micronaut.data.connection.jdbc.advice.ContextualConnection;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.jdbc.runtime.JdbcOperations;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import io.micronaut.transaction.annotation.Transactional;
+import io.micronaut.transaction.annotation.TransactionalEventListener;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest(packages = "io.micronaut.data.tck.entities", transactional = false)
+@SQLiteDBProperties
+@Property(name = "datasources.other.name", value = "otherdb")
+@Property(name = "datasources.other.schema-generate", value = "CREATE_DROP")
+@Property(name = "datasources.other.dialect", value = "SQLITE")
+@Property(name = "datasources.other.db-type", value = "sqlite")
+@Property(name = "datasources.other.packages", value = "io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities,io.micronaut.data.jdbc.sqlite")
+@Property(name = "datasources.other.driverClassName", value = "org.sqlite.JDBC")
+@Property(name = "datasources.other.url", value = "jdbc:sqlite:file:other?mode=memory&cache=shared")
+@Property(name = "datasources.other.username", value = "")
+@Property(name = "datasources.other.password", value = "")
+class MultipleDataSourceTest {
+
+ @Inject
+ SQLitePersonRepository personRepository;
+
+ @Inject
+ OtherPersonRepository otherPersonRepository;
+
+ @Inject
+ DbService service;
+
+ @AfterEach
+ void cleanup() {
+ personRepository.deleteAll();
+ otherPersonRepository.deleteAll();
+ }
+
+ @Test
+ void testMultipleDataSources() {
+ personRepository.save(person("Fred"));
+ personRepository.save(person("Bob"));
+
+ assertEquals(2, personRepository.count());
+ assertEquals(0, otherPersonRepository.count());
+
+ otherPersonRepository.save(person("Joe"));
+
+ assertEquals("Joe", otherPersonRepository.findAll().get(0).getName());
+
+ otherPersonRepository.saveTwoOtherDb(person("One"), person("Two"));
+ otherPersonRepository.saveTwoOtherDb2(person("Three"), person("Four"));
+
+ assertEquals(5, otherPersonRepository.count());
+ assertDoesNotThrow(service::save);
+ }
+
+ private static Person person(String name) {
+ Person person = new Person();
+ person.setName(name);
+ return person;
+ }
+
+ @Singleton
+ static class DbService {
+
+ private final Connection defaultConnection;
+ private final Connection otherConnection;
+ private final SQLitePersonRepository personRepository;
+ private final OtherPersonRepository otherPersonRepository;
+ private final ApplicationEventPublisher eventPublisher;
+ private final List personsSaved = new ArrayList<>();
+
+ DbService(Connection defaultConnection,
+ @Named("other") Connection otherConnection,
+ SQLitePersonRepository personRepository,
+ OtherPersonRepository otherPersonRepository,
+ ApplicationEventPublisher eventPublisher) {
+ this.defaultConnection = defaultConnection;
+ this.otherConnection = otherConnection;
+ this.personRepository = personRepository;
+ this.otherPersonRepository = otherPersonRepository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @TransactionalEventListener
+ void savedListenerDefault(Person person) {
+ add(person);
+ }
+
+ @TransactionalEventListener(transactionManager = "other")
+ void savedListenerOther(Person person) {
+ add(person);
+ }
+
+ private void add(Person person) {
+ boolean alreadyAdded = personsSaved.stream().anyMatch(saved -> saved.getId().equals(person.getId()));
+ if (!alreadyAdded) {
+ personsSaved.add(person);
+ }
+ }
+
+ void save() {
+ if (!personsSaved.isEmpty()) {
+ throw new IllegalStateException("Expected no saved persons before transaction test");
+ }
+ saveTx1();
+ if (personsSaved.size() != 2) {
+ throw new IllegalStateException("Expected two transactional events");
+ }
+ if (!"Two".equals(personsSaved.get(0).getName())) {
+ throw new IllegalStateException("Expected other transaction event first");
+ }
+ if (!"One".equals(personsSaved.get(1).getName())) {
+ throw new IllegalStateException("Expected default transaction event second");
+ }
+ }
+
+ @Transactional
+ void saveTx1() {
+ Person person = person("One");
+ personRepository.save(person);
+ eventPublisher.publishEvent(person);
+ saveTx2();
+ if (personsSaved.size() != 1) {
+ throw new IllegalStateException("Expected nested transaction event to be visible before outer commit");
+ }
+ if (!"Two".equals(personsSaved.get(0).getName())) {
+ throw new IllegalStateException("Expected nested transaction event first");
+ }
+ }
+
+ @Transactional("other")
+ void saveTx2() {
+ Person person = person("Two");
+ otherPersonRepository.save(person);
+ eventPublisher.publishEvent(person);
+ Connection unwrappedDefaultConnection = unwrap(defaultConnection);
+ Connection unwrappedOtherConnection = unwrap(otherConnection);
+ if (unwrappedDefaultConnection == unwrappedOtherConnection) {
+ throw new IllegalStateException("Expected separate data source connections");
+ }
+ }
+
+ private Connection unwrap(Connection connection) {
+ try {
+ return connection.unwrap(Connection.class);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Unable to unwrap connection", e);
+ }
+ }
+ }
+
+ @JdbcRepository(dataSource = "other", dialect = Dialect.SQLITE)
+ static abstract class OtherPersonRepository implements CrudRepository {
+
+ private final JdbcOperations jdbcOperations;
+ private final Connection otherConnection;
+
+ OtherPersonRepository(@Named("other") JdbcOperations jdbcOperations,
+ @Named("default") Connection defaultConnection,
+ @Named("other") Connection otherConnection) {
+ this.jdbcOperations = jdbcOperations;
+ this.otherConnection = otherConnection;
+ assertTrue(defaultConnection instanceof ContextualConnection);
+ assertTrue(otherConnection instanceof ContextualConnection);
+ }
+
+ @Transactional("other")
+ void saveTwoOtherDb(Person one, Person two) {
+ saveTwo(one, two);
+ }
+
+ @Transactional(transactionManager = "other")
+ void saveTwoOtherDb2(Person one, Person two) {
+ saveTwo(one, two);
+ }
+
+ void saveTwo(Person one, Person two) {
+ Connection jdbcOperationsConnection = unwrap(jdbcOperations.getConnection());
+ Connection unwrappedOtherConnection = unwrap(otherConnection);
+ if (jdbcOperationsConnection != unwrappedOtherConnection) {
+ throw new IllegalStateException("Expected JDBC operations to use the other connection");
+ }
+ jdbcOperations.prepareStatement("INSERT INTO `person` (`enabled`,`age`,`name`) VALUES (?,?,?)", statement -> {
+ statement.setBoolean(1, one.isEnabled());
+ statement.setInt(2, one.getAge());
+ statement.setString(3, one.getName());
+ statement.addBatch();
+ statement.clearParameters();
+ statement.setBoolean(1, two.isEnabled());
+ statement.setInt(2, two.getAge());
+ statement.setString(3, two.getName());
+ statement.addBatch();
+ return statement.executeBatch();
+ });
+ }
+
+ private Connection unwrap(Connection connection) {
+ try {
+ return connection.unwrap(Connection.class);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Unable to unwrap connection", e);
+ }
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/NoIdEntity.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/NoIdEntity.java
new file mode 100644
index 00000000000..4065160da39
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/NoIdEntity.java
@@ -0,0 +1,39 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+
+@MappedEntity
+public class NoIdEntity {
+
+ @Id
+ private Long uid;
+
+ private String name;
+
+ private String id;
+
+ public Long getUid() {
+ return uid;
+ }
+
+ public void setUid(Long uid) {
+ this.uid = uid;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/OrganizationRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/OrganizationRepository.java
new file mode 100644
index 00000000000..6d79efdb887
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/OrganizationRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.jdbc.entities.Organization;
+
+import java.util.UUID;
+
+public interface OrganizationRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRecordRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRecordRepository.java
new file mode 100644
index 00000000000..316ffd3859c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRecordRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.AccountRecordRepository;
+import io.micronaut.data.tck.repositories.AccountRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteAccountRecordRepository extends AccountRecordRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRepository.java
new file mode 100644
index 00000000000..7e3a82f6b49
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Account;
+import io.micronaut.data.tck.repositories.AccountRepository;
+import io.micronaut.data.tck.repositories.ArraysEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteAccountRepository extends AccountRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysEntityRepository.java
new file mode 100644
index 00000000000..048909e6887
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysEntityRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.ArraysEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteArraysEntityRepository extends ArraysEntityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysTest.java
new file mode 100644
index 00000000000..b9e5ebbb799
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysTest.java
@@ -0,0 +1,12 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+@Disabled("SQLite does not support SQL ARRAY columns")
+class SQLiteArraysTest {
+
+ @Test
+ void sqliteDoesNotSupportSqlArrayColumns() {
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncBookRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncBookRepository.java
new file mode 100644
index 00000000000..1012c27ffe7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncBookRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-2024 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.BookAsyncRepository;
+import io.micronaut.data.tck.repositories.PersonAsyncRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteAsyncBookRepository extends BookAsyncRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncPersonRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncPersonRepository.java
new file mode 100644
index 00000000000..86420f95ea7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncPersonRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.PersonAsyncRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteAsyncPersonRepository extends PersonAsyncRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncRepositoryTest.java
new file mode 100644
index 00000000000..56826aeca53
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncRepositoryTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.repositories.BookAsyncRepository;
+import io.micronaut.data.tck.repositories.PersonAsyncRepository;
+import io.micronaut.data.tck.tests.AbstractAsyncRepositorySpec;
+
+import java.util.Map;
+
+public class SQLiteAsyncRepositoryTest extends AbstractAsyncRepositorySpec implements SQLiteTestingPropertyProvider {
+
+ @Override
+ public Map getProperties() {
+ return SQLiteTestingPropertyProvider.super.getProperties();
+ }
+
+ @Override
+ public PersonAsyncRepository getPersonRepository() {
+ return getApplicationContext().getBean(SQLiteAsyncPersonRepository.class);
+ }
+
+ @Override
+ public BookAsyncRepository getBookRepository() {
+ return getApplicationContext().getBean(SQLiteAsyncBookRepository.class);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAuthorRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAuthorRepository.java
new file mode 100644
index 00000000000..061669d6ab0
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAuthorRepository.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.jdbc.mapper.SqlResultConsumer;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.Author;
+import io.micronaut.data.tck.entities.AuthorDTO;
+import io.micronaut.data.tck.entities.Book;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteAuthorRepository extends io.micronaut.data.tck.repositories.AuthorRepository {
+
+ @Query("select *, author.name as author_name, author.nick_name as author_nick_name from book as book inner join author as author where book.title = :title and book.total_pages > :pages")
+ Book customSearch(String title, int pages, SqlResultConsumer mappingFunction);
+
+ default Book testReadSingleProperty(String title, int pages) {
+ return customSearch(title, pages, (book, context) -> {
+ Author author = new Author();
+ author.setName(context.readString("author_name"));
+ book.setAuthor(author);
+ });
+ }
+
+ default Book testReadAssociatedEntity(String title, int pages) {
+ return customSearch(title, pages, (book, context) -> book.setAuthor(context.readEntity("author_", Author.class)));
+ }
+
+ default Book testReadDTO(String title, int pages) {
+ return customSearch(title, pages, (book, context) ->
+ {
+ AuthorDTO dto = context.readDTO("author_", Author.class, AuthorDTO.class);
+ Author author = new Author();
+ author.setName(dto.getName());
+ book.setAuthor(author);
+ }
+ );
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesRepository.java
new file mode 100644
index 00000000000..bbe6da58d5f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.BasicTypesRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteBasicTypesRepository extends BasicTypesRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesTest.java
new file mode 100644
index 00000000000..a877cce7e8b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesTest.java
@@ -0,0 +1,91 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.MappedProperty;
+import io.micronaut.data.model.DataType;
+import io.micronaut.data.model.PersistentEntity;
+import io.micronaut.data.tck.entities.BasicTypes;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteBasicTypesTest {
+
+ @Inject
+ SQLiteBasicTypesRepository repository;
+
+ @Inject
+ DataSource dataSource;
+
+ @Test
+ void testQueryThatReturnsNull() {
+ assertFalse(repository.somethingThatMightSometimesReturnNull().isPresent());
+ }
+
+ @Test
+ void testBasicTypeMappingForPrimitiveIntegerProperty() {
+ assertMappedPropertyType("primitiveInteger", DataType.INTEGER);
+ }
+
+ @Test
+ void testBasicTypeMappingForWrapperIntegerProperty() {
+ assertMappedPropertyType("wrapperInteger", DataType.INTEGER);
+ }
+
+ @Test
+ void testBasicTypeMappingForPrimitiveBooleanProperty() {
+ assertMappedPropertyType("primitiveBoolean", DataType.BOOLEAN);
+ }
+
+ @Test
+ void testBasicTypeMappingForWrapperBooleanProperty() {
+ assertMappedPropertyType("wrapperBoolean", DataType.BOOLEAN);
+ }
+
+ @Test
+ void testBasicTypeMappingForPrimitiveShortProperty() {
+ assertMappedPropertyType("primitiveShort", DataType.SHORT);
+ }
+
+ @Test
+ void testBasicTypeMappingForWrapperShortProperty() {
+ assertMappedPropertyType("wrapperShort", DataType.SHORT);
+ }
+
+ @Test
+ void testBasicTypeMappingForPrimitiveLongProperty() {
+ assertMappedPropertyType("primitiveLong", DataType.LONG);
+ }
+
+ @Test
+ void testBasicTypeMappingForWrapperLongProperty() {
+ assertMappedPropertyType("wrapperLong", DataType.LONG);
+ }
+
+ @Test
+ void testBasicTypeMappingForPrimitiveDoubleProperty() {
+ assertMappedPropertyType("primitiveDouble", DataType.DOUBLE);
+ }
+
+ @Test
+ void testBasicTypeMappingForWrapperDoubleProperty() {
+ assertMappedPropertyType("wrapperDouble", DataType.DOUBLE);
+ }
+
+ @Test
+ void testBasicTypeMappingForUuidProperty() {
+ assertMappedPropertyType("uuid", DataType.UUID);
+ }
+
+ private void assertMappedPropertyType(String property, DataType type) {
+ PersistentEntity entity = PersistentEntity.of(BasicTypes.class);
+ var prop = entity.getPropertyByName(property);
+ assertEquals(type, prop.getAnnotation(MappedProperty.class).enumValue("type", DataType.class).orElseThrow());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookDtoRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookDtoRepository.java
new file mode 100644
index 00000000000..d70860d2624
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookDtoRepository.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.BookDtoRepository;
+import jakarta.inject.Singleton;
+
+@Singleton
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteBookDtoRepository extends BookDtoRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookEntityRepository.java
new file mode 100644
index 00000000000..b14004b5dc3
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookEntityRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.embedded.BookEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteBookEntityRepository extends BookEntityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookPageRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookPageRepository.java
new file mode 100644
index 00000000000..3b610bd8d93
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookPageRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.BookPageRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteBookPageRepository extends BookPageRepository {}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookRepository.java
new file mode 100644
index 00000000000..80baa0a0452
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookRepository.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.annotation.Where;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.Book;
+
+import java.util.List;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public abstract class SQLiteBookRepository extends io.micronaut.data.tck.repositories.BookRepository {
+
+ public SQLiteBookRepository(SQLiteAuthorRepository authorRepository) {
+ super(authorRepository);
+ }
+
+ @Query("UPDATE book SET total_pages = :pages WHERE title = :title")
+ abstract Long setPages(int pages, String title);
+
+ @Query("DELETE FROM book WHERE title = :title")
+ abstract Long wipeOutBook(String title);
+
+ @Where(value = "total_pages > :pages")
+ abstract List findByTitleStartsWith(String title, int pages);
+
+ @Query(value = "select count(*) from book b where b.title like :title and b.total_pages > :pages", nativeQuery = true)
+ abstract int countNativeByTitleWithPagesGreaterThan(String title, int pages);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookService.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookService.java
new file mode 100644
index 00000000000..2541f6df08f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookService.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.exceptions.EmptyResultException;
+import io.micronaut.data.jdbc.runtime.JdbcOperations;
+import io.micronaut.data.tck.entities.Author;
+import io.micronaut.data.tck.entities.Book;
+import jakarta.inject.Singleton;
+
+import jakarta.transaction.Transactional;
+import java.sql.ResultSet;
+import java.util.HashSet;
+import java.util.Set;
+
+@Singleton
+public class SQLiteBookService {
+ private final JdbcOperations jdbcOperations;
+
+ public SQLiteBookService(JdbcOperations jdbcOperations) {
+ this.jdbcOperations = jdbcOperations;
+ }
+
+ @Transactional
+ public Author findByName(String name) {
+ return jdbcOperations.prepareStatement("SELECT author_.id,author_.name,author_.nick_name,author_books_.id AS _books_id,author_books_.author_id AS _books_author_id,author_books_.title AS _books_title,author_books_.total_pages AS _books_total_pages,author_books_.publisher_id AS _books_publisher_id,author_books_.last_updated AS _books_last_updated, author_books_genre_.id AS _books_genre_id FROM author AS author_ INNER JOIN book author_books_ ON author_.id=author_books_.author_id LEFT JOIN genre author_books_genre_ ON author_books_genre_.id = author_books_.genre_id WHERE (author_.name = ?)", statement -> {
+ statement.setString(1, name);
+ ResultSet resultSet = statement.executeQuery();
+ if (resultSet.next()) {
+ Author author = jdbcOperations.readEntity(resultSet, Author.class);
+ Set books = new HashSet<>();
+ do {
+ books.add(jdbcOperations.readEntity("_books_", resultSet, Book.class));
+ } while (resultSet.next());
+ author.setBooks(books);
+ return author;
+ }
+ throw new EmptyResultException();
+ });
+ }
+
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCarRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCarRepository.java
new file mode 100644
index 00000000000..04633428831
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCarRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CarRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteCarRepository extends CarRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCascadeTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCascadeTest.java
new file mode 100644
index 00000000000..fc3a4ba8843
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCascadeTest.java
@@ -0,0 +1,33 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties(packages = "io.micronaut.data.jdbc.sqlite")
+class SQLiteCascadeTest {
+
+ @Inject
+ CascadeEntityRepository repository;
+
+ @Test
+ void testCascadeSave() {
+ CascadeSubEntityA entityA = new CascadeSubEntityA(null, 1, null);
+ CascadeSubEntityB entityB = new CascadeSubEntityB(null, 2, null);
+ CascadeEntity entity = new CascadeEntity(null, List.of(entityA), List.of(entityB));
+
+ entity = repository.save(entity);
+ var opt = repository.findById(entity.id());
+
+ assertTrue(opt.isPresent());
+ CascadeEntity loadedEntity = opt.get();
+ assertEquals(1, loadedEntity.subEntityAs().size());
+ assertEquals(1, loadedEntity.subEntityBs().size());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCityRepository.java
new file mode 100644
index 00000000000..5842122d1b9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCityRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteCityRepository extends CityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompanyRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompanyRepository.java
new file mode 100644
index 00000000000..b6f06e560c1
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompanyRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CompanyRepository;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteCompanyRepository extends CompanyRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompositePrimaryKeyTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompositePrimaryKeyTest.java
new file mode 100644
index 00000000000..ecd8342dc12
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompositePrimaryKeyTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.jdbc.entities.Project;
+import io.micronaut.data.tck.jdbc.entities.ProjectId;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteCompositePrimaryKeyTest {
+
+ @Inject
+ SQLiteProjectRepository projectRepository;
+
+ @Test
+ void testCrudWithCompositeId() {
+ ProjectId id = new ProjectId(10, 1);
+ Project p = new Project(id, "Project 1");
+ p.setOrg("test");
+ Project project = projectRepository.save(p);
+
+ assertEquals(10, project.getProjectId().getDepartmentId());
+ assertEquals(1, project.getProjectId().getProjectId());
+
+ project = projectRepository.findAll().iterator().next();
+ assertEquals(10, project.getProjectId().getDepartmentId());
+ assertEquals(1, project.getProjectId().getProjectId());
+
+ project = projectRepository.findById(id).orElse(null);
+ assertNotNull(project);
+ assertEquals(10, project.getProjectId().getDepartmentId());
+ assertEquals(1, project.getProjectId().getProjectId());
+ assertEquals("project 1", project.getName());
+ assertTrue(projectRepository.existsById(id));
+
+ projectRepository.update(id, "Project Changed");
+ project = projectRepository.findById(id).orElse(null);
+ assertNotNull(project);
+ assertEquals("project changed", project.getName());
+ assertEquals("PROJECT CHANGED", project.getDbName());
+
+ projectRepository.deleteById(id);
+ project = projectRepository.findById(id).orElse(null);
+ assertFalse(project != null);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRegionCityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRegionCityRepository.java
new file mode 100644
index 00000000000..6c1093b3a07
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRegionCityRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CountryRegionCityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteCountryRegionCityRepository extends CountryRegionCityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRepository.java
new file mode 100644
index 00000000000..366afc9bd97
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CountryRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteCountryRepository extends CountryRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCursoredPaginationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCursoredPaginationTest.java
new file mode 100644
index 00000000000..03c9b500497
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCursoredPaginationTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.model.CursoredPage;
+import io.micronaut.data.model.CursoredPageable;
+import io.micronaut.data.model.Page;
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.Sort;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.data.tck.repositories.PersonRepository;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteCursoredPaginationTest {
+
+ @Inject
+ SQLitePersonRepository personRepository;
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @BeforeEach
+ void setup() {
+ bookRepository.deleteAll();
+ personRepository.deleteAll();
+ personRepository.saveAll(createPeople());
+ }
+
+ @AfterEach
+ void cleanup() {
+ bookRepository.deleteAll();
+ personRepository.deleteAll();
+ }
+
+ @Test
+ void testCursoredPageableListForSorting() {
+ for (Object[] args : sortingArguments()) {
+ Sort sorting = (Sort) args[0];
+ String name1 = (String) args[1];
+ String name2 = (String) args[2];
+ String name10 = (String) args[3];
+ String name19 = (String) args[4];
+
+ CursoredPageable pageable = CursoredPageable.from(10, sorting);
+ CursoredPage page = assertCursored(personRepository.findAll(pageable));
+
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.getContent().stream().allMatch(Person.class::isInstance));
+ assertEquals(name1, page.getContent().get(0).getName());
+ assertEquals(name2, page.getContent().get(1).getName());
+ assertEquals(780, page.getTotalSize());
+ assertEquals(78, page.getTotalPages());
+ assertTrue(page.getCursor(0).isPresent());
+ assertTrue(page.getCursor(9).isPresent());
+ assertTrue(page.hasNext());
+
+ page = assertCursored(personRepository.findAll(page.nextPageable()));
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertEquals(name10, page.getContent().get(0).getName());
+ assertEquals(name19, page.getContent().get(9).getName());
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasPrevious());
+
+ pageable = page.previousPageable();
+ page = assertCursored(personRepository.findAll(pageable));
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(name1, page.getContent().get(0).getName());
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasPrevious());
+ }
+ }
+
+ @Test
+ void testPageableListWithRowRemoval() {
+ for (Object[] args : rowRemovalArguments()) {
+ setup();
+ Sort sorting = (Sort) args[0];
+ String elem1 = (String) args[1];
+ String elem2 = (String) args[2];
+ String elem10 = (String) args[3];
+ String elem19 = (String) args[4];
+
+ Pageable pageable = Pageable.from(0, 10, sorting);
+ CursoredPage page = personRepository.retrieve(pageable);
+
+ assertEquals(10, page.getContent().size());
+ assertEquals(elem1, page.getContent().get(0).getName());
+ assertEquals(elem2, page.getContent().get(1).getName());
+ assertTrue(page.hasNext());
+
+ personRepository.delete(page.getContent().get(1));
+ personRepository.delete(page.getContent().get(9));
+ page = personRepository.retrieve(page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertEquals(elem10, page.getContent().get(0).getName());
+ assertEquals(elem19, page.getContent().get(9).getName());
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasPrevious());
+
+ pageable = page.previousPageable();
+ page = personRepository.retrieve(pageable);
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(elem1, page.getContent().get(0).getName());
+ assertEquals(8, page.getContent().size());
+ assertTrue(page.getCursor(7).isPresent());
+ assertTrue(page.getCursor(8).isEmpty());
+ assertFalse(page.hasPrevious());
+ assertTrue(page.hasNext());
+ }
+ }
+
+ @Test
+ void testPageableListWithRowAddition() {
+ for (Object[] args : rowAdditionArguments()) {
+ setup();
+ Sort sorting = (Sort) args[0];
+ String elem1 = (String) args[1];
+ String elem2 = (String) args[2];
+ String elem3 = (String) args[3];
+ String elem10 = (String) args[4];
+ String elem19 = (String) args[5];
+
+ CursoredPage page = personRepository.retrieve(CursoredPageable.from(10, sorting));
+
+ assertEquals(10, page.getContent().size());
+ assertEquals(elem1, page.getContent().get(0).getName());
+ assertEquals(elem2, page.getContent().get(1).getName());
+ assertTrue(page.hasNext());
+
+ personRepository.saveAll(List.of(
+ person("AAAAA00"),
+ person("AAAAA01"),
+ person("ZZZZZ08"),
+ person("ZZZZZ07")
+ ));
+ page = personRepository.retrieve(page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertEquals(elem10, page.getContent().get(0).getName());
+ assertEquals(elem19, page.getContent().get(9).getName());
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasPrevious());
+
+ page = personRepository.retrieve(page.previousPageable());
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(elem3, page.getContent().get(0).getName());
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasPrevious());
+
+ page = personRepository.retrieve(page.previousPageable());
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(elem1, page.getContent().get(0).getName());
+ assertEquals(elem2, page.getContent().get(1).getName());
+ assertTrue(page.getCursor(1).isPresent());
+ assertTrue(page.getCursor(2).isEmpty());
+ assertEquals(2, page.getContent().size());
+ assertFalse(page.hasPrevious());
+ }
+ }
+
+ @Test
+ void testCursoredPageable() {
+ List>> resultFunctions = List.of(
+ pageable -> personRepository.findByNameLike("A%", pageable),
+ pageable -> personRepository.findAll(PersonRepository.Specifications.nameLike("A%"), (CursoredPageable) pageable)
+ );
+ for (Function> resultFunction : resultFunctions) {
+ CursoredPageable pageable = CursoredPageable.from(10, null);
+ Page page = resultFunction.apply(pageable);
+ Page page2 = personRepository.findPeople("A%", pageable);
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(30, page.getTotalSize());
+ assertEquals(page.getTotalSize(), page2.getTotalSize());
+ List firstContentIds = ids(page.getContent());
+ assertTrue(page.getContent().stream().map(Person::getName).allMatch(name -> name.startsWith("A")));
+
+ page = resultFunction.apply(page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertNotEquals(firstContentIds, ids(page.getContent()));
+ assertTrue(page.getContent().stream().map(Person::getName).allMatch(name -> name.startsWith("A")));
+
+ page = resultFunction.apply(page.previousPageable());
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(10, page.getContent().size());
+ assertEquals(firstContentIds, ids(page.getContent()));
+ assertTrue(page.getContent().stream().map(Person::getName).allMatch(name -> name.startsWith("A")));
+ }
+ }
+
+ @Test
+ void testFindWithLeftJoin() {
+ List books = new ArrayList<>();
+ bookRepository.saveAll(List.of(
+ book("Book 1", 100),
+ book("Book 2", 100)
+ )).forEach(books::add);
+
+ CursoredPage page = assertCursored(bookRepository.findByTotalPagesGreaterThan(
+ 50, CursoredPageable.from(books.size(), null)
+ ));
+
+ assertEquals(books.size(), page.getContent().size());
+ assertEquals(books.size(), page.getTotalSize());
+
+ CursoredPage pageOfTitles = page.map(Book::getTitle);
+ assertTrue(pageOfTitles.hasTotalSize());
+ List titles = pageOfTitles.getContent();
+ assertTrue(titles.contains("Book 1"));
+ assertTrue(titles.contains("Book 2"));
+
+ page = assertCursored(bookRepository.findAll(page.nextPageable().withoutTotal()));
+ pageOfTitles = page.map(Book::getTitle);
+ assertFalse(pageOfTitles.hasTotalSize());
+ titles = pageOfTitles.getContent();
+
+ assertTrue(titles.isEmpty());
+ }
+
+ @Test
+ void testCursoredPageableWithoutPageSize() {
+ CursoredPageable pageable = CursoredPageable.from(Sort.of(Sort.Order.desc("name")));
+ CursoredPage page = personRepository.findAll(PersonRepository.Specifications.nameLike("BBBB%"), pageable);
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(30, page.getTotalSize());
+ assertFalse(page.getContent().isEmpty());
+ assertTrue(page.getContent().stream().allMatch(Person.class::isInstance));
+
+ page = personRepository.findAll(PersonRepository.Specifications.nameLike("BBBB%"), page.nextPageable());
+
+ assertEquals(0, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertEquals(30, page.getTotalSize());
+ assertEquals(0, page.nextPageable().getOffset());
+ assertEquals(2, page.nextPageable().getNumber());
+ assertTrue(page.getContent().isEmpty());
+ }
+
+ private static List sortingArguments() {
+ return List.of(
+ new Object[]{null, "AAAAA00", "AAAAA01", "BBBBB00", "BBBBB09"},
+ new Object[]{Sort.of(Sort.Order.desc("id")), "ZZZZZ09", "ZZZZZ08", "YYYYY09", "YYYYY00"},
+ new Object[]{Sort.of(Sort.Order.asc("name")), "AAAAA00", "AAAAA00", "AAAAA03", "AAAAA06"},
+ new Object[]{Sort.of(Sort.Order.desc("name")), "ZZZZZ09", "ZZZZZ09", "ZZZZZ06", "ZZZZZ03"},
+ new Object[]{Sort.of(Sort.Order.asc("age"), Sort.Order.asc("name")), "AAAAA00", "BBBBB00", "KKKKK00", "TTTTT00"},
+ new Object[]{Sort.of(Sort.Order.desc("age"), Sort.Order.asc("name")), "AAAAA09", "BBBBB09", "KKKKK09", "TTTTT09"}
+ );
+ }
+
+ private static List rowRemovalArguments() {
+ return List.of(
+ new Object[]{null, "AAAAA00", "AAAAA01", "BBBBB00", "BBBBB09"},
+ new Object[]{Sort.of(Sort.Order.desc("id")), "ZZZZZ09", "ZZZZZ08", "YYYYY09", "YYYYY00"},
+ new Object[]{Sort.of(Sort.Order.asc("name")), "AAAAA00", "AAAAA00", "AAAAA03", "AAAAA06"},
+ new Object[]{Sort.of(Sort.Order.desc("name")), "ZZZZZ09", "ZZZZZ09", "ZZZZZ06", "ZZZZZ03"}
+ );
+ }
+
+ private static List rowAdditionArguments() {
+ return List.of(
+ new Object[]{Sort.of(Sort.Order.asc("name")), "AAAAA00", "AAAAA00", "AAAAA00", "AAAAA03", "AAAAA06"},
+ new Object[]{Sort.of(Sort.Order.desc("name")), "ZZZZZ09", "ZZZZZ09", "ZZZZZ09", "ZZZZZ06", "ZZZZZ03"}
+ );
+ }
+
+ private static List ids(List people) {
+ return people.stream().map(Person::getId).toList();
+ }
+
+ private static Person person(String name) {
+ Person person = new Person();
+ person.setName(name);
+ return person;
+ }
+
+ private static Book book(String title, int totalPages) {
+ Book book = new Book();
+ book.setTitle(title);
+ book.setTotalPages(totalPages);
+ return book;
+ }
+
+ private static List createPeople() {
+ List people = new ArrayList<>();
+ for (int group = 0; group < 3; group++) {
+ for (char letter = 'A'; letter <= 'Z'; letter++) {
+ for (int num = 0; num < 10; num++) {
+ Person person = new Person();
+ person.setName(String.valueOf(letter).repeat(5) + String.format("%02d", num));
+ person.setAge(group * 10 + num + 1);
+ people.add(person);
+ }
+ }
+ }
+ return people;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static CursoredPage assertCursored(Page page) {
+ return assertInstanceOf(CursoredPage.class, page);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCustomIdTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCustomIdTest.java
new file mode 100644
index 00000000000..dd9fd8be76d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCustomIdTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Task;
+import io.micronaut.data.tck.entities.TaskGenericEntity;
+import io.micronaut.data.tck.entities.TaskGenericEntity2;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteCustomIdTest {
+
+ @Inject
+ SQLiteTaskRepository taskRepository;
+
+ @Inject
+ SQLiteTaskGenericEntityRepository taskGenericEntityRepository;
+
+ @Inject
+ SQLiteTaskGenericEntity2Repository taskGenericEntity2Repository;
+
+ @Test
+ void testSaveAndReadEntity() {
+ Task task = taskRepository.save(new Task("Task 1"));
+ assertNotNull(task.getTaskId());
+
+ task = taskRepository.findById(task.getTaskId()).orElse(null);
+ assertNotNull(task);
+ assertNotNull(task.getTaskId());
+ assertEquals("Task 1", task.getName());
+ }
+
+ @Test
+ void testSaveAndReadGenericEntity() {
+ TaskGenericEntity task = taskGenericEntityRepository.save(new TaskGenericEntity("Task 1"));
+ assertNotNull(task.getId());
+
+ task = taskGenericEntityRepository.findById(task.getId()).orElse(null);
+ assertNotNull(task);
+ assertNotNull(task.getId());
+ assertEquals("Task 1", task.getName());
+ }
+
+ @Test
+ void testSaveAndReadGenericEntity2() {
+ TaskGenericEntity2 task = taskGenericEntity2Repository.save(new TaskGenericEntity2("Task 1"));
+ assertNotNull(task.getId());
+
+ task = taskGenericEntity2Repository.findById(task.getId()).orElse(null);
+ assertNotNull(task);
+ assertNotNull(task.getId());
+ assertEquals("Task 1", task.getName());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBProperties.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBProperties.java
new file mode 100644
index 00000000000..e35fcd9a88c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBProperties.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SQLiteDBProperties {
+
+ String name() default "mydb";
+
+ String packages() default "io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities,io.micronaut.data.jdbc.sqlite";
+
+ String schemaGenerate() default "CREATE_DROP";
+
+ String dialect() default "SQLITE";
+
+ String dbType() default "sqlite";
+
+ String driverClassName() default "org.sqlite.JDBC";
+
+ String url() default "jdbc:sqlite:file:%s?mode=memory&cache=shared&foreign_keys=ON&busy_timeout=5000";
+
+ String username() default "";
+
+ String password() default "";
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBPropertiesTestPropertyProviderFactory.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBPropertiesTestPropertyProviderFactory.java
new file mode 100644
index 00000000000..678d1874d1b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBPropertiesTestPropertyProviderFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.support.TestPropertyProvider;
+import io.micronaut.test.support.TestPropertyProviderFactory;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class SQLiteDBPropertiesTestPropertyProviderFactory implements TestPropertyProviderFactory {
+
+ @Override
+ public TestPropertyProvider create(Map availableProperties, Class> testClass) {
+ SQLiteDBProperties sqliteDbProperties = testClass.getAnnotation(SQLiteDBProperties.class);
+ if (sqliteDbProperties == null) {
+ return Collections::emptyMap;
+ }
+ return () -> Map.of(
+ "datasources.default.name", sqliteDbProperties.name(),
+ "datasources.default.packages", sqliteDbProperties.packages(),
+ "datasources.default.schema-generate", sqliteDbProperties.schemaGenerate(),
+ "datasources.default.dialect", sqliteDbProperties.dialect(),
+ "datasources.default.db-type", sqliteDbProperties.dbType(),
+ "datasources.default.driverClassName", sqliteDbProperties.driverClassName(),
+ "datasources.default.url", sqliteDbProperties.url(),
+ "datasources.default.username", sqliteDbProperties.username(),
+ "datasources.default.password", sqliteDbProperties.password()
+ );
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDisabledDataSourceTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDisabledDataSourceTest.java
new file mode 100644
index 00000000000..e052e4dae29
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDisabledDataSourceTest.java
@@ -0,0 +1,33 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.exceptions.NoSuchBeanException;
+import io.micronaut.data.jdbc.config.DataJdbcConfiguration;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@MicronautTest(transactional = false)
+@SQLiteDBProperties
+@Property(name = "datasources.default.enabled", value = "false")
+class SQLiteDisabledDataSourceTest {
+
+ @Inject
+ ApplicationContext applicationContext;
+
+ @Test
+ void testDisabledDataSource() {
+ assertThrows(NoSuchBeanException.class, () -> applicationContext.getBean(DataSource.class));
+
+ DataJdbcConfiguration dataJdbcConfiguration = applicationContext.getBean(DataJdbcConfiguration.class);
+ assertNotNull(dataJdbcConfiguration);
+ assertFalse(dataJdbcConfiguration.isEnabled());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyRecordTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyRecordTest.java
new file mode 100644
index 00000000000..788a2a7907b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyRecordTest.java
@@ -0,0 +1,312 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.context.env.Environment;
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.data.connection.ConnectionDefinition;
+import io.micronaut.data.connection.annotation.Connectable;
+import io.micronaut.data.tck.entities.AccountRecord;
+import io.micronaut.data.tck.repositories.AccountRecordRepository;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Delete;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Header;
+import io.micronaut.http.annotation.Post;
+import io.micronaut.http.annotation.Put;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.scheduling.TaskExecutors;
+import io.micronaut.scheduling.annotation.ExecuteOn;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SQLiteDiscriminatorMultitenancyRecordTest {
+
+ @Test
+ void testDiscriminatorMultitenancy() {
+ Map properties = new HashMap<>(createProperties());
+ properties.put("accountRepositoryClass", SQLiteAccountRecordRepository.class.getName());
+ properties.put("spec.name", "discriminator-multitenancy-record");
+ properties.put("micronaut.data.multi-tenancy.mode", "DISCRIMINATOR");
+ properties.put("micronaut.multitenancy.tenantresolver.httpheader.enabled", "true");
+ properties.put("datasource.default.schema-generate", "create-drop");
+
+ try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties, Environment.TEST)) {
+ ApplicationContext context = embeddedServer.getApplicationContext();
+ FooAccountRecordClient fooAccountClient = context.getBean(FooAccountRecordClient.class);
+ BarAccountRecordClient barAccountClient = context.getBean(BarAccountRecordClient.class);
+
+ fooAccountClient.deleteAll();
+ barAccountClient.deleteAll();
+
+ AccountRecordDto fooAccount = fooAccountClient.save("The Stand");
+ assertNotNull(fooAccount.getId());
+
+ fooAccount = fooAccountClient.findOne(fooAccount.getId()).orElse(null);
+ assertNotNull(fooAccount);
+ assertEquals("The Stand", fooAccount.getName());
+ assertEquals("foo", fooAccount.getTenancy());
+ assertEquals(1, fooAccountClient.findAll().size());
+ assertEquals(0, barAccountClient.findAll().size());
+
+ fooAccountClient.updateTenancy(fooAccount.getId(), "bar");
+ assertEquals(0, fooAccountClient.findAll().size());
+ assertEquals(1, barAccountClient.findAll().size());
+ assertTrue(fooAccountClient.findOne(fooAccount.getId()).isEmpty());
+ assertTrue(barAccountClient.findOne(fooAccount.getId()).isPresent());
+
+ barAccountClient.updateTenancy(fooAccount.getId(), "foo");
+ assertEquals(1, fooAccountClient.findAll().size());
+ assertEquals(0, barAccountClient.findAll().size());
+ assertTrue(fooAccountClient.findOne(fooAccount.getId()).isPresent());
+ assertTrue(barAccountClient.findOne(fooAccount.getId()).isEmpty());
+
+ AccountRecordDto barAccount = barAccountClient.save("The Bar");
+ List allAccounts = barAccountClient.findAllTenants();
+ Long fooAccountId = fooAccount.getId();
+
+ assertEquals("bar", barAccount.getTenancy());
+ assertEquals(2, allAccounts.size());
+ assertEquals("bar", allAccounts.stream().filter(account -> account.getId().equals(barAccount.getId())).findFirst().orElseThrow().getTenancy());
+ assertEquals("foo", allAccounts.stream().filter(account -> account.getId().equals(fooAccountId)).findFirst().orElseThrow().getTenancy());
+ assertEquals(allAccounts, fooAccountClient.findAllTenants());
+
+ List barAccounts = barAccountClient.findAllBarTenants();
+ assertEquals(1, barAccounts.size());
+ assertEquals(barAccount.getId(), barAccounts.getFirst().getId());
+ assertEquals("bar", barAccounts.getFirst().getTenancy());
+ assertEquals(barAccounts, fooAccountClient.findAllBarTenants());
+
+ List fooAccounts = barAccountClient.findAllFooTenants();
+ assertEquals(1, fooAccounts.size());
+ assertEquals(fooAccountId, fooAccounts.getFirst().getId());
+ assertEquals("foo", fooAccounts.getFirst().getTenancy());
+ assertEquals(fooAccounts, fooAccountClient.findAllFooTenants());
+
+ List exp = barAccountClient.findTenantExpression();
+ assertEquals(1, exp.size());
+ assertEquals("bar", exp.getFirst().getTenancy());
+ assertEquals(exp, fooAccountClient.findTenantExpression());
+
+ barAccountClient.deleteAll();
+ assertEquals(1, fooAccountClient.findAll().size());
+
+ fooAccountClient.deleteAll();
+ assertEquals(0, fooAccountClient.findAll().size());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqlitediscriminatormultitenancyrecord", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities,io.micronaut.data.jdbc.sqlite");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
+@ExecuteOn(TaskExecutors.IO)
+@Controller("/accounts")
+class AccountRecordController {
+
+ private final AccountRecordRepository accountRepository;
+
+ AccountRecordController(ApplicationContext beanContext) throws ClassNotFoundException {
+ String className = beanContext.getProperty("accountRepositoryClass", String.class).orElseThrow();
+ this.accountRepository = (AccountRecordRepository) beanContext.getBean(Class.forName(className));
+ }
+
+ @Post
+ AccountRecordDto save(String name) {
+ AccountRecord newAccount = new AccountRecord(null, name, null);
+ AccountRecord account = accountRepository.save(newAccount);
+ return new AccountRecordDto(account);
+ }
+
+ @Put("/{id}/tenancy")
+ void updateTenancy(Long id, String tenancy) {
+ AccountRecord account = accountRepository.findById(id).orElseThrow();
+ accountRepository.update(new AccountRecord(account.id(), account.name(), tenancy));
+ }
+
+ @Get("/{id}")
+ Optional findOne(Long id) {
+ return accountRepository.findById(id).map(AccountRecordDto::new);
+ }
+
+ @Get
+ List findAll() {
+ return findAll0();
+ }
+
+ @Get("/alltenants")
+ List findAllTenants() {
+ return accountRepository.findAll$withAllTenants().stream().map(AccountRecordDto::new).toList();
+ }
+
+ @Get("/foo")
+ List findAllFooTenants() {
+ return accountRepository.findAll$withTenantFoo().stream().map(AccountRecordDto::new).toList();
+ }
+
+ @Get("/bar")
+ List findAllBarTenants() {
+ return accountRepository.findAll$withTenantBar().stream().map(AccountRecordDto::new).toList();
+ }
+
+ @Get("/expression")
+ List findTenantExpression() {
+ return accountRepository.findAll$withTenantExpression().stream().map(AccountRecordDto::new).toList();
+ }
+
+ @Connectable
+ protected List findAll0() {
+ return findAll1();
+ }
+
+ @Connectable(propagation = ConnectionDefinition.Propagation.MANDATORY)
+ protected List findAll1() {
+ return accountRepository.findAll().stream().map(AccountRecordDto::new).toList();
+ }
+
+ @Delete
+ void deleteAll() {
+ deleteAll0();
+ }
+
+ @Transactional
+ protected void deleteAll0() {
+ deleteAll1();
+ }
+
+ @Transactional(Transactional.TxType.MANDATORY)
+ protected void deleteAll1() {
+ accountRepository.deleteAll();
+ }
+}
+
+@Introspected
+class AccountRecordDto {
+ private Long id;
+ private String name;
+ private String tenancy;
+
+ AccountRecordDto() {
+ }
+
+ AccountRecordDto(AccountRecord account) {
+ this.id = account.id();
+ this.name = account.name();
+ this.tenancy = account.tenancy();
+ }
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ String getTenancy() {
+ return tenancy;
+ }
+
+ void setTenancy(String tenancy) {
+ this.tenancy = tenancy;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof AccountRecordDto that)) {
+ return false;
+ }
+ return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(tenancy, that.tenancy);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, tenancy);
+ }
+}
+
+@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
+@Client("/accounts")
+interface AccountRecordClient {
+
+ @Post
+ AccountRecordDto save(String name);
+
+ @Put("/{id}/tenancy")
+ void updateTenancy(Long id, String tenancy);
+
+ @Get("/{id}")
+ Optional findOne(Long id);
+
+ @Get
+ List findAll();
+
+ @Get("/alltenants")
+ List findAllTenants();
+
+ @Get("/foo")
+ List findAllFooTenants();
+
+ @Get("/bar")
+ List findAllBarTenants();
+
+ @Get("/expression")
+ List findTenantExpression();
+
+ @Delete
+ void deleteAll();
+}
+
+@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
+@Header(name = "tenantId", value = "foo")
+@Client("/accounts")
+interface FooAccountRecordClient extends AccountRecordClient {
+}
+
+@Requires(property = "spec.name", value = "discriminator-multitenancy-record")
+@Header(name = "tenantId", value = "bar")
+@Client("/accounts")
+interface BarAccountRecordClient extends AccountRecordClient {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyTest.java
new file mode 100644
index 00000000000..c6ba9840b7b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyTest.java
@@ -0,0 +1,117 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.env.Environment;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.data.tck.tests.AccountDto;
+import io.micronaut.data.tck.tests.BarAccountClient;
+import io.micronaut.data.tck.tests.FooAccountClient;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SQLiteDiscriminatorMultitenancyTest {
+
+ @Test
+ void testDiscriminatorMultitenancy() {
+ Map properties = new HashMap<>(createProperties());
+ properties.put("accountRepositoryClass", SQLiteAccountRepository.class.getName());
+ properties.put("spec.name", "discriminator-multitenancy");
+ properties.put("micronaut.data.multi-tenancy.mode", "DISCRIMINATOR");
+ properties.put("micronaut.multitenancy.tenantresolver.httpheader.enabled", "true");
+ properties.put("datasource.default.schema-generate", "create-drop");
+
+ try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties, Environment.TEST)) {
+ ApplicationContext context = embeddedServer.getApplicationContext();
+ FooAccountClient fooAccountClient = context.getBean(FooAccountClient.class);
+ BarAccountClient barAccountClient = context.getBean(BarAccountClient.class);
+
+ fooAccountClient.deleteAll();
+ barAccountClient.deleteAll();
+
+ AccountDto fooAccount = fooAccountClient.save("The Stand");
+ assertNotNull(fooAccount.getId());
+
+ fooAccount = fooAccountClient.findOne(fooAccount.getId()).orElse(null);
+ assertNotNull(fooAccount);
+ assertEquals("The Stand", fooAccount.getName());
+ assertEquals("foo", fooAccount.getTenancy());
+ assertEquals(1, fooAccountClient.findAll().size());
+ assertEquals(0, barAccountClient.findAll().size());
+
+ fooAccountClient.updateTenancy(fooAccount.getId(), "bar");
+ assertEquals(0, fooAccountClient.findAll().size());
+ assertEquals(1, barAccountClient.findAll().size());
+ assertTrue(fooAccountClient.findOne(fooAccount.getId()).isEmpty());
+ assertTrue(barAccountClient.findOne(fooAccount.getId()).isPresent());
+
+ barAccountClient.updateTenancy(fooAccount.getId(), "foo");
+ assertEquals(1, fooAccountClient.findAll().size());
+ assertEquals(0, barAccountClient.findAll().size());
+ assertTrue(fooAccountClient.findOne(fooAccount.getId()).isPresent());
+ assertTrue(barAccountClient.findOne(fooAccount.getId()).isEmpty());
+
+ AccountDto barAccount = barAccountClient.save("The Bar");
+ List allAccounts = barAccountClient.findAllTenants();
+ Long fooAccountId = fooAccount.getId();
+ assertEquals("bar", barAccount.getTenancy());
+ assertEquals(2, allAccounts.size());
+ assertEquals("bar", allAccounts.stream().filter(account -> account.getId().equals(barAccount.getId())).findFirst().orElseThrow().getTenancy());
+ assertEquals("foo", allAccounts.stream().filter(account -> account.getId().equals(fooAccountId)).findFirst().orElseThrow().getTenancy());
+ assertEquals(allAccounts, fooAccountClient.findAllTenants());
+
+ List barAccounts = barAccountClient.findAllBarTenants();
+ assertEquals(1, barAccounts.size());
+ assertEquals(barAccount.getId(), barAccounts.getFirst().getId());
+ assertEquals("bar", barAccounts.getFirst().getTenancy());
+ assertEquals(barAccounts, fooAccountClient.findAllBarTenants());
+
+ List fooAccounts = barAccountClient.findAllFooTenants();
+ assertEquals(1, fooAccounts.size());
+ assertEquals(fooAccountId, fooAccounts.getFirst().getId());
+ assertEquals("foo", fooAccounts.getFirst().getTenancy());
+ assertEquals(fooAccounts, fooAccountClient.findAllFooTenants());
+
+ List exp = barAccountClient.findTenantExpression();
+ assertEquals(1, exp.size());
+ assertEquals("bar", exp.getFirst().getTenancy());
+ assertEquals(exp, fooAccountClient.findTenantExpression());
+
+ barAccountClient.deleteAll();
+ assertEquals(1, fooAccountClient.findAll().size());
+
+ fooAccountClient.deleteAll();
+ assertEquals(0, fooAccountClient.findAll().size());
+ assertFalse(fooAccountClient.findAll().size() > 0);
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqlitediscriminatormultitenancy", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities,io.micronaut.data.jdbc.sqlite");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsReactiveRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsReactiveRepository.java
new file mode 100644
index 00000000000..0db0613f00f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsReactiveRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.DomainEventsReactiveRepository;
+
+@JdbcRepository(dialect= Dialect.SQLITE)
+public interface SQLiteDomainEventsReactiveRepository extends DomainEventsReactiveRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsRepository.java
new file mode 100644
index 00000000000..15dfc19541c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.DomainEventsRepository;
+
+@JdbcRepository(dialect=Dialect.SQLITE)
+public interface SQLiteDomainEventsRepository extends DomainEventsRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement1Repository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement1Repository.java
new file mode 100644
index 00000000000..b4a0e050aa9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement1Repository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.CarRepository;
+import io.micronaut.data.tck.repositories.DoubleImplement1Repository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteDoubleImplement1Repository extends DoubleImplement1Repository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement2Repository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement2Repository.java
new file mode 100644
index 00000000000..f1b41765196
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement2Repository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.DoubleImplement2Repository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteDoubleImplement2Repository extends DoubleImplement2Repository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement3Repository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement3Repository.java
new file mode 100644
index 00000000000..5fa0244c7b4
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement3Repository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.DoubleImplement3Repository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteDoubleImplement3Repository extends DoubleImplement3Repository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDtoTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDtoTest.java
new file mode 100644
index 00000000000..8f10a2f71d8
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDtoTest.java
@@ -0,0 +1,185 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.data.annotation.DateUpdated;
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.NamingStrategy;
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.naming.NamingStrategies;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class SQLiteDtoTest {
+
+ @Test
+ void testDtos() {
+ try (ApplicationContext applicationContext = ApplicationContext.run(createProperties())) {
+ ThingRepository thingRepository = applicationContext.getBean(ThingRepository.class);
+
+ Thing thing = new Thing();
+ thing.setName("Test");
+ thing.setScore(123);
+ thing.setSite("XYZ");
+
+ Thing saved = thingRepository.save(thing);
+ assertNotNull(saved.getId());
+
+ List things = thingRepository.findThingDTOsByThingId(saved.getId());
+
+ assertEquals(1, things.size());
+ ThingDto dto = things.get(0);
+ assertEquals(saved.getId().intValue(), dto.getThingId());
+ assertEquals("Test", dto.getThingName());
+ assertNotNull(dto.getThingUpdatedAt());
+ assertNotNull(dto.getThingUpdatedAtTime());
+ assertFalse(dto.getThingUpdatedAtTime().toString().isEmpty());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqlitedto", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface ThingRepository extends CrudRepository {
+
+ @Query("""
+ SELECT thing.id AS thingId,
+ thing.name AS thingName,
+ replace(strftime('%Y-%m-%dT%H:%M:%f', thing.updatedAt / 1000.0, 'unixepoch'), '.000', '') AS thingUpdatedAt,
+ strftime('%H:%M:%f', thing.updatedAt / 1000.0, 'unixepoch') AS thingUpdatedAtTime
+ FROM the_things thing
+ WHERE thing.id = :id
+ """)
+ List findThingDTOsByThingId(Long id);
+}
+
+@MappedEntity(value = "the_things", namingStrategy = NamingStrategies.Raw.class)
+class Thing {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+ private String name;
+ private Integer score;
+ private String site;
+
+ @DateUpdated
+ private LocalDateTime updatedAt;
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ Integer getScore() {
+ return score;
+ }
+
+ void setScore(Integer score) {
+ this.score = score;
+ }
+
+ String getSite() {
+ return site;
+ }
+
+ void setSite(String site) {
+ this.site = site;
+ }
+
+ LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
+
+@Introspected
+@NamingStrategy(NamingStrategies.Raw.class)
+class ThingDto {
+
+ private Integer thingId;
+ private String thingName;
+ private LocalDateTime thingUpdatedAt;
+ private LocalTime thingUpdatedAtTime;
+
+ Integer getThingId() {
+ return thingId;
+ }
+
+ void setThingId(Integer thingId) {
+ this.thingId = thingId;
+ }
+
+ String getThingName() {
+ return thingName;
+ }
+
+ void setThingName(String thingName) {
+ this.thingName = thingName;
+ }
+
+ LocalDateTime getThingUpdatedAt() {
+ return thingUpdatedAt;
+ }
+
+ void setThingUpdatedAt(LocalDateTime thingUpdatedAt) {
+ this.thingUpdatedAt = thingUpdatedAt;
+ }
+
+ LocalTime getThingUpdatedAtTime() {
+ return thingUpdatedAtTime;
+ }
+
+ void setThingUpdatedAtTime(LocalTime thingUpdatedAtTime) {
+ this.thingUpdatedAtTime = thingUpdatedAtTime;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEagerContextTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEagerContextTest.java
new file mode 100644
index 00000000000..885b878e367
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEagerContextTest.java
@@ -0,0 +1,81 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.BeanLocator;
+import io.micronaut.data.jdbc.config.SchemaGenerator;
+import io.micronaut.data.tck.entities.Person;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Singleton;
+import org.junit.jupiter.api.Test;
+
+import java.io.UncheckedIOException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SQLiteEagerContextTest {
+
+ @Test
+ void testEagerStart() {
+ Map properties = new HashMap<>(createSqliteDataSourceProperties("default"));
+ properties.put("eager-test", true);
+ properties.putAll(createSqliteDataSourceProperties("other"));
+
+ try (ApplicationContext context = ApplicationContext.builder(properties)
+ .eagerInitSingletons(true)
+ .start()) {
+ SQLitePersonRepository personRepository = context.getBean(SQLitePersonRepository.class);
+ assertEquals(4, personRepository.findAll().size());
+ }
+ }
+
+ private static Map createSqliteDataSourceProperties(String dataSourceName) {
+ try {
+ var databaseFile = Files.createTempFile(dataSourceName.toLowerCase(Locale.ENGLISH), ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ String prefix = "datasources." + dataSourceName;
+ Map properties = new HashMap<>();
+ properties.put(prefix + ".url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put(prefix + ".schema-generate", "CREATE");
+ properties.put(prefix + ".dialect", "SQLITE");
+ properties.put(prefix + ".db-type", "sqlite");
+ properties.put(prefix + ".username", "");
+ properties.put(prefix + ".password", "");
+ properties.put(prefix + ".packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put(prefix + ".driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+
+ @Singleton
+ static class SimpleService {
+
+ private final SQLitePersonRepository personRepository;
+
+ SimpleService(SQLitePersonRepository personRepository) {
+ this.personRepository = personRepository;
+ }
+
+ @PostConstruct
+ void init(SchemaGenerator schemaGenerator, BeanLocator beanLocator) {
+ schemaGenerator.createOrValidateSchema(beanLocator);
+
+ personRepository.save(newPerson("a"));
+ personRepository.save(newPerson("c"));
+ personRepository.save(newPerson("b"));
+ personRepository.save(newPerson("d"));
+ }
+
+ private Person newPerson(String name) {
+ Person person = new Person();
+ person.setName(name);
+ return person;
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbedded2Test.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbedded2Test.java
new file mode 100644
index 00000000000..d3068f40d77
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbedded2Test.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.data.annotation.Embeddable;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+
+import java.util.Objects;
+
+import static io.micronaut.data.model.query.builder.sql.Dialect.SQLITE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteEmbedded2Test {
+
+ @Inject
+ FooRepo repo;
+
+ @Test
+ void filledInnerCanBeRetrieved() {
+ Foo saved = repo.save(new Foo(0, new Bar("1", "2")));
+ Foo found = repo.findById(saved.getId()).orElseThrow();
+ assertEquals(new Bar("1", "2"), found.getBar());
+ }
+
+ @Test
+ void partiallyFilledInnerCanBeRetrieved() {
+ Foo saved = repo.save(new Foo(0, new Bar("1", null)));
+ Foo found = repo.findById(saved.getId()).orElseThrow();
+ assertEquals(new Bar("1", null), found.getBar());
+ }
+}
+
+@JdbcRepository(dialect = SQLITE)
+interface FooRepo extends CrudRepository {
+}
+
+@Embeddable
+@Introspected
+final class Bar {
+ private String bar1;
+ @Nullable
+ private String bar2;
+
+ Bar() {
+ }
+
+ Bar(String bar1, @Nullable String bar2) {
+ this.bar1 = bar1;
+ this.bar2 = bar2;
+ }
+
+ String getBar1() {
+ return bar1;
+ }
+
+ void setBar1(String bar1) {
+ this.bar1 = bar1;
+ }
+
+ @Nullable
+ String getBar2() {
+ return bar2;
+ }
+
+ void setBar2(@Nullable String bar2) {
+ this.bar2 = bar2;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Bar bar)) {
+ return false;
+ }
+ return Objects.equals(bar1, bar.bar1) && Objects.equals(bar2, bar.bar2);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bar1, bar2);
+ }
+}
+
+@Entity
+@Introspected
+final class Foo {
+ @Id
+ private int id;
+
+ @Nullable
+ @Embedded
+ private Bar bar;
+
+ Foo() {
+ }
+
+ Foo(int id, @Nullable Bar bar) {
+ this.id = id;
+ this.bar = bar;
+ }
+
+ int getId() {
+ return id;
+ }
+
+ void setId(int id) {
+ this.id = id;
+ }
+
+ @Nullable
+ Bar getBar() {
+ return bar;
+ }
+
+ void setBar(@Nullable Bar bar) {
+ this.bar = bar;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedCascadeTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedCascadeTest.java
new file mode 100644
index 00000000000..a1b32d2443b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedCascadeTest.java
@@ -0,0 +1,189 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.annotation.Embeddable;
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import org.jspecify.annotations.NonNull;
+import org.junit.jupiter.api.Test;
+
+import java.io.Serializable;
+import java.io.UncheckedIOException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class SQLiteEmbeddedCascadeTest {
+
+ @Test
+ void testEmbeddedCascade() {
+ try (ApplicationContext applicationContext = ApplicationContext.run(createProperties())) {
+ TemplateRepository templateRepository = applicationContext.getBean(TemplateRepository.class);
+
+ Template template = new Template();
+ template.setName("Template test");
+
+ Tag tag = new Tag();
+ TagPk tagPk = new TagPk();
+ tagPk.setTag("New tag");
+ tagPk.setTemplate(template);
+ tag.setId(tagPk);
+
+ template.getTags().add(tag);
+
+ Template saved = templateRepository.save(template);
+ Template loaded = templateRepository.findById(saved.getId()).orElseThrow();
+
+ assertNotNull(loaded);
+ assertEquals(1, loaded.getTags().size());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqliteembeddedcascade", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface TemplateRepository extends CrudRepository {
+
+ @Join("tags")
+ @Override
+ Optional findById(Long id);
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface TagRepository extends CrudRepository {
+}
+
+@MappedEntity
+class Template {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @NonNull
+ private String name;
+
+ @OneToMany(mappedBy = "id.template", cascade = CascadeType.ALL)
+ private Set tags = new HashSet<>();
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ Set getTags() {
+ return tags;
+ }
+
+ void setTags(Set tags) {
+ this.tags = tags;
+ }
+}
+
+@MappedEntity
+class Tag {
+
+ @EmbeddedId
+ private TagPk id;
+
+ TagPk getId() {
+ return id;
+ }
+
+ void setId(TagPk id) {
+ this.id = id;
+ }
+}
+
+@Embeddable
+class TagPk implements Serializable {
+
+ @NonNull
+ @Column(name = "tag")
+ private String tag;
+
+ @ManyToOne
+ @JoinColumn(name = "template_id")
+ @Column(name = "template_id")
+ private Template template;
+
+ String getTag() {
+ return tag;
+ }
+
+ void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ Template getTemplate() {
+ return template;
+ }
+
+ void setTemplate(Template template) {
+ this.template = template;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TagPk tagPk)) {
+ return false;
+ }
+ return Objects.equals(tag, tagPk.tag) && Objects.equals(template, tagPk.template);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tag, template);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedIdTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedIdTest.java
new file mode 100644
index 00000000000..52d36d20759
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedIdTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.data.annotation.Embeddable;
+import io.micronaut.data.annotation.EmbeddedId;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.MappedProperty;
+import io.micronaut.data.annotation.Relation;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.CursoredPage;
+import io.micronaut.data.model.CursoredPageable;
+import io.micronaut.data.model.Page;
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.Sort;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.repository.PageableRepository;
+import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
+import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;
+import io.micronaut.data.tck.entities.Shipment;
+import io.micronaut.data.tck.entities.ShipmentId;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import jakarta.persistence.Entity;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import jakarta.validation.constraints.NotNull;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import static io.micronaut.data.model.query.builder.sql.Dialect.SQLITE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteEmbeddedIdTest {
+
+ @Inject
+ ShipmentRepository repository;
+
+ @Inject
+ ItemGroupRepository groupRepository;
+
+ @Inject
+ ConfigurationItemRepository configurationItemRepository;
+
+ @Test
+ void testEmptyOneToManyViaEmbeddedId() {
+ ItemGroup itemGroup = new ItemGroup(1L);
+ itemGroup.setSecondId(2L);
+ groupRepository.save(itemGroup);
+
+ ItemGroup entity = groupRepository.findById(1L).orElseThrow();
+
+ assertEquals(0, entity.getItems().size());
+ }
+
+ @Test
+ void testCrud() {
+ repository.deleteAll();
+
+ ShipmentId id = new ShipmentId("a", "b");
+ repository.save(new Shipment(id, "test"));
+
+ ShipmentId id2 = new ShipmentId("c", "d");
+ repository.save(new Shipment(id2, "test2"));
+
+ ShipmentId id3 = new ShipmentId("e", "f");
+ repository.save(new Shipment(id3, "test3"));
+
+ ShipmentId id4 = new ShipmentId("g", "h");
+ repository.save(new Shipment(id4, "test4"));
+
+ Shipment entity = repository.findById(id).orElse(null);
+
+ assertEquals(4, repository.count());
+ assertNotNull(entity);
+
+ entity.setField("changed");
+ repository.update(entity);
+ entity = repository.findById(id).orElse(null);
+
+ assertNotNull(entity);
+ assertEquals("changed", entity.getField());
+ assertEquals("b", entity.getShipmentId().getCity());
+
+ repository.deleteById(id2);
+ assertEquals(3, repository.count());
+
+ repository.delete(entity);
+ assertEquals(2, repository.count());
+
+ List all = repository.findAll();
+ assertEquals(2, all.size());
+
+ Shipment foundByCountry = repository.findByShipmentIdCountry("g");
+ assertEquals("test4", foundByCountry.getField());
+ assertEquals("g", foundByCountry.getShipmentId().getCountry());
+ assertEquals("h", foundByCountry.getShipmentId().getCity());
+
+ Shipment foundByCountryAndCity = repository.findByShipmentIdCountryAndShipmentIdCity("g", "h");
+ assertEquals("test4", foundByCountryAndCity.getField());
+ assertEquals("g", foundByCountryAndCity.getShipmentId().getCountry());
+ assertEquals("h", foundByCountryAndCity.getShipmentId().getCity());
+
+ List foundAllOrderByCityDesc = repository.findAllOrderByShipmentIdCityDesc();
+ List foundAllOrderByCountryCityDesc = repository.findAllOrderByShipmentIdCountryAndShipmentIdCityDesc();
+
+ assertEquals(2, foundAllOrderByCityDesc.size());
+ assertEquals("test4", foundAllOrderByCityDesc.get(0).getField());
+ assertEquals(id4.getCountry(), foundAllOrderByCityDesc.get(0).getShipmentId().getCountry());
+ assertEquals(id4.getCity(), foundAllOrderByCityDesc.get(0).getShipmentId().getCity());
+ assertEquals("test3", foundAllOrderByCityDesc.get(1).getField());
+ assertEquals(id3.getCountry(), foundAllOrderByCityDesc.get(1).getShipmentId().getCountry());
+ assertEquals(id3.getCity(), foundAllOrderByCityDesc.get(1).getShipmentId().getCity());
+ assertEquals(2, foundAllOrderByCountryCityDesc.size());
+ assertEquals("test3", foundAllOrderByCountryCityDesc.get(0).getField());
+ assertEquals("test4", foundAllOrderByCountryCityDesc.get(1).getField());
+
+ List foundAllOrderByDynamic = repository.findAll(Sort.of(
+ Sort.Order.desc("shipmentId.country"),
+ Sort.Order.asc("shipmentId.city")
+ ));
+
+ assertEquals(2, foundAllOrderByDynamic.size());
+ assertEquals("g", foundAllOrderByDynamic.get(0).getShipmentId().getCountry());
+ assertEquals("h", foundAllOrderByDynamic.get(0).getShipmentId().getCity());
+ assertEquals("e", foundAllOrderByDynamic.get(1).getShipmentId().getCountry());
+ assertEquals("f", foundAllOrderByDynamic.get(1).getShipmentId().getCity());
+
+ repository.deleteAll(List.of(all.getFirst()));
+ assertEquals(1, repository.count());
+
+ repository.deleteAll();
+ assertEquals(0, repository.count());
+ }
+
+ @Test
+ void testCriteriaOrderOfEmbedded() {
+ repository.deleteAll();
+
+ repository.save(new Shipment(new ShipmentId("a", "b"), "test"));
+ repository.save(new Shipment(new ShipmentId("c", "d"), "test2"));
+ repository.save(new Shipment(new ShipmentId("e", "f"), "test3"));
+ repository.save(new Shipment(new ShipmentId("g", "h"), "test4"));
+
+ Sort.Order.Direction sortDirection = Sort.Order.Direction.ASC;
+ Pageable pageable = Pageable.UNPAGED.order(new Sort.Order("shipmentId.city", sortDirection, false));
+ Page page = repository.findAll(pageable);
+
+ assertEquals(4, page.getTotalSize());
+ assertEquals("b", page.getContent().getFirst().getShipmentId().getCity());
+
+ repository.deleteAll();
+ }
+
+ @Test
+ void testCursoredPageable() {
+ repository.save(new Shipment(new ShipmentId("c1", "a"), "test"));
+ repository.save(new Shipment(new ShipmentId("c1", "b"), "test2"));
+ repository.save(new Shipment(new ShipmentId("c1", "c"), "test3"));
+ repository.save(new Shipment(new ShipmentId("c1", "d"), "test4"));
+ repository.save(new Shipment(new ShipmentId("c2", "a1"), "test5"));
+
+ CursoredPageable cursoredPageable = CursoredPageable.from(3, Sort.of());
+ CursoredPage page = repository.findByShipmentIdCountry("c1", cursoredPageable);
+
+ assertEquals(3, page.getContent().size());
+ assertEquals(true, page.hasNext());
+
+ page = repository.findByShipmentIdCountry("c1", page.nextPageable());
+
+ assertEquals(1, page.getContent().size());
+ assertEquals(false, page.hasNext());
+
+ repository.deleteAll();
+ }
+
+ @Test
+ void testPagination() {
+ ConfigItemEntityId id = new ConfigItemEntityId();
+ id.setOheId("oheid1");
+ id.setId("id1");
+
+ ConfigItemEntity entity = new ConfigItemEntity();
+ entity.setId(id);
+ entity.setName("name1");
+ entity.setDescription("desc1");
+ entity.setType("type1");
+
+ ConfigItemEntity configItem = configurationItemRepository.save(entity);
+ Page page = configurationItemRepository.findAll(Pageable.from(0, 10));
+
+ assertNotNull(page);
+ assertEquals(1, page.getContent().size());
+ assertEquals("name1", page.getContent().getFirst().getName());
+
+ long cnt = configurationItemRepository.countByIdOheId(id.getOheId());
+ assertEquals(1, cnt);
+
+ PredicateSpecification idPredicate = new PredicateSpecification<>() {
+ @Override
+ public @Nullable Predicate toPredicate(@NonNull Root root,
+ @NonNull CriteriaBuilder criteriaBuilder) {
+ return criteriaBuilder.equal(root.get("id").get("oheId"), configItem.getId().getOheId());
+ }
+ };
+ List list = configurationItemRepository.findAll(idPredicate);
+ assertEquals(1, list.size());
+
+ Page newPage = configurationItemRepository.findAll(idPredicate, Pageable.from(0, 10));
+ assertEquals(1, newPage.getContent().size());
+
+ configurationItemRepository.deleteAll();
+ }
+}
+
+@Entity
+class ItemGroup {
+
+ @Id
+ private Long id;
+ private Long secondId;
+
+ @Relation(value = Relation.Kind.ONE_TO_MANY)
+ private Set- items = new HashSet<>();
+
+ ItemGroup() {
+ }
+
+ ItemGroup(Long id) {
+ this.id = id;
+ }
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ Long getSecondId() {
+ return secondId;
+ }
+
+ void setSecondId(Long secondId) {
+ this.secondId = secondId;
+ }
+
+ Set
- getItems() {
+ return items;
+ }
+
+ void setItems(Set
- items) {
+ this.items = items;
+ }
+}
+
+@Entity
+class Item {
+
+ @EmbeddedId
+ private ItemGroupId id;
+
+ ItemGroupId getId() {
+ return id;
+ }
+
+ void setId(ItemGroupId id) {
+ this.id = id;
+ }
+}
+
+@Introspected
+@Embeddable
+class ItemGroupId {
+
+ private Long firstId;
+ private Long secondId;
+
+ ItemGroupId(Long firstId, Long secondId) {
+ this.firstId = firstId;
+ this.secondId = secondId;
+ }
+
+ Long getFirstId() {
+ return firstId;
+ }
+
+ void setFirstId(Long firstId) {
+ this.firstId = firstId;
+ }
+
+ Long getSecondId() {
+ return secondId;
+ }
+
+ void setSecondId(Long secondId) {
+ this.secondId = secondId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ItemGroupId that)) {
+ return false;
+ }
+ return Objects.equals(firstId, that.firstId) && Objects.equals(secondId, that.secondId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstId, secondId);
+ }
+}
+
+@JdbcRepository(dialect = SQLITE)
+interface ItemGroupRepository extends CrudRepository
{
+
+ @Override
+ @Join(value = "items", type = Join.Type.LEFT_FETCH)
+ Optional findById(@NotNull Long id);
+}
+
+@Embeddable
+class ConfigItemEntityId {
+
+ @MappedProperty("ohe_id")
+ private String oheId;
+
+ @MappedProperty("id")
+ private String id;
+
+ String getOheId() {
+ return oheId;
+ }
+
+ void setOheId(String oheId) {
+ this.oheId = oheId;
+ }
+
+ String getId() {
+ return id;
+ }
+
+ void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConfigItemEntityId that)) {
+ return false;
+ }
+ return Objects.equals(oheId, that.oheId) && Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oheId, id);
+ }
+}
+
+@MappedEntity("CONFIGURATION_ITEM")
+class ConfigItemEntity {
+
+ @EmbeddedId
+ private ConfigItemEntityId id;
+
+ @Nullable
+ private String description;
+ private String name;
+ private String type;
+
+ ConfigItemEntityId getId() {
+ return id;
+ }
+
+ void setId(ConfigItemEntityId id) {
+ this.id = id;
+ }
+
+ @Nullable
+ String getDescription() {
+ return description;
+ }
+
+ void setDescription(@Nullable String description) {
+ this.description = description;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ String getType() {
+ return type;
+ }
+
+ void setType(String type) {
+ this.type = type;
+ }
+}
+
+@JdbcRepository(dialect = SQLITE)
+interface ConfigurationItemRepository extends PageableRepository, JpaSpecificationExecutor {
+
+ long countByIdOheId(String oheId);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedTest.java
new file mode 100644
index 00000000000..1d25d2f2261
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Address;
+import io.micronaut.data.tck.entities.Restaurant;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteEmbeddedTest {
+
+ @Inject
+ SQLiteRestaurantRepository restaurantRepository;
+
+ @Test
+ void testSaveAndRetrieveEntityWithEmbedded() {
+ restaurantRepository.save(new Restaurant("Fred's Cafe", new Address("High St.", "7896")));
+ Restaurant restaurant = restaurantRepository.save(new Restaurant("Joe's Cafe", new Address("Smith St.", "1234")));
+ restaurantRepository.save(new Restaurant("Fred's Cafe", new Address("Main St.", "2201")));
+
+ assertNotNull(restaurant);
+ assertNotNull(restaurant.getId());
+ assertEquals("Smith St.", restaurant.getAddress().getStreet());
+ assertEquals("1234", restaurant.getAddress().getZipCode());
+
+ restaurant = restaurantRepository.findByAddressStreet("Smith St.").orElse(null);
+ assertNotNull(restaurant);
+ assertEquals("Joe's Cafe", restaurant.getName());
+
+ String maxStreet = restaurantRepository.getMaxAddressStreetByName("Fred's Cafe");
+ String minStreet = restaurantRepository.getMinAddressStreetByName("Fred's Cafe");
+ assertEquals("Main St.", maxStreet);
+ assertEquals("High St.", minStreet);
+
+ restaurant = restaurantRepository.findById(restaurant.getId()).orElse(null);
+ assertNotNull(restaurant);
+ assertNotNull(restaurant.getId());
+ assertEquals("Smith St.", restaurant.getAddress().getStreet());
+ assertEquals("1234", restaurant.getAddress().getZipCode());
+ assertNull(restaurant.getHqAddress());
+
+ restaurant.setHqAddress(new Address("John St.", "4567"));
+ restaurantRepository.update(restaurant);
+ restaurant = restaurantRepository.findById(restaurant.getId()).orElse(null);
+
+ assertNotNull(restaurant);
+ assertNotNull(restaurant.getAddress());
+ assertNotNull(restaurant.getHqAddress());
+ assertEquals("John St.", restaurant.getHqAddress().getStreet());
+
+ restaurant = restaurantRepository.findByAddress(restaurant.getAddress());
+ assertEquals("Smith St.", restaurant.getAddress().getStreet());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnabledPersonRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnabledPersonRepository.java
new file mode 100644
index 00000000000..fe5bfcedfe0
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnabledPersonRepository.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Where;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Person;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+@Where("enabled = true")
+public interface SQLiteEnabledPersonRepository extends CrudRepository {
+
+ int countByNameLike(String name);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClass2Repository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClass2Repository.java
new file mode 100644
index 00000000000..572e47d86c9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClass2Repository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.EntityWithIdClass2Repository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteEntityWithIdClass2Repository extends EntityWithIdClass2Repository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClassRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClassRepository.java
new file mode 100644
index 00000000000..6d8f5765cbd
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClassRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.EntityWithIdClassRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteEntityWithIdClassRepository extends EntityWithIdClassRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnumsMappingTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnumsMappingTest.java
new file mode 100644
index 00000000000..1152410961e
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnumsMappingTest.java
@@ -0,0 +1,328 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.annotation.TypeDef;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.jdbc.runtime.JdbcOperations;
+import io.micronaut.data.model.DataType;
+import io.micronaut.data.model.PersistentEntity;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties(packages = "io.micronaut.data.jdbc.sqlite")
+class SQLiteEnumsMappingTest {
+
+ @Inject
+ EnumEntityRepository enumEntityRepository;
+
+ @Inject
+ JpaEnumEntityRepository jpaEnumEntityRepository;
+
+ @AfterEach
+ void cleanup() {
+ enumEntityRepository.deleteAll();
+ jpaEnumEntityRepository.deleteAll();
+ }
+
+ @Test
+ void testInsertsAreBrokenForCustomQueries() {
+ assertDoesNotThrow(() -> enumEntityRepository.insertValue("b", "b", 1));
+ }
+
+ @Test
+ void testReadLowerCaseEnum() {
+ enumEntityRepository.insertValueExplicit("B", "b");
+ EnumEntity result = enumEntityRepository.findByAsString(MyEnum.B);
+
+ assertNotNull(result);
+ assertEquals(MyEnum.B, result.getAsDefault());
+ assertEquals(MyEnum.B, result.getAsString());
+ }
+
+ @Test
+ void testEnums() {
+ EnumEntity entity = new EnumEntity();
+ entity.setAsDefault(MyEnum.A);
+ entity.setAsString(MyEnum.B);
+ entity.setAsInt(MyEnum.C);
+
+ entity = enumEntityRepository.save(entity);
+ entity = enumEntityRepository.findById(entity.getId()).orElseThrow();
+
+ assertEquals(MyEnum.A, entity.getAsDefault());
+ assertEquals(MyEnum.B, entity.getAsString());
+ assertEquals(MyEnum.C, entity.getAsInt());
+
+ EnumEntityDto dto = enumEntityRepository.queryById(entity.getId());
+
+ assertEquals("a", dto.getAsDefault());
+ assertEquals("b", dto.getAsString());
+ assertEquals(2, dto.getAsInt());
+
+ int updated = enumEntityRepository.update(entity.getId(), MyEnum.D, MyEnum.E, MyEnum.F);
+ entity = enumEntityRepository.findById(entity.getId()).orElseThrow();
+
+ assertEquals(1, updated);
+ assertEquals(MyEnum.D, entity.getAsDefault());
+ assertEquals(MyEnum.E, entity.getAsString());
+ assertEquals(MyEnum.F, entity.getAsInt());
+
+ Optional result = enumEntityRepository.find(MyEnum.D, MyEnum.E, MyEnum.F);
+ assertEquals(entity.getId(), result.orElseThrow().getId());
+ }
+
+ @Test
+ void jpaTestEnums() {
+ JpaEnumEntity entity = new JpaEnumEntity();
+ entity.setAsDefault(MyEnum.A);
+ entity.setAsString(MyEnum.B);
+ entity.setAsInt(MyEnum.C);
+
+ entity = jpaEnumEntityRepository.save(entity);
+ entity = jpaEnumEntityRepository.findById(entity.getId()).orElseThrow();
+
+ assertEquals(MyEnum.A, entity.getAsDefault());
+ assertEquals(MyEnum.B, entity.getAsString());
+ assertEquals(MyEnum.C, entity.getAsInt());
+
+ int updated = jpaEnumEntityRepository.update(entity.getId(), MyEnum.D, MyEnum.E, MyEnum.F);
+ entity = jpaEnumEntityRepository.findById(entity.getId()).orElseThrow();
+
+ assertEquals(1, updated);
+ assertEquals(MyEnum.D, entity.getAsDefault());
+ assertEquals(MyEnum.E, entity.getAsString());
+ assertEquals(MyEnum.F, entity.getAsInt());
+
+ Optional result = jpaEnumEntityRepository.find(MyEnum.D, MyEnum.E, MyEnum.F);
+ assertEquals(entity.getId(), result.orElseThrow().getId());
+ }
+
+ @Test
+ void testCreateTableWithEnums() {
+ SqlQueryBuilder builder = new SqlQueryBuilder(Dialect.SQLITE);
+
+ String sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(EnumEntity.class));
+
+ assertEquals("CREATE TABLE \"enum_entity\" (\"id\" INTEGER PRIMARY KEY,\"as_default\" VARCHAR(255) NOT NULL,\"as_string\" VARCHAR(255) NOT NULL,\"as_int\" INT NOT NULL);", sql);
+ }
+
+ @Test
+ void testJpaCreateTableWithEnums() {
+ SqlQueryBuilder builder = new SqlQueryBuilder(Dialect.SQLITE);
+
+ String sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(JpaEnumEntity.class));
+
+ assertEquals("CREATE TABLE \"jpa_enum_entity\" (\"id\" INTEGER PRIMARY KEY,\"as_default\" INT NOT NULL,\"as_string\" VARCHAR(255) NOT NULL,\"as_int\" INT NOT NULL);", sql);
+ }
+
+ @Test
+ void testCreateTableWithEnums2() {
+ SqlQueryBuilder builder = new SqlQueryBuilder(Dialect.SQLITE);
+
+ String sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(EnumEntity.class));
+
+ assertEquals("CREATE TABLE \"enum_entity\" (\"id\" INTEGER PRIMARY KEY,\"as_default\" VARCHAR(255) NOT NULL,\"as_string\" VARCHAR(255) NOT NULL,\"as_int\" INT NOT NULL);", sql);
+ }
+
+ @Test
+ void testJpaCreateTableWithEnums2() {
+ SqlQueryBuilder builder = new SqlQueryBuilder(Dialect.SQLITE);
+
+ String sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(JpaEnumEntity.class));
+
+ assertEquals("CREATE TABLE \"jpa_enum_entity\" (\"id\" INTEGER PRIMARY KEY,\"as_default\" INT NOT NULL,\"as_string\" VARCHAR(255) NOT NULL,\"as_int\" INT NOT NULL);", sql);
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+abstract class EnumEntityRepository implements CrudRepository {
+
+ private final JdbcOperations jdbcOperations;
+
+ EnumEntityRepository(JdbcOperations jdbcOperations) {
+ this.jdbcOperations = jdbcOperations;
+ }
+
+ @Transactional
+ void insertValueExplicit(String a, String b) {
+ jdbcOperations.prepareStatement("INSERT INTO ENUM_ENTITY(as_default, as_string, as_int) VALUES(?,?,1)", statement -> {
+ statement.setString(1, a);
+ statement.setString(2, b);
+ return statement.execute();
+ });
+ }
+
+ @Query("INSERT INTO ENUM_ENTITY(as_default, as_string, as_int) VALUES(:asDefault,:asString,:asInt)")
+ abstract void insertValue(String asDefault, String asString, int asInt);
+
+ abstract EnumEntity findByAsString(MyEnum value);
+
+ abstract int update(@Id Long id, MyEnum asDefault, MyEnum asString, MyEnum asInt);
+
+ abstract Optional find(MyEnum asDefault, MyEnum asString, MyEnum asInt);
+
+ abstract EnumEntityDto queryById(Long id);
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface JpaEnumEntityRepository extends CrudRepository {
+
+ int update(@Id Long id, MyEnum asDefault, MyEnum asString, MyEnum asInt);
+
+ Optional find(MyEnum asDefault, MyEnum asString, MyEnum asInt);
+}
+
+@MappedEntity
+class EnumEntity {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+ private MyEnum asDefault;
+
+ @TypeDef(type = DataType.STRING)
+ private MyEnum asString;
+
+ @TypeDef(type = DataType.INTEGER)
+ private MyEnum asInt;
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ MyEnum getAsDefault() {
+ return asDefault;
+ }
+
+ void setAsDefault(MyEnum asDefault) {
+ this.asDefault = asDefault;
+ }
+
+ MyEnum getAsString() {
+ return asString;
+ }
+
+ void setAsString(MyEnum asString) {
+ this.asString = asString;
+ }
+
+ MyEnum getAsInt() {
+ return asInt;
+ }
+
+ void setAsInt(MyEnum asInt) {
+ this.asInt = asInt;
+ }
+}
+
+@Introspected
+class EnumEntityDto {
+
+ private String asDefault;
+ private String asString;
+ private Object asInt;
+
+ String getAsDefault() {
+ return asDefault;
+ }
+
+ void setAsDefault(String asDefault) {
+ this.asDefault = asDefault;
+ }
+
+ String getAsString() {
+ return asString;
+ }
+
+ void setAsString(String asString) {
+ this.asString = asString;
+ }
+
+ Object getAsInt() {
+ return asInt;
+ }
+
+ void setAsInt(Object asInt) {
+ this.asInt = asInt;
+ }
+}
+
+@Entity
+class JpaEnumEntity {
+
+ @javax.persistence.Id
+ @javax.persistence.GeneratedValue
+ private Long id;
+ private MyEnum asDefault;
+
+ @Enumerated(EnumType.STRING)
+ private MyEnum asString;
+
+ @Enumerated(EnumType.ORDINAL)
+ private MyEnum asInt;
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ MyEnum getAsDefault() {
+ return asDefault;
+ }
+
+ void setAsDefault(MyEnum asDefault) {
+ this.asDefault = asDefault;
+ }
+
+ MyEnum getAsString() {
+ return asString;
+ }
+
+ void setAsString(MyEnum asString) {
+ this.asString = asString;
+ }
+
+ MyEnum getAsInt() {
+ return asInt;
+ }
+
+ void setAsInt(MyEnum asInt) {
+ this.asInt = asInt;
+ }
+}
+
+enum MyEnum {
+ A, B, C, D, E, F;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEventsTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEventsTest.java
new file mode 100644
index 00000000000..36fb31bdf7d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEventsTest.java
@@ -0,0 +1,119 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.DomainEvents;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest(rollback = false)
+@SQLiteDBProperties
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class SQLiteEventsTest {
+
+ @Inject
+ SQLiteDomainEventsRepository eventsRepository;
+
+ @Inject
+ SQLiteDomainEventsReactiveRepository eventsReactiveRepository;
+
+ private DomainEvents entityUnderTest;
+
+ @Test
+ @Order(1)
+ void testPreAndPostPersistEventTriggered() {
+ entityUnderTest = new DomainEvents();
+ entityUnderTest.setName("test");
+ eventsRepository.save(entityUnderTest);
+
+ assertCounters(entityUnderTest, 1, 1, 0, 0, 0, 0, 0);
+ }
+
+ @Test
+ @Order(2)
+ void testPostLoadEventTriggered() {
+ DomainEvents loaded = eventsRepository.findById(entityUnderTest.getUuid()).orElse(null);
+
+ assertNotNull(loaded);
+ assertCounters(loaded, 0, 0, 0, 0, 0, 0, 1);
+ }
+
+ @Test
+ @Order(3)
+ void testsPreAndPostUpdateEventsTriggered() {
+ entityUnderTest.setName("changed");
+ eventsRepository.update(entityUnderTest);
+
+ assertCounters(entityUnderTest, 1, 1, 1, 1, 0, 0, 0);
+ }
+
+ @Test
+ @Order(4)
+ void testsPreAndPostRemoveEventsTriggered() {
+ entityUnderTest.setName("changed");
+ eventsRepository.delete(entityUnderTest);
+
+ assertCounters(entityUnderTest, 1, 1, 1, 1, 1, 1, 0);
+ }
+
+ @Test
+ @Order(5)
+ void testPreAndPostPersistEventTriggeredReactive() {
+ entityUnderTest = new DomainEvents();
+ entityUnderTest.setName("test");
+ eventsReactiveRepository.save(entityUnderTest).blockingGet();
+
+ assertCounters(entityUnderTest, 1, 1, 0, 0, 0, 0, 0);
+ }
+
+ @Test
+ @Order(6)
+ void testPostLoadEventTriggeredReactive() {
+ DomainEvents loaded = eventsReactiveRepository.findById(entityUnderTest.getUuid()).blockingGet();
+
+ assertNotNull(loaded);
+ assertCounters(loaded, 0, 0, 0, 0, 0, 0, 1);
+ }
+
+ @Test
+ @Order(7)
+ void testsPreAndPostUpdateEventsTriggeredReactive() {
+ entityUnderTest.setName("changed");
+ eventsReactiveRepository.update(entityUnderTest).blockingGet();
+
+ assertCounters(entityUnderTest, 1, 1, 1, 1, 0, 0, 0);
+ }
+
+ @Test
+ @Order(8)
+ void testsPreAndPostRemoveEventsTriggeredReactive() {
+ entityUnderTest.setName("changed");
+ eventsReactiveRepository.delete(entityUnderTest).blockingAwait();
+
+ assertCounters(entityUnderTest, 1, 1, 1, 1, 1, 1, 0);
+ }
+
+ private void assertCounters(DomainEvents domainEvents,
+ int prePersist,
+ int postPersist,
+ int preUpdate,
+ int postUpdate,
+ int preRemove,
+ int postRemove,
+ int postLoad) {
+ assertEquals(prePersist, domainEvents.getPrePersist());
+ assertEquals(postPersist, domainEvents.getPostPersist());
+ assertEquals(preUpdate, domainEvents.getPreUpdate());
+ assertEquals(postUpdate, domainEvents.getPostUpdate());
+ assertEquals(preRemove, domainEvents.getPreRemove());
+ assertEquals(postRemove, domainEvents.getPostRemove());
+ assertEquals(postLoad, domainEvents.getPostLoad());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteExampleEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteExampleEntityRepository.java
new file mode 100644
index 00000000000..fcf74ebdb3e
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteExampleEntityRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.ExampleEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteExampleEntityRepository extends ExampleEntityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFaceRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFaceRepository.java
new file mode 100644
index 00000000000..2d592c1fcdc
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFaceRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.FaceRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteFaceRepository extends FaceRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFoodRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFoodRepository.java
new file mode 100644
index 00000000000..8d034617a28
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFoodRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.FoodRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteFoodRepository extends FoodRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteGenreRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteGenreRepository.java
new file mode 100644
index 00000000000..d1b2dd35a56
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteGenreRepository.java
@@ -0,0 +1,12 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
+import io.micronaut.data.tck.entities.Genre;
+import io.micronaut.data.tck.repositories.GenreRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteGenreRepository extends GenreRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteHouseEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteHouseEntityRepository.java
new file mode 100644
index 00000000000..2ca00080af8
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteHouseEntityRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.embedded.HouseEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteHouseEntityRepository extends HouseEntityRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteIntervalRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteIntervalRepository.java
new file mode 100644
index 00000000000..9b8e691a6f2
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteIntervalRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.IntervalRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteIntervalRepository extends IntervalRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJSONTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJSONTest.java
new file mode 100644
index 00000000000..65f49a0bb0c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJSONTest.java
@@ -0,0 +1,317 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Discount;
+import io.micronaut.data.tck.entities.JsonEntity;
+import io.micronaut.data.tck.entities.Sale;
+import io.micronaut.data.tck.entities.SaleDTO;
+import io.micronaut.data.tck.entities.SaleItem;
+import io.micronaut.data.tck.entities.SampleData;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteJSONTest {
+
+ @Inject
+ SQLiteSaleRepository saleRepository;
+
+ @Inject
+ SQLiteSaleItemRepository saleItemRepository;
+
+ @Inject
+ SQLiteJsonEntityRepository jsonEntityRepository;
+
+ @AfterEach
+ void cleanup() {
+ saleRepository.deleteAll();
+ saleItemRepository.deleteAll();
+ jsonEntityRepository.deleteAll();
+ }
+
+ @Test
+ void testReadAndWriteJson() {
+ Sale sale = new Sale();
+ sale.setName("test 1");
+ sale.setData(new LinkedHashMap<>(java.util.Map.of("foo", "bar")));
+ sale.setQuantities(new LinkedHashMap<>(java.util.Map.of("foo", 10)));
+ saleRepository.save(sale);
+ sale = saleRepository.findById(sale.getId()).orElse(null);
+
+ assertNotNull(sale);
+ assertEquals("test 1", sale.getName());
+ assertEquals(java.util.Map.of("foo", "bar"), sale.getData());
+ assertEquals(java.util.Map.of("foo", 10), sale.getQuantities());
+
+ sale.getData().put("foo2", "bar2");
+ saleRepository.update(sale);
+ sale = saleRepository.findById(sale.getId()).orElse(null);
+ assertNotNull(sale);
+ assertTrue(sale.getData().containsKey("foo2"));
+
+ sale.setData(new LinkedHashMap<>(java.util.Map.of("foo", "changed")));
+ saleRepository.update(sale);
+ sale = saleRepository.findById(sale.getId()).orElse(null);
+
+ assertNotNull(sale);
+ assertEquals("test 1", sale.getName());
+ assertEquals(java.util.Map.of("foo", "changed"), sale.getData());
+ assertEquals(java.util.Map.of("foo", 10), sale.getQuantities());
+
+ SaleDTO dto = saleRepository.getById(sale.getId());
+ assertEquals("test 1", dto.getName());
+ assertEquals(java.util.Map.of("foo", "changed"), dto.getData());
+ }
+
+ @Test
+ void testReadAndWriteWithUpdated() {
+ Sale sale = new Sale();
+ sale.setName("test 1");
+ sale.setData(new LinkedHashMap<>(java.util.Map.of("foo", "bar")));
+ sale.setDataList(List.of("abc1", "abc2"));
+ saleRepository.save(sale);
+ sale = saleRepository.findById(sale.getId()).orElse(null);
+
+ assertNotNull(sale);
+ assertEquals("test 1", sale.getName());
+ assertEquals(java.util.Map.of("foo", "bar"), sale.getData());
+ assertEquals(List.of("abc1", "abc2"), sale.getDataList());
+
+ saleRepository.updateData(sale.getId(), java.util.Map.of("foo", "changed"), List.of("changed1", "changed2", "changed3"));
+ sale = saleRepository.findById(sale.getId()).orElse(null);
+
+ assertNotNull(sale);
+ assertEquals("test 1", sale.getName());
+ assertEquals(java.util.Map.of("foo", "changed"), sale.getData());
+ assertEquals(List.of("changed1", "changed2", "changed3"), sale.getDataList());
+ }
+
+ @Test
+ void testReadWriteJsonWithStringField() {
+ Sale sale = new Sale();
+ sale.setName("sale");
+ String extraData = "{\"color\":\"blue\"}";
+ sale.setExtraData(extraData);
+
+ saleRepository.save(sale);
+
+ assertEquals(extraData, saleRepository.findById(sale.getId()).orElseThrow().getExtraData());
+ }
+
+ @Test
+ void testReadAndWriteJsonWithChildRows() {
+ Sale sale = new Sale();
+ sale.setName("test 1");
+ sale = saleRepository.save(sale);
+ List items = saleItemRepository.saveAll(List.of(
+ new SaleItem(null, sale, "item 1", java.util.Map.of("count", "1")),
+ new SaleItem(null, sale, "item 2", java.util.Map.of("count", "2")),
+ new SaleItem(null, sale, "item 3", java.util.Map.of("count", "3"))
+ ));
+
+ Sale saleById = saleRepository.findById(sale.getId()).orElse(null);
+ assertNotNull(saleById);
+ assertEquals("test 1", saleById.getName());
+ assertEquals(items.size(), saleById.getItems().size());
+ assertEquals(
+ items.stream()
+ .map(item -> Map.of(
+ "id", item.getId(),
+ "name", item.getName(),
+ "data", item.getData()
+ ))
+ .collect(java.util.stream.Collectors.toSet()),
+ saleById.getItems().stream()
+ .map(item -> Map.of(
+ "id", item.getId(),
+ "name", item.getName(),
+ "data", item.getData()
+ ))
+ .collect(java.util.stream.Collectors.toSet())
+ );
+ }
+
+ @Test
+ void testReadAndWriteJsonWithConstructorArgs() {
+ Sale sale = new Sale();
+ sale.setName("test 1");
+ sale = saleRepository.save(sale);
+ SaleItem item = saleItemRepository.save(new SaleItem(null, sale, "item 1", java.util.Map.of("count", "1")));
+
+ SaleItem itemById = saleItemRepository.findById(item.getId()).orElse(null);
+ assertNotNull(itemById);
+ assertEquals("item 1", itemById.getName());
+ assertEquals(java.util.Map.of("count", "1"), itemById.getData());
+ assertEquals(sale.getId(), itemById.getSale().getId());
+ }
+
+ @Test
+ void testReadDtoFromJsonStringField() {
+ Discount discount = new Discount();
+ discount.setAmount(12d);
+ discount.setNumberOfDays(5);
+ discount.setNote("Valid since April 1st");
+
+ Sale sale = new Sale();
+ sale.setName("sale");
+ sale.setExtraData("{\"amount\":12.0,\"numberOfDays\":5,\"note\":\"Valid since April 1st\"}");
+
+ sale = saleRepository.save(sale);
+ Optional optSale = saleRepository.findById(sale.getId());
+ Optional optLoadedDiscount = saleRepository.getDiscountById(sale.getId());
+
+ assertTrue(optSale.isPresent());
+ assertTrue(optLoadedDiscount.isPresent());
+ Discount loadedDiscount = optLoadedDiscount.orElseThrow();
+ assertEquals(discount.getAmount(), loadedDiscount.getAmount());
+ assertEquals(discount.getNote(), loadedDiscount.getNote());
+ assertEquals(discount.getNumberOfDays(), loadedDiscount.getNumberOfDays());
+ }
+
+ @Test
+ void testReadEntityFromJsonStringField() {
+ Sale sale1 = new Sale();
+ sale1.setName("sale1");
+ sale1.setData(new LinkedHashMap<>(java.util.Map.of("sale1_field1", "value1")));
+ sale1.setDataList(List.of("sale1_data1", "sale2_data2"));
+ sale1.setQuantities(new LinkedHashMap<>(java.util.Map.of("sale1_item1", 3, "sale1_item2", 2)));
+ sale1 = saleRepository.save(sale1);
+ SaleItem item1 = saleItemRepository.save(new SaleItem(null, sale1, "sale1 item 1", java.util.Map.of("count", "1")));
+ SaleItem item2 = saleItemRepository.save(new SaleItem(null, sale1, "sale1 item 2", java.util.Map.of("count", "2")));
+
+ sale1 = saleRepository.findById(sale1.getId()).orElseThrow();
+ sale1.setExtraData("{"
+ + "\"id\":" + sale1.getId() + ","
+ + "\"name\":\"sale1\","
+ + "\"data\":{\"sale1_field1\":\"value1\"},"
+ + "\"quantities\":{\"sale1_item1\":3,\"sale1_item2\":2},"
+ + "\"dataList\":[\"sale1_data1\",\"sale2_data2\"],"
+ + "\"items\":["
+ + "{\"id\":" + item1.getId() + ",\"name\":\"sale1 item 1\",\"data\":{\"count\":\"1\"}},"
+ + "{\"id\":" + item2.getId() + ",\"name\":\"sale1 item 2\",\"data\":{\"count\":\"2\"}}"
+ + "]"
+ + "}");
+ saleRepository.update(sale1);
+
+ List loadedSales = saleRepository.findAllByNameFromJson(sale1.getName());
+ assertEquals(1, loadedSales.size());
+ verifySale(sale1, loadedSales.getFirst());
+
+ Optional optLoadedSale = saleRepository.findByNameFromJson(sale1.getName());
+ assertTrue(optLoadedSale.isPresent());
+ verifySale(sale1, optLoadedSale.orElseThrow());
+
+ optLoadedSale = saleRepository.findByName(sale1.getName());
+ assertTrue(optLoadedSale.isPresent());
+ verifySale(sale1, optLoadedSale.orElseThrow());
+ }
+
+ @Test
+ void testSaveUpdateIterable() {
+ JsonEntity a = new JsonEntity();
+ a.setId(1L);
+ a.setValues(List.of("item1", "item2"));
+ jsonEntityRepository.save(a);
+
+ JsonEntity loaded = jsonEntityRepository.findById(1L).orElseThrow();
+ List loadedValues = new ArrayList<>();
+ loaded.getValues().forEach(loadedValues::add);
+ assertEquals(List.of("item1", "item2"), loadedValues);
+
+ loadedValues.add("item3");
+ loaded.setValues(loadedValues);
+ jsonEntityRepository.update(loaded);
+ loaded = jsonEntityRepository.findById(1L).orElseThrow();
+ loadedValues = new ArrayList<>();
+ loaded.getValues().forEach(loadedValues::add);
+ assertEquals(List.of("item1", "item2", "item3"), loadedValues);
+
+ JsonEntity b = jsonEntityRepository.save(2L, List.of("newitem1", "newitem2", "newitem3"));
+ loaded = jsonEntityRepository.findById(2L).orElseThrow();
+ List bValues = new ArrayList<>();
+ b.getValues().forEach(bValues::add);
+ assertEquals(2L, b.getId());
+ assertEquals(List.of("newitem1", "newitem2", "newitem3"), bValues);
+ loadedValues = new ArrayList<>();
+ loaded.getValues().forEach(loadedValues::add);
+ assertEquals(List.of("newitem1", "newitem2", "newitem3"), loadedValues);
+
+ loadedValues.set(1, "newitem2_updated");
+ jsonEntityRepository.update(loaded.getId(), loadedValues);
+ loaded = jsonEntityRepository.findById(2L).orElseThrow();
+ loadedValues = new ArrayList<>();
+ loaded.getValues().forEach(loadedValues::add);
+ assertEquals(List.of("newitem1", "newitem2_updated", "newitem3"), loadedValues);
+ }
+
+ @Disabled("JSON FORMAT not supported")
+ @Test
+ void testJsonFieldsRetrieval() {
+ JsonEntity jsonEntity = new JsonEntity();
+ jsonEntity.setId(1L);
+ SampleData sampleData = new SampleData();
+ sampleData.setEtag(UUID.randomUUID().toString());
+ sampleData.setMemo("memo".getBytes(Charset.defaultCharset()));
+ sampleData.setUuid(UUID.randomUUID());
+ sampleData.setDuration(Duration.ofHours(15));
+ sampleData.setLocalDateTime(LocalDateTime.now());
+ sampleData.setDescription("sample description");
+ sampleData.setGrade(1);
+ sampleData.setRating(9.75d);
+ jsonEntity.setJsonDefault(sampleData);
+ jsonEntity.setJsonBlob(sampleData);
+ jsonEntity.setJsonString(sampleData);
+ jsonEntityRepository.save(jsonEntity);
+
+ Optional optSampleDataFromJsonDefault = jsonEntityRepository.findJsonDefaultById(jsonEntity.getId());
+ Optional optSampleDataFromJsonString = jsonEntityRepository.findJsonStringById(jsonEntity.getId());
+ Optional optSampleDataFromJsonBlob = jsonEntityRepository.findJsonBlobById(jsonEntity.getId());
+
+ assertTrue(optSampleDataFromJsonDefault.isPresent() && optSampleDataFromJsonDefault.orElseThrow().equals(sampleData));
+ assertTrue(optSampleDataFromJsonString.isPresent() && optSampleDataFromJsonString.orElseThrow().equals(sampleData));
+ assertTrue(optSampleDataFromJsonBlob.isPresent() && optSampleDataFromJsonBlob.orElseThrow().equals(sampleData));
+
+ JsonEntity loadedJsonEntity = jsonEntityRepository.findById(jsonEntity.getId()).orElseThrow();
+ assertEquals(jsonEntity.getId(), loadedJsonEntity.getId());
+ assertEquals(jsonEntity.getJsonString(), loadedJsonEntity.getJsonString());
+ assertEquals(jsonEntity.getJsonBlob(), loadedJsonEntity.getJsonBlob());
+ assertEquals(jsonEntity.getJsonDefault(), loadedJsonEntity.getJsonDefault());
+
+ jsonEntity.getJsonString().setDescription("Updated via param");
+ jsonEntity.getJsonBlob().setGrade(15);
+ jsonEntityRepository.updateJsonStringById(jsonEntity.getId(), jsonEntity.getJsonString());
+ jsonEntityRepository.updateJsonBlobById(jsonEntity.getId(), jsonEntity.getJsonBlob());
+ optSampleDataFromJsonString = jsonEntityRepository.findJsonStringById(jsonEntity.getId());
+ optSampleDataFromJsonBlob = jsonEntityRepository.findJsonBlobById(jsonEntity.getId());
+
+ assertTrue(optSampleDataFromJsonString.isPresent());
+ assertEquals("Updated via param", optSampleDataFromJsonString.orElseThrow().getDescription());
+ assertTrue(optSampleDataFromJsonBlob.isPresent());
+ assertEquals(15, optSampleDataFromJsonBlob.orElseThrow().getGrade());
+ }
+
+ private void verifySale(Sale actualSale, Sale expectedSale) {
+ assertEquals(actualSale.getId(), expectedSale.getId());
+ assertEquals(actualSale.getName(), expectedSale.getName());
+ assertEquals(actualSale.getDataList(), expectedSale.getDataList());
+ assertEquals(actualSale.getQuantities(), expectedSale.getQuantities());
+ assertEquals(actualSale.getItems().size(), expectedSale.getItems().size());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJakartaDataTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJakartaDataTest.java
new file mode 100644
index 00000000000..0f95b2f64e3
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJakartaDataTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.tests.AbstractJakartaDataTest;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@MicronautTest(transactional = false)
+public class SQLiteJakartaDataTest extends AbstractJakartaDataTest implements SQLiteTestingPropertyProvider {
+ @Disabled("LEFT Function not supported in SQLite")
+ @Test
+ public void testRuntimeRestrictionsWithLeft() {
+ super.testRuntimeRestrictionsWithLeft();
+ }
+
+ @Disabled("RIGHT Function not supported in SQLite")
+ @Test
+ public void testRuntimeRestrictionsWithRight() {
+ super.testRuntimeRestrictionsWithRight();
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJoinFetchTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJoinFetchTest.java
new file mode 100644
index 00000000000..4f41376303d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJoinFetchTest.java
@@ -0,0 +1,142 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.AuthorBooksDto;
+import io.micronaut.data.tck.entities.BookDto;
+import io.micronaut.data.tck.repositories.AuthorJoinTypeRepositories;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteJoinFetchTest {
+
+ @Inject
+ ApplicationContext context;
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @Inject
+ SQLiteAuthorRepository authorRepository;
+
+ @Inject
+ SQLiteAuthorJoinLeftFetchRepository authorJoinLeftFetchRepository;
+
+ @Inject
+ SQLiteAuthorJoinLeftRepository authorJoinLeftRepository;
+
+ @Inject
+ SQLiteAuthorJoinRightFetchRepository authorJoinRightFetchRepository;
+
+ @Inject
+ SQLiteAuthorJoinRightRepository authorJoinRightRepository;
+
+ @Inject
+ SQLiteAuthorJoinInnerRepository authorJoinInnerRepository;
+
+ @BeforeEach
+ void setup() {
+ saveSampleBooks();
+ }
+
+ @AfterEach
+ void cleanup() {
+ bookRepository.deleteAll();
+ authorRepository.deleteAll();
+ }
+
+ @Test
+ void leftJoinDoesNotFetchProjectedEntities() {
+ var authors = authorJoinLeftRepository.findAll();
+
+ assertFalse(authors.isEmpty());
+ assertTrue(authors.getFirst().getBooks().isEmpty());
+ }
+
+ @Test
+ void leftFetchJoinFetchesProjectedEntities() {
+ var authors = authorJoinLeftFetchRepository.findAll();
+
+ assertFalse(authors.isEmpty());
+ var titles = authors.getFirst().getBooks().stream().map(book -> book.getTitle()).toList();
+ assertTrue(titles.containsAll(Arrays.asList("The Stand", "Pet Sematary")));
+ }
+
+ @Test
+ void rightJoinDoesNotFetchProjectedEntities() {
+ var authors = authorJoinRightRepository.findAll();
+
+ assertFalse(authors.isEmpty());
+ assertTrue(authors.getFirst().getBooks().isEmpty());
+ }
+
+ @Test
+ void rightFetchJoinFetchesProjectedEntities() {
+ var authors = authorJoinRightFetchRepository.findAll();
+
+ assertFalse(authors.isEmpty());
+ var titles = authors.getFirst().getBooks().stream().map(book -> book.getTitle()).toList();
+ assertTrue(titles.containsAll(Arrays.asList("The Stand", "Pet Sematary")));
+ }
+
+ @Test
+ void fetchJoinFetchesProjectedEntities() {
+ var authors = context.createBean(SQLiteAuthorJoinFetchRepository.class).findAll();
+
+ assertFalse(authors.isEmpty());
+ var titles = authors.getFirst().getBooks().stream().map(book -> book.getTitle()).toList();
+ assertTrue(titles.containsAll(Arrays.asList("The Stand", "Pet Sematary")));
+ }
+
+ @Test
+ void innerJoinDoesNotFetchProjectedEntities() {
+ var authors = authorJoinInnerRepository.findAll();
+
+ assertFalse(authors.isEmpty());
+ assertTrue(authors.getFirst().getBooks().isEmpty());
+ }
+
+ private void saveSampleBooks() {
+ bookRepository.saveAuthorBooks(Arrays.asList(
+ new AuthorBooksDto("Stephen King", Arrays.asList(
+ new BookDto("The Stand", 1000),
+ new BookDto("Pet Sematary", 400)
+ ))
+ ));
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinFetchRepository extends AuthorJoinTypeRepositories.AuthorJoinFetchRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinInnerRepository extends AuthorJoinTypeRepositories.AuthorJoinInnerRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinLeftFetchRepository extends AuthorJoinTypeRepositories.AuthorJoinLeftFetchRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinLeftRepository extends AuthorJoinTypeRepositories.AuthorJoinLeftRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinRightFetchRepository extends AuthorJoinTypeRepositories.AuthorJoinRightFetchRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteAuthorJoinRightRepository extends AuthorJoinTypeRepositories.AuthorJoinRightRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJsonEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJsonEntityRepository.java
new file mode 100644
index 00000000000..4325e21590b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJsonEntityRepository.java
@@ -0,0 +1,15 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.SampleData;
+import io.micronaut.data.tck.repositories.JsonEntityRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteJsonEntityRepository extends JsonEntityRepository {
+
+ @Query("UPDATE json_entity SET json_blob = :jsonBlob FORMAT JSON WHERE id = :id")
+ @Override
+ void updateJsonBlobById(Long id, SampleData jsonBlob);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteManualSchemaTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteManualSchemaTest.java
new file mode 100644
index 00000000000..685ee54e5af
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteManualSchemaTest.java
@@ -0,0 +1,123 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.tck.entities.Patient;
+import io.micronaut.inject.qualifiers.Qualifiers;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SQLiteManualSchemaTest {
+
+ @Test
+ void testSaveAndLoadRecordWhenIdNotFirstFieldInTheTable() throws Exception {
+ try (ApplicationContext context = ApplicationContext.run(createProperties())) {
+ DataSource dataSource = DelegatingDataSource.unwrapDataSource(context.getBean(DataSource.class, Qualifiers.byName("default")));
+ SQLitePatientRepository patientRepository = context.getBean(SQLitePatientRepository.class);
+
+ createSchema(dataSource);
+
+ Patient patient = new Patient();
+ patient.setName("Patient1");
+ patient.setHistory("Enter some details");
+ patientRepository.save(patient);
+
+ var optPatient = patientRepository.findById(patient.getId());
+ assertTrue(optPatient.isPresent());
+ assertEquals(patient.getId(), optPatient.orElseThrow().getId());
+
+ dropSchema(dataSource);
+ }
+ }
+
+ @Disabled("FORMAT JSON")
+ @Test
+ void testManualInsertAndDtoRetrieval() throws Exception {
+ try (ApplicationContext context = ApplicationContext.run(createProperties())) {
+ DataSource dataSource = DelegatingDataSource.unwrapDataSource(context.getBean(DataSource.class, Qualifiers.byName("default")));
+ SQLitePatientRepository patientRepository = context.getBean(SQLitePatientRepository.class);
+
+ createSchema(dataSource);
+ String name = "pt1";
+ String history = "flu";
+ String doctorNotes = "mild";
+ List appointments = List.of("Dr1 April 2022", "Dr2 June 2022");
+ insertRecord(dataSource, name, history, doctorNotes);
+ patientRepository.updateAppointmentsByName(name, appointments);
+
+ var patientDtos = patientRepository.findAllByNameWithQuery(name);
+ assertEquals(1, patientDtos.size());
+ assertEquals(name, patientDtos.getFirst().getName());
+ assertEquals(history, patientDtos.getFirst().getHistory());
+ assertEquals(doctorNotes, patientDtos.getFirst().getDoctorNotes());
+ assertEquals(appointments, patientDtos.getFirst().getAppointments());
+
+ var optPatientDto = patientRepository.findByNameWithQuery(name);
+ assertTrue(optPatientDto.isPresent());
+ var patientDto = optPatientDto.orElseThrow();
+ assertEquals(name, patientDto.getName());
+ assertEquals(history, patientDto.getHistory());
+ assertEquals(doctorNotes, patientDto.getDoctorNotes());
+ assertEquals(appointments, patientDto.getAppointments());
+
+ dropSchema(dataSource);
+ }
+ }
+
+ private void createSchema(DataSource dataSource) throws Exception {
+ try (Connection connection = dataSource.getConnection()) {
+ for (String statement : List.of("CREATE TABLE patient (name TEXT,id INTEGER PRIMARY KEY,history TEXT,doctor_notes TEXT,appointments TEXT);")) {
+ connection.prepareStatement(statement).executeUpdate();
+ }
+ }
+ }
+
+ private void dropSchema(DataSource dataSource) throws Exception {
+ try (Connection connection = dataSource.getConnection()) {
+ for (String statement : List.of("DROP TABLE patient")) {
+ connection.prepareStatement(statement).executeUpdate();
+ }
+ }
+ }
+
+ private void insertRecord(DataSource dataSource, String name, String history, String doctorNotes) throws Exception {
+ try (Connection connection = dataSource.getConnection()) {
+ var insertStmt = connection.prepareStatement("INSERT INTO patient (name, history, doctor_notes) VALUES (?, ?, ?)");
+ insertStmt.setString(1, name);
+ insertStmt.setString(2, history);
+ insertStmt.setString(3, doctorNotes);
+ assertEquals(1, insertStmt.executeUpdate());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqlitemanualschema", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "NONE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMappedEntityTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMappedEntityTest.java
new file mode 100644
index 00000000000..92e4737392d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMappedEntityTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteMappedEntityTest {
+
+ @Inject
+ SQLiteDoubleImplement1Repository di1;
+
+ @Inject
+ SQLiteDoubleImplement2Repository di2;
+
+ @Inject
+ SQLiteDoubleImplement3Repository di3;
+
+ @Test
+ void testMappedEntitiesWithMultipleInterfaces() {
+ assertNotNull(di1.get());
+ assertNotNull(di2.get());
+ assertNotNull(di3.get());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMealRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMealRepository.java
new file mode 100644
index 00000000000..f6e16e3a989
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMealRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Meal;
+import io.micronaut.data.tck.repositories.MealRepository;
+import io.micronaut.validation.Validated;
+import jakarta.validation.Valid;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteMealRepository extends MealRepository, CrudRepository<@Valid Meal, Long> {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMultitenancyTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMultitenancyTest.java
new file mode 100644
index 00000000000..32c7ae090cf
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMultitenancyTest.java
@@ -0,0 +1,95 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.env.Environment;
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.tck.tests.BarBookClient;
+import io.micronaut.data.tck.tests.BookDto;
+import io.micronaut.data.tck.tests.FooBookClient;
+import io.micronaut.runtime.server.EmbeddedServer;
+import org.junit.jupiter.api.Test;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class SQLiteMultitenancyTest {
+
+ @Test
+ void testDatasourceMultitenancy() {
+ Map properties = new HashMap<>();
+ properties.putAll(createDataSourceProperties("foo"));
+ properties.putAll(createDataSourceProperties("bar"));
+ properties.put("bookRepositoryClass", SQLiteBookRepository.class.getName());
+ properties.put("spec.name", "multitenancy");
+ properties.put("micronaut.data.multi-tenancy.mode", "DATASOURCE");
+ properties.put("micronaut.multitenancy.tenantresolver.httpheader.enabled", "true");
+
+ try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties, Environment.TEST)) {
+ ApplicationContext context = embeddedServer.getApplicationContext();
+ FooBookClient fooBookClient = context.getBean(FooBookClient.class);
+ BarBookClient barBookClient = context.getBean(BarBookClient.class);
+
+ fooBookClient.deleteAll();
+ barBookClient.deleteAll();
+ assertEquals(2, context.getBeansOfType(DataSource.class).size());
+
+ BookDto book = fooBookClient.save("The Stand", 1000);
+ assertNotNull(book.getId());
+
+ book = fooBookClient.findOne(book.getId()).orElse(null);
+ assertNotNull(book);
+ assertEquals("The Stand", book.getTitle());
+ assertEquals(1, fooBookClient.findAll().size());
+ assertEquals(0, barBookClient.findAll().size());
+ assertEquals(1, getBooksCount(context.getBean(DataSource.class, io.micronaut.inject.qualifiers.Qualifiers.byName("foo"))));
+ assertEquals(0, getBooksCount(context.getBean(DataSource.class, io.micronaut.inject.qualifiers.Qualifiers.byName("bar"))));
+
+ barBookClient.deleteAll();
+ assertEquals(1, fooBookClient.findAll().size());
+
+ fooBookClient.deleteAll();
+ assertEquals(0, fooBookClient.findAll().size());
+ }
+ }
+
+ private long getBooksCount(DataSource dataSource) {
+ try (Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection();
+ PreparedStatement ps = connection.prepareStatement("select count(*) from book");
+ ResultSet resultSet = ps.executeQuery()) {
+ resultSet.next();
+ return resultSet.getLong(1);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Map createDataSourceProperties(String dataSourceName) {
+ try {
+ var databaseFile = Files.createTempFile(dataSourceName.toLowerCase(), ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ String prefix = "datasources." + dataSourceName;
+ properties.put(prefix + ".url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put(prefix + ".schema-generate", "CREATE");
+ properties.put(prefix + ".dialect", "SQLITE");
+ properties.put(prefix + ".db-type", "sqlite");
+ properties.put(prefix + ".username", "");
+ properties.put(prefix + ".password", "");
+ properties.put(prefix + ".packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put(prefix + ".driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepository.java
new file mode 100644
index 00000000000..077ac97a3c7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteNoIdEntityRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepositoryTest.java
new file mode 100644
index 00000000000..22ff1a2cc66
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepositoryTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017-2023 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteNoIdEntityRepositoryTest {
+
+ @Inject
+ SQLiteNoIdEntityRepository noIdEntityRepository;
+
+ @Test
+ void testTheRepositoryWorks() {
+ assertTrue(noIdEntityRepository.findById(123456L).isEmpty());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryBehaviorTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryBehaviorTest.java
new file mode 100644
index 00000000000..fecd59b1b7b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryBehaviorTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class SQLiteNoTxOpsRepositoryBehaviorTest extends AbstractSQLiteRepositoryBehaviorTest {
+
+ @Override
+ public Map getProperties() {
+ Map properties = new HashMap<>(super.getProperties());
+ properties.put("micronaut.data.jdbc.transaction-per-operation", "false");
+ properties.put("micronaut.data.jdbc.allow-connection-per-operation", "true");
+ return properties;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryTest.java
new file mode 100644
index 00000000000..ced7f1ad7d0
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SQLiteNoTxOpsRepositoryTest extends SQLiteRepositoryTest {
+
+ @Override
+ public Map getProperties() {
+ Map properties = new HashMap<>(super.getProperties());
+ properties.put("micronaut.data.jdbc.transaction-per-operation", "false");
+ properties.put("micronaut.data.jdbc.allow-connection-per-operation", "true");
+ return properties;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoseRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoseRepository.java
new file mode 100644
index 00000000000..3f344931107
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoseRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.NoseRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteNoseRepository extends NoseRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNullableConstructorTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNullableConstructorTest.java
new file mode 100644
index 00000000000..837439a3dbd
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNullableConstructorTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Plant;
+import io.micronaut.data.tck.repositories.PlantRepository;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteNullableConstructorTest {
+
+ @Inject
+ PlantRepository plantRepository;
+
+ @Test
+ void testSaveAndRetrieveNullableAssociation() {
+ Plant plant = plantRepository.save(new Plant("Orange", null));
+ assertNotNull(plant.getId());
+
+ plant = plantRepository.findById(plant.getId());
+ assertNotNull(plant.getId());
+ assertEquals("Orange", plant.getName());
+ assertNull(plant.getNursery());
+ assertNull(plant.getMaxHeight());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrderTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrderTest.java
new file mode 100644
index 00000000000..1a9bc36b068
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrderTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.Sort;
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteOrderTest {
+
+ @Inject
+ SQLitePersonRepository personRepository;
+
+ @Test
+ void testOrderCaseInsensitive() {
+ personRepository.save(person("ABC4"));
+ personRepository.save(person("abc3"));
+ personRepository.save(person("abc2"));
+ personRepository.save(person("ABC1"));
+
+ Sort.Order order = new Sort.Order("name", Sort.Order.Direction.ASC, true);
+ var list = personRepository.list(Pageable.from(0, 10).order(order));
+
+ assertEquals("ABC1", list.get(0).getName());
+ assertEquals("abc2", list.get(1).getName());
+ assertEquals("abc3", list.get(2).getName());
+ assertEquals("ABC4", list.get(3).getName());
+ }
+
+ private static Person person(String name) {
+ Person person = new Person();
+ person.setName(name);
+ return person;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrganizationRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrganizationRepository.java
new file mode 100644
index 00000000000..362a4617542
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrganizationRepository.java
@@ -0,0 +1,8 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteOrganizationRepository extends OrganizationRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePageRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePageRepository.java
new file mode 100644
index 00000000000..ee910ff59b4
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePageRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.PageRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLitePageRepository extends PageRepository {}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePaginationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePaginationTest.java
new file mode 100644
index 00000000000..b838094f600
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePaginationTest.java
@@ -0,0 +1,195 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.model.Page;
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.Sort;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLitePaginationTest {
+
+ @Inject
+ SQLitePersonRepository personRepository;
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @BeforeEach
+ void setup() {
+ personRepository.deleteAll();
+
+ List people = new ArrayList<>();
+ for (int num = 0; num < 50; num++) {
+ for (char c = 'A'; c <= 'Z'; c++) {
+ Person person = new Person();
+ person.setName(String.valueOf(c).repeat(5) + num);
+ people.add(person);
+ }
+ }
+
+ personRepository.saveAll(people);
+ }
+
+ @AfterEach
+ void cleanup() {
+ personRepository.deleteAll();
+ }
+
+ @Test
+ void testSort() {
+ var results = personRepository.listTop10(Sort.unsorted().order("name", Sort.Order.Direction.DESC));
+
+ assertEquals(10, results.size());
+ assertTrue(results.getFirst().getName().startsWith("Z"));
+ }
+
+ @Test
+ void testPageableList() {
+ assertEquals(1300, personRepository.count());
+
+ Pageable pageable = Pageable.from(0, 10);
+ Page page = personRepository.findAll(pageable);
+
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.getContent().get(0).getName().startsWith("A"));
+ assertTrue(page.getContent().get(1).getName().startsWith("B"));
+ assertEquals(1300, page.getTotalSize());
+ assertEquals(130, page.getTotalPages());
+ assertEquals(10, page.nextPageable().getOffset());
+ assertEquals(10, page.nextPageable().getSize());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasTotalSize());
+ assertFalse(page.hasPrevious());
+
+ page = personRepository.findAll(page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertTrue(page.getContent().get(0).getName().startsWith("K"));
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertTrue(page.hasPrevious());
+
+ page = personRepository.findAll(page.previousPageable());
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertTrue(page.getContent().get(0).getName().startsWith("A"));
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.hasNext());
+ assertFalse(page.hasPrevious());
+ }
+
+ @Test
+ void testPageableListWithoutTotalCount() {
+ Pageable pageable = Pageable.from(0, 10).withoutTotal();
+ Page page = personRepository.findAll(pageable);
+
+ assertEquals(10, page.getContent().size());
+ assertFalse(page.hasTotalSize());
+ assertEquals(0, page.getTotalPages());
+ assertEquals(-1, page.getTotalSize());
+ }
+
+ @Test
+ void testPageableSort() {
+ assertEquals(1300, personRepository.count());
+
+ Page page = personRepository.findAll(
+ Pageable.from(0, 10).order("name", Sort.Order.Direction.DESC)
+ );
+
+ assertEquals(10, page.getContent().size());
+ assertTrue(page.getContent().get(0).getName().startsWith("Z"));
+ assertTrue(page.getContent().get(1).getName().startsWith("Z"));
+ assertEquals(1300, page.getTotalSize());
+ assertEquals(130, page.getTotalPages());
+ assertEquals(10, page.nextPageable().getOffset());
+ assertEquals(10, page.nextPageable().getSize());
+
+ page = personRepository.findAll(page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertTrue(page.getContent().get(0).getName().startsWith("Z"));
+ }
+
+ @Test
+ void testPageableFindBy() {
+ Pageable pageable = Pageable.from(0, 10);
+ Page page = personRepository.findByNameLike("A%", pageable);
+ Page page2 = personRepository.findPeople("A%", pageable);
+ var slice = personRepository.queryByNameLike("A%", pageable);
+
+ assertEquals(0, page.getOffset());
+ assertEquals(0, page.getPageNumber());
+ assertEquals(50, page.getTotalSize());
+ assertEquals(page.getTotalSize(), page2.getTotalSize());
+ assertEquals(0, slice.getOffset());
+ assertEquals(0, slice.getPageNumber());
+ assertEquals(10, slice.getSize());
+ assertFalse(slice.getContent().isEmpty());
+ assertFalse(page.getContent().isEmpty());
+
+ page = personRepository.findByNameLike("A%", page.nextPageable());
+
+ assertEquals(10, page.getOffset());
+ assertEquals(1, page.getPageNumber());
+ assertEquals(50, page.getTotalSize());
+ assertEquals(20, page.nextPageable().getOffset());
+ assertEquals(2, page.nextPageable().getNumber());
+ }
+
+ @Test
+ void testTotalSizeOfFindWithLeftJoin() {
+ var books = bookRepository.saveAll(List.of(
+ book("Book 1", 100),
+ book("Book 2", 100)
+ ));
+
+ var page = bookRepository.findByTotalPagesGreaterThan(0, Pageable.from(0, books.size()));
+
+ assertEquals(books.size(), page.getContent().size());
+ assertEquals(books.size(), page.getTotalSize());
+
+ bookRepository.deleteAll();
+ }
+
+ @Test
+ void testPagingWithCriteriaAndLimit() {
+ bookRepository.saveAll(List.of(
+ book("Book 1", 100),
+ book("Book 2", 100),
+ book("Book 3", 100),
+ book("Book 4", 200)
+ ));
+
+ var page = bookRepository.findBooksByTotalPages(100, Pageable.from(0, 2));
+
+ assertEquals(3, page.getTotalSize());
+ assertEquals(2, page.getContent().size());
+
+ bookRepository.deleteAll();
+ }
+
+ private Book book(String title, int totalPages) {
+ Book book = new Book();
+ book.setTitle(title);
+ book.setTotalPages(totalPages);
+ return book;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePatientRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePatientRepository.java
new file mode 100644
index 00000000000..328df8e31d9
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePatientRepository.java
@@ -0,0 +1,19 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.annotation.Parameter;
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.annotation.TypeDef;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.DataType;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.PatientRepository;
+
+import java.util.List;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLitePatientRepository extends PatientRepository {
+
+ @Override
+ @Query("UPDATE patient SET appointments = :appointments FORMAT JSON WHERE name = :name")
+ void updateAppointmentsByName(@Parameter String name, @TypeDef(type = DataType.JSON) List appointments);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePersonRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePersonRepository.java
new file mode 100644
index 00000000000..042f7957f59
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePersonRepository.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.jdbc.runtime.JdbcOperations;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.Person;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public abstract class SQLitePersonRepository implements io.micronaut.data.tck.repositories.PersonRepository {
+
+ private final JdbcOperations jdbcOperations;
+
+ public SQLitePersonRepository(JdbcOperations jdbcOperations) {
+ this.jdbcOperations = jdbcOperations;
+ }
+
+ @Override
+ public abstract Person save(String name, int age);
+
+ @Override
+ @Query("INSERT INTO person(name, age, enabled) VALUES (:name, :age, TRUE)")
+ public abstract int saveCustom(String name, int age);
+
+ public Stream> findAllAndStream() {
+ return jdbcOperations.prepareStatement("SELECT * from person order by name asc", statement -> {
+ statement.setFetchSize(5000);
+ ResultSet resultSet = statement.executeQuery();
+ return StreamSupport.stream(new ResultsetSpliterator(resultSet), false);
+ });
+ }
+}
+
+
+class ResultsetSpliterator extends Spliterators.AbstractSpliterator> {
+
+ private final ResultSet resultSet;
+
+ public ResultsetSpliterator(final ResultSet resultSet) {
+ super(Long.MAX_VALUE, Spliterator.ORDERED);
+ this.resultSet = resultSet;
+ }
+
+ @Override
+ public boolean tryAdvance(Consumer action) {
+ if (next()) {
+ try {
+ action.accept(getMap());
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ return true;
+ } else {
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+ }
+
+ private boolean next() {
+ try {
+ return resultSet.next();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Map getMap() throws SQLException {
+ Map record = new HashMap<>();
+ for (int i = 0; i < resultSet.getMetaData().getColumnCount(); i++) {
+ record.put(resultSet.getMetaData().getColumnName(i + 1), resultSet.getObject(i + 1));
+ }
+ return record;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePlantRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePlantRepository.java
new file mode 100644
index 00000000000..f730c226643
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePlantRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.PlantRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLitePlantRepository extends PlantRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProductDtoRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProductDtoRepository.java
new file mode 100644
index 00000000000..8c29efc40ca
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProductDtoRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.ProductDtoRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteProductDtoRepository extends ProductDtoRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProjectRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProjectRepository.java
new file mode 100644
index 00000000000..3618879f8de
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProjectRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.jdbc.entities.ProjectId;
+import io.micronaut.data.tck.repositories.ProjectRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteProjectRepository extends ProjectRepository {
+ @Override
+ void update(ProjectId projectId, String name);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteQueryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteQueryTest.java
new file mode 100644
index 00000000000..0f550defbec
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteQueryTest.java
@@ -0,0 +1,115 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.exceptions.EmptyResultException;
+import io.micronaut.data.tck.entities.AuthorBooksDto;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.BookDto;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteQueryTest {
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @Inject
+ SQLiteAuthorRepository authorRepository;
+
+ @BeforeEach
+ void setup() {
+ addBookSeedData();
+ }
+
+ @AfterEach
+ void cleanup() {
+ bookRepository.deleteAll();
+ authorRepository.deleteAll();
+ }
+
+ @Test
+ void testIsNullOrEmpty() {
+ assertEquals(8, bookRepository.count());
+ assertEquals(2, bookRepository.findByAuthorIsNull().size());
+ assertEquals(6, bookRepository.findByAuthorIsNotNull().size());
+ assertEquals(1, bookRepository.countByTitleIsEmpty());
+ assertEquals(7, bookRepository.countByTitleIsNotEmpty());
+ }
+
+ @Test
+ void testStringComparisonMethods() {
+ assertEquals(2, authorRepository.countByNameContains("e"));
+ assertEquals("Stephen King", authorRepository.findByNameStartsWith("S").getName());
+ assertEquals("Don Winslow", authorRepository.findByNameEndsWith("w").getName());
+ assertEquals("Don Winslow", authorRepository.findByNameIgnoreCase("don winslow").getName());
+ }
+
+ @Test
+ void testWhereAnnotationPlaceholder() {
+ int size = bookRepository.countNativeByTitleWithPagesGreaterThan("The%", 300);
+ var books = bookRepository.findByTitleStartsWith("The", 300);
+
+ assertEquals(size, books.size());
+ }
+
+ @Test
+ void testExplicitQueryUpdateMethods() {
+ Long updated = bookRepository.setPages(800, "The Border");
+
+ assertEquals(800, bookRepository.findByTitle("The Border").getTotalPages());
+ assertEquals(1L, updated);
+
+ var king = authorRepository.findByName("Stephen King");
+ Book whatever = new Book();
+ whatever.setAuthor(king);
+ whatever.setTitle("Whatever");
+ whatever.setTotalPages(200);
+ bookRepository.save(whatever);
+
+ assertEquals("Whatever", bookRepository.findByTitle("Whatever").getTitle());
+
+ Long removed = bookRepository.wipeOutBook("Whatever");
+ assertEquals(1L, removed);
+ assertThrows(EmptyResultException.class, () -> bookRepository.findByTitle("Whatever"));
+ }
+
+ private void addBookSeedData() {
+ Book anonymous = new Book();
+ anonymous.setTitle("Anonymous");
+ anonymous.setTotalPages(400);
+ bookRepository.save(anonymous);
+
+ Book blank = new Book();
+ blank.setTitle("");
+ blank.setTotalPages(0);
+ bookRepository.save(blank);
+
+ saveSampleBooks();
+ }
+
+ private void saveSampleBooks() {
+ bookRepository.saveAuthorBooks(Arrays.asList(
+ new AuthorBooksDto("Stephen King", Arrays.asList(
+ new BookDto("The Stand", 1000),
+ new BookDto("Pet Cemetery", 400)
+ )),
+ new AuthorBooksDto("James Patterson", Arrays.asList(
+ new BookDto("Along Came a Spider", 300),
+ new BookDto("Double Cross", 300)
+ )),
+ new AuthorBooksDto("Don Winslow", Arrays.asList(
+ new BookDto("The Power of the Dog", 600),
+ new BookDto("The Border", 700)
+ ))
+ ));
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveBookRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveBookRepository.java
new file mode 100644
index 00000000000..df57aa45632
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveBookRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2024 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.BookReactiveRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteReactiveBookRepository extends BookReactiveRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactivePersonRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactivePersonRepository.java
new file mode 100644
index 00000000000..bcedb1d658d
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactivePersonRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.PersonReactiveRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteReactivePersonRepository extends PersonReactiveRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveRepositoryTest.java
new file mode 100644
index 00000000000..1e0917bb1f6
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveRepositoryTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.repositories.BookReactiveRepository;
+import io.micronaut.data.tck.repositories.PersonReactiveRepository;
+import io.micronaut.data.tck.repositories.StudentReactiveRepository;
+import io.micronaut.data.tck.tests.AbstractReactiveRepositorySpec;
+
+import java.util.Map;
+
+public class SQLiteReactiveRepositoryTest extends AbstractReactiveRepositorySpec implements SQLiteTestingPropertyProvider {
+
+ @Override
+ public Map getProperties() {
+ return SQLiteTestingPropertyProvider.super.getProperties();
+ }
+
+ @Override
+ public PersonReactiveRepository getPersonRepository() {
+ return getApplicationContext().getBean(SQLiteReactivePersonRepository.class);
+ }
+
+ @Override
+ public StudentReactiveRepository getStudentRepository() {
+ return getApplicationContext().getBean(SQLiteStudentReactiveRepository.class);
+ }
+
+ @Override
+ public BookReactiveRepository getBookRepository() {
+ return getApplicationContext().getBean(SQLiteReactiveBookRepository.class);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRegionRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRegionRepository.java
new file mode 100644
index 00000000000..60d9fff8396
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRegionRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.RegionRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteRegionRepository extends RegionRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryBehaviorTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryBehaviorTest.java
new file mode 100644
index 00000000000..bb5691152f1
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryBehaviorTest.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+class SQLiteRepositoryBehaviorTest extends AbstractSQLiteRepositoryBehaviorTest {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryScopeTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryScopeTest.java
new file mode 100644
index 00000000000..22641c91dc0
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryScopeTest.java
@@ -0,0 +1,124 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.BeanContext;
+import io.micronaut.context.annotation.Prototype;
+import io.micronaut.data.runtime.intercept.DataInterceptorResolver;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteRepositoryScopeTest {
+
+ private static final Field INTERCEPTORS_FIELD = interceptorsField();
+
+ @Inject
+ BeanContext beanContext;
+
+ private DataInterceptorResolver dataInterceptor;
+ private SQLiteBookRepository bookRepository;
+
+ @Test
+ void testDefaultRepositoryScopeIsPrototype() {
+ SQLiteBookRepository instance1 = beanContext.getBean(SQLiteBookRepository.class);
+ SQLiteBookRepository instance2 = beanContext.getBean(SQLiteBookRepository.class);
+
+ assertNotSame(instance1, instance2);
+ }
+
+ @Test
+ void testExplicitSingletonRepositoryScopeIsHonored() {
+ SQLiteBookDtoRepository instance1 = beanContext.getBean(SQLiteBookDtoRepository.class);
+ SQLiteBookDtoRepository instance2 = beanContext.getBean(SQLiteBookDtoRepository.class);
+
+ assertSame(instance1, instance2);
+ }
+
+ @Test
+ void testNoMemoryLeak1() {
+ DataInterceptorResolver resolver = getDataInterceptor();
+ SQLiteBookRepository instance = beanContext.getBean(SQLiteBookRepository.class);
+
+ for (int i = 0; i < 30000; i++) {
+ instance.deleteAll();
+ assertTrue(interceptorCount(resolver) < 10000);
+ }
+ }
+
+ @Test
+ void testNoMemoryLeak2() {
+ DataInterceptorResolver resolver = getDataInterceptor();
+ SQLiteBookRepository instance = getBookRepository();
+
+ for (int i = 0; i < 30000; i++) {
+ instance.deleteAll();
+ assertTrue(interceptorCount(resolver) < 10000);
+ }
+ }
+
+ @Test
+ void testNoMemoryLeak3() {
+ DataInterceptorResolver resolver = getDataInterceptor();
+ MyPrototypeService myService = beanContext.getBean(MyPrototypeService.class);
+
+ myService.getBookRepository().deleteAll();
+ for (int i = 0; i < 30000; i++) {
+ assertTrue(interceptorCount(resolver) < 10000);
+ }
+ }
+
+ private DataInterceptorResolver getDataInterceptor() {
+ if (dataInterceptor == null) {
+ dataInterceptor = beanContext.getBean(DataInterceptorResolver.class);
+ }
+ return dataInterceptor;
+ }
+
+ private SQLiteBookRepository getBookRepository() {
+ if (bookRepository == null) {
+ bookRepository = beanContext.getBean(SQLiteBookRepository.class);
+ }
+ return bookRepository;
+ }
+
+ @SuppressWarnings("unchecked")
+ private int interceptorCount(DataInterceptorResolver resolver) {
+ try {
+ return ((Map) INTERCEPTORS_FIELD.get(resolver)).size();
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Unable to access interceptor cache", e);
+ }
+ }
+
+ private static Field interceptorsField() {
+ try {
+ Field field = DataInterceptorResolver.class.getDeclaredField("interceptors");
+ field.setAccessible(true);
+ return field;
+ } catch (NoSuchFieldException e) {
+ throw new IllegalStateException("Unable to locate interceptor cache field", e);
+ }
+ }
+
+ @Prototype
+ static class MyPrototypeService {
+
+ private final SQLiteBookRepository bookRepository;
+
+ MyPrototypeService(SQLiteBookRepository bookRepository) {
+ this.bookRepository = bookRepository;
+ }
+
+ SQLiteBookRepository getBookRepository() {
+ return bookRepository;
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryTest.java
new file mode 100644
index 00000000000..736e234b74b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2017-2026 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
+import io.micronaut.data.tck.repositories.AuthorRepository;
+import io.micronaut.data.tck.repositories.BasicTypesRepository;
+import io.micronaut.data.tck.repositories.BookDtoRepository;
+import io.micronaut.data.tck.repositories.BookRepository;
+import io.micronaut.data.tck.repositories.CarRepository;
+import io.micronaut.data.tck.repositories.CityRepository;
+import io.micronaut.data.tck.repositories.CompanyRepository;
+import io.micronaut.data.tck.repositories.CountryRegionCityRepository;
+import io.micronaut.data.tck.repositories.CountryRepository;
+import io.micronaut.data.tck.repositories.EntityWithIdClass2Repository;
+import io.micronaut.data.tck.repositories.EntityWithIdClassRepository;
+import io.micronaut.data.tck.repositories.ExampleEntityRepository;
+import io.micronaut.data.tck.repositories.FaceRepository;
+import io.micronaut.data.tck.repositories.FoodRepository;
+import io.micronaut.data.tck.repositories.GenreRepository;
+import io.micronaut.data.tck.repositories.IntervalRepository;
+import io.micronaut.data.tck.repositories.MealRepository;
+import io.micronaut.data.tck.repositories.NoseRepository;
+import io.micronaut.data.tck.repositories.PageRepository;
+import io.micronaut.data.tck.repositories.PersonRepository;
+import io.micronaut.data.tck.repositories.RegionRepository;
+import io.micronaut.data.tck.repositories.RoleRepository;
+import io.micronaut.data.tck.repositories.StudentRepository;
+import io.micronaut.data.tck.repositories.TimezoneBasicTypesRepository;
+import io.micronaut.data.tck.repositories.UserRepository;
+import io.micronaut.data.tck.repositories.UserRoleRepository;
+import io.micronaut.data.tck.tests.AbstractRepositorySpec;
+import io.micronaut.inject.qualifiers.Qualifiers;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+
+public class SQLiteRepositoryTest extends AbstractRepositorySpec implements SQLiteTestingPropertyProvider {
+
+ @Override
+ public Map getProperties() {
+ return SQLiteTestingPropertyProvider.super.getProperties();
+ }
+
+ @Override
+ public EntityWithIdClassRepository getEntityWithIdClassRepository() {
+ return getApplicationContext().getBean(SQLiteEntityWithIdClassRepository.class);
+ }
+
+ @Override
+ public EntityWithIdClass2Repository getEntityWithIdClass2Repository() {
+ return getApplicationContext().getBean(SQLiteEntityWithIdClass2Repository.class);
+ }
+
+ @Override
+ public NoseRepository getNoseRepository() {
+ return getApplicationContext().getBean(SQLiteNoseRepository.class);
+ }
+
+ @Override
+ public FaceRepository getFaceRepository() {
+ return getApplicationContext().getBean(SQLiteFaceRepository.class);
+ }
+
+ @Override
+ public PersonRepository getPersonRepository() {
+ return getApplicationContext().getBean(SQLitePersonRepository.class);
+ }
+
+ @Override
+ public BookRepository getBookRepository() {
+ return getApplicationContext().getBean(SQLiteBookRepository.class);
+ }
+
+ @Override
+ public GenreRepository getGenreRepository() {
+ return getApplicationContext().getBean(SQLiteGenreRepository.class);
+ }
+
+ @Override
+ public AuthorRepository getAuthorRepository() {
+ return getApplicationContext().getBean(SQLiteAuthorRepository.class);
+ }
+
+ @Override
+ public CompanyRepository getCompanyRepository() {
+ return getApplicationContext().getBean(SQLiteCompanyRepository.class);
+ }
+
+ @Override
+ public BookDtoRepository getBookDtoRepository() {
+ return getApplicationContext().getBean(SQLiteBookDtoRepository.class);
+ }
+
+ @Override
+ public CountryRepository getCountryRepository() {
+ return getApplicationContext().getBean(SQLiteCountryRepository.class);
+ }
+
+ @Override
+ public CityRepository getCityRepository() {
+ return getApplicationContext().getBean(SQLiteCityRepository.class);
+ }
+
+ @Override
+ public RegionRepository getRegionRepository() {
+ return getApplicationContext().getBean(SQLiteRegionRepository.class);
+ }
+
+ @Override
+ public CountryRegionCityRepository getCountryRegionCityRepository() {
+ return getApplicationContext().getBean(SQLiteCountryRegionCityRepository.class);
+ }
+
+ @Override
+ public UserRoleRepository getUserRoleRepository() {
+ return getApplicationContext().getBean(SQLiteUserRoleRepository.class);
+ }
+
+ @Override
+ public RoleRepository getRoleRepository() {
+ return getApplicationContext().getBean(SQLiteRoleRepository.class);
+ }
+
+ @Override
+ public UserRepository getUserRepository() {
+ return getApplicationContext().getBean(SQLiteUserRepository.class);
+ }
+
+ @Override
+ public MealRepository getMealRepository() {
+ return getApplicationContext().getBean(SQLiteMealRepository.class);
+ }
+
+ @Override
+ public FoodRepository getFoodRepository() {
+ return getApplicationContext().getBean(SQLiteFoodRepository.class);
+ }
+
+ @Override
+ public StudentRepository getStudentRepository() {
+ return getApplicationContext().getBean(SQLiteStudentRepository.class);
+ }
+
+ @Override
+ public CarRepository getCarRepository() {
+ return getApplicationContext().getBean(SQLiteCarRepository.class);
+ }
+
+ @Override
+ public BasicTypesRepository getBasicTypeRepository() {
+ return getApplicationContext().getBean(SQLiteBasicTypesRepository.class);
+ }
+
+ @Override
+ public TimezoneBasicTypesRepository getTimezoneBasicTypeRepository() {
+ return getApplicationContext().getBean(SQLiteTimezoneBasicTypesRepository.class);
+ }
+
+ @Override
+ public PageRepository getPageRepository() {
+ return getApplicationContext().getBean(SQLitePageRepository.class);
+ }
+
+ @Override
+ public ExampleEntityRepository getExampleEntityRepository() {
+ return getApplicationContext().getBean(SQLiteExampleEntityRepository.class);
+ }
+
+ @Override
+ public IntervalRepository getIntervalRepository() {
+ return getApplicationContext().getBean(SQLiteIntervalRepository.class);
+ }
+
+ @Override
+ public boolean isSupportsArrays() {
+ return true;
+ }
+
+ @Override
+ protected boolean skipQueryByDataArray() {
+ return true;
+ }
+
+ @Override
+ protected void cleanupBooks() {
+ try (Connection connection = dataSource().getConnection();
+ Statement statement = connection.createStatement()) {
+ statement.executeUpdate("DELETE FROM \"book_student\"");
+ } catch (SQLException e) {
+ throw new IllegalStateException("Failed to clean up book_student rows", e);
+ }
+ super.cleanupBooks();
+ }
+
+ @Override
+ protected void cleanupData() {
+ getStudentRepository().deleteAll();
+ super.cleanupData();
+ }
+
+ private DataSource dataSource() {
+ DataSource dataSource = getApplicationContext().getBean(DataSource.class, Qualifiers.byName("default"));
+ return DelegatingDataSource.unwrapDataSource(dataSource);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRestaurantRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRestaurantRepository.java
new file mode 100644
index 00000000000..3126443a90c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRestaurantRepository.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.entities.Restaurant;
+import io.micronaut.data.tck.repositories.RestaurantRepository;
+
+import java.util.Optional;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteRestaurantRepository extends RestaurantRepository {
+
+ Optional findByAddressStreet(String street);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRoleRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRoleRepository.java
new file mode 100644
index 00000000000..6fefdd90d4b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRoleRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.RoleRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteRoleRepository extends RoleRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleItemRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleItemRepository.java
new file mode 100644
index 00000000000..400d386ccf3
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleItemRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.SaleItemRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteSaleItemRepository extends SaleItemRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleRepository.java
new file mode 100644
index 00000000000..83d934d8d93
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.SaleRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteSaleRepository extends SaleRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaCreateDropTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaCreateDropTest.java
new file mode 100644
index 00000000000..c00f37740a7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaCreateDropTest.java
@@ -0,0 +1,56 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.repositories.BookRepository;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SQLiteSchemaCreateDropTest {
+
+ @Test
+ void bookIsCreated() {
+ try (ApplicationContext context = ApplicationContext.run(createProperties())) {
+ BookRepository bookRepository = context.getBean(SQLiteBookRepository.class);
+ Book book = new Book();
+ book.setTitle("title");
+ bookRepository.save(book);
+
+ assertEquals(1, bookRepository.count());
+ }
+ }
+
+ @Test
+ void bookWasDropped() {
+ try (ApplicationContext context = ApplicationContext.run(createProperties())) {
+ BookRepository bookRepository = context.getBean(SQLiteBookRepository.class);
+ assertEquals(0, bookRepository.count());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqliteschemacreatedrop", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE_DROP");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite,io.micronaut.data.tck.entities,io.micronaut.data.tck.jdbc.entities");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaGenerationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaGenerationTest.java
new file mode 100644
index 00000000000..68b82f63944
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaGenerationTest.java
@@ -0,0 +1,21 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteSchemaGenerationTest {
+
+ @Inject
+ SQLiteOrganizationRepository repository;
+
+ @Test
+ void testUuidGeneratedValue() {
+ assertEquals(0L, repository.count());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfBookRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfBookRepository.java
new file mode 100644
index 00000000000..d27dba23379
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfBookRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.ShelfBookRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteShelfBookRepository extends ShelfBookRepository {}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfRepository.java
new file mode 100644
index 00000000000..0bafbd8a4c6
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.ShelfRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SQLiteShelfRepository extends ShelfRepository {}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStreamingStatementTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStreamingStatementTest.java
new file mode 100644
index 00000000000..ee1b49f442e
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStreamingStatementTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteStreamingStatementTest {
+
+ @Inject
+ SQLitePersonRepository personRepository;
+
+ @Test
+ void testStreamingOrder() {
+ personRepository.save(person("a"));
+ personRepository.save(person("c"));
+ personRepository.save(person("b"));
+ personRepository.save(person("d"));
+
+ var list = personRepository.findAllAndStream().toList();
+
+ assertEquals(4, list.size());
+ assertEquals("a", list.get(0).get("name"));
+ assertEquals("b", list.get(1).get("name"));
+ assertEquals("c", list.get(2).get("name"));
+ assertEquals("d", list.get(3).get("name"));
+ }
+
+ private static Person person(String name) {
+ Person person = new Person();
+ person.setName(name);
+ return person;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentReactiveRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentReactiveRepository.java
new file mode 100644
index 00000000000..a8c53e52f29
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentReactiveRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.StudentReactiveRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteStudentReactiveRepository extends StudentReactiveRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentRepository.java
new file mode 100644
index 00000000000..33187bbd1ff
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.StudentRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteStudentRepository extends StudentRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTableRatingsRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTableRatingsRepository.java
new file mode 100644
index 00000000000..2dc3009dd6a
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTableRatingsRepository.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import org.jspecify.annotations.Nullable;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTableRatingsRepository extends CrudRepository {
+
+ @Nullable
+ TableRatings findByRating(int rating);
+
+ void updateRating(@Id Long id, int rating);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntity2Repository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntity2Repository.java
new file mode 100644
index 00000000000..b8bf77d36d7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntity2Repository.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.TaskGenericEntity;
+import io.micronaut.data.tck.entities.TaskGenericEntity2;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTaskGenericEntity2Repository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntityRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntityRepository.java
new file mode 100644
index 00000000000..cbeef66f10a
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntityRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.GenericEntity;
+import io.micronaut.data.tck.entities.Task;
+import io.micronaut.data.tck.entities.TaskGenericEntity;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTaskGenericEntityRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskRepository.java
new file mode 100644
index 00000000000..a8eb1437974
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.tck.entities.Task;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTaskRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTestingPropertyProvider.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTestingPropertyProvider.java
new file mode 100644
index 00000000000..9c7cc0fbc2b
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTestingPropertyProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.runtime.config.SchemaGenerate;
+import io.micronaut.test.support.TestPropertyProvider;
+
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public interface SQLiteTestingPropertyProvider extends TestPropertyProvider {
+
+ default SchemaGenerate schemaGenerate() {
+ return SchemaGenerate.CREATE;
+ }
+
+ default List packages() {
+ String currentClassPackage = getClass().getPackage().getName();
+ return Arrays.asList(currentClassPackage, "io.micronaut.data.tck.entities", "io.micronaut.data.tck.jdbc.entities");
+ }
+
+ default boolean shouldAddDefaultDbProperties() {
+ return true;
+ }
+
+ @Override
+ default Map getProperties() {
+ return shouldAddDefaultDbProperties() ? getSQLiteDataSourceProperties("default") : Map.of();
+ }
+
+ default Map getSQLiteDataSourceProperties(String dataSourceName) {
+ String prefix = "datasources." + dataSourceName;
+ String url = createUrl(dataSourceName);
+ return Map.of(
+ (prefix + ".url"), url,
+ (prefix + ".schema-generate"), schemaGenerate().toString(),
+ (prefix + ".dialect"), "SQLITE",
+ (prefix + ".db-type"), "sqlite",
+ (prefix + ".username"), "",
+ (prefix + ".password"), "",
+ (prefix + ".packages"), String.join(",", packages()),
+ (prefix + ".driverClassName"), "org.sqlite.JDBC"
+ );
+ }
+
+ private static String createUrl(String dataSourceName) {
+ try {
+ var databaseFile = Files.createTempFile(dataSourceName.toLowerCase(Locale.ENGLISH), ".db").toFile();
+ databaseFile.deleteOnExit();
+ return "jdbc:sqlite:" + databaseFile.getAbsolutePath();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create SQLite test database", e);
+ }
+ }
+
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTimezoneBasicTypesRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTimezoneBasicTypesRepository.java
new file mode 100644
index 00000000000..ffe4095ea99
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTimezoneBasicTypesRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.TimezoneBasicTypesRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTimezoneBasicTypesRepository extends TimezoneBasicTypesRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainRepository.java
new file mode 100644
index 00000000000..d452389b11e
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.TrainRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTrainRepository extends TrainRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainsRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainsRepository.java
new file mode 100644
index 00000000000..6d4d20a7bb7
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainsRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.TrainsRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteTrainsRepository extends TrainsRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTransactionsTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTransactionsTest.java
new file mode 100644
index 00000000000..180844a7f64
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTransactionsTest.java
@@ -0,0 +1,640 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.connection.ConnectionOperations;
+import io.micronaut.data.connection.jdbc.operations.DefaultDataSourceConnectionOperations;
+import io.micronaut.data.tck.services.TxBookService;
+import io.micronaut.data.tck.services.TxEventsService;
+import io.micronaut.transaction.SynchronousTransactionManager;
+import io.micronaut.transaction.TransactionOperations;
+import io.micronaut.transaction.jdbc.DataSourceTransactionManager;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class SQLiteTransactionsTest {
+
+ private static final long CONNECTIONS = 1000;
+
+ private ApplicationContext context;
+ private DataSourceTransactionManager transactionOperations;
+ private DefaultDataSourceConnectionOperations connectionOperations;
+ private TxBookService txBookService;
+ private TxEventsService txEventsService;
+
+ @BeforeAll
+ void setupContext() {
+ context = ApplicationContext.run(createProperties());
+ transactionOperations = context.getBean(DataSourceTransactionManager.class);
+ connectionOperations = context.getBean(DefaultDataSourceConnectionOperations.class);
+ txBookService = context.getBean(TxBookService.class);
+ txEventsService = context.getBean(TxEventsService.class);
+ }
+
+ @AfterEach
+ void cleanup() {
+ bookService().cleanup();
+ txEventsService.cleanup();
+ }
+
+ @AfterAll
+ void closeContext() {
+ if (context != null) {
+ context.close();
+ }
+ }
+
+ @Test
+ void connectableWithNestedTransaction() {
+ assertDoesNotThrow(() -> {
+ try {
+ bookService().bookAddedInConnectableNestedTransaction();
+ assertEquals(1, bookService().countBooksTransactional());
+ } catch (NoClassDefFoundError ignored) {
+ }
+ });
+ }
+
+ @Test
+ void customNameTransaction() {
+ bookService().bookAddedCustomNamedTransaction(() -> {
+ var status = transactionOperations.findTransactionStatus().orElseThrow();
+ if (!"MyTx".equals(status.getTransactionDefinition().getName())) {
+ throw new IllegalStateException("Expected a custom TX name!");
+ }
+ });
+
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInReadOnlyTransactionNotThrowingError() {
+ if (!supportsReadOnlyFlag() || failsInsertInReadOnlyTx()) {
+ return;
+ }
+
+ assertDoesNotThrow(() -> bookService().bookAddedInReadOnlyTransaction());
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testReadOnlyTransactionAddingBookInInnerTransactionNotThrowingError() {
+ if (!supportsReadOnlyFlag() || failsInsertInReadOnlyTx()) {
+ return;
+ }
+
+ assertDoesNotThrow(() -> bookService().readOnlyTxCallingAddingBookInAnotherTransaction());
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInNeverPropagation() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ bookService().bookAddedInNeverPropagation(noTxCheck());
+
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInNeverPropagationSync() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ bookService().bookAddedInNeverPropagationSync(noTxCheck());
+
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInInnerNeverPropagation() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ Exception e = assertThrows(Exception.class, () -> bookService().bookAddedInInnerNeverPropagation(noTxCheck()));
+
+ assertEquals("Existing transaction found for transaction marked with propagation 'never'", e.getMessage());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInInnerNeverPropagationSync() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ Exception e = assertThrows(Exception.class, () -> bookService().bookAddedInInnerNeverPropagationSync(noTxCheck()));
+
+ assertEquals("Existing transaction found for transaction marked with propagation 'never'", e.getMessage());
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInNotSupportedPropagation() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ bookService().bookAddedInNoSupportedPropagation(noTxCheck());
+
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInNotSupportedPropagationAndFailed() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ assertThrows(Exception.class, () -> bookService().bookAddedInNoSupportedPropagationAndFailed(noTxCheck()));
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInInnerNotSupportedPropagationAndFailedWithExceptionSuppressed() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ bookService().bookAddedInInnerNoSupportedPropagationFailedAndExceptionSuppressed(noTxCheck());
+
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookAddedInInnerNotSupportedPropagation() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ bookService().bookAddedInInnerNoSupportedPropagation(noTxCheck());
+
+ assertEquals(supportsModificationInNonTransaction() ? 1 : 0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testMandatoryTransactionMissing() {
+ Exception e = assertThrows(Exception.class, () -> bookService().mandatoryTransaction());
+ assertEquals("No existing transaction found for transaction marked with propagation 'mandatory'", e.getMessage());
+ }
+
+ @Test
+ void testMandatoryTransactionMissingSync() {
+ Exception e = assertThrows(Exception.class, () -> bookService().mandatoryTransactionSync());
+ assertEquals("No existing transaction found for transaction marked with propagation 'mandatory'", e.getMessage());
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ }
+
+ @Test
+ void testBookIsAddedInMandatoryTransaction() {
+ bookService().bookAddedInMandatoryTransaction();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInMandatoryTransactionSync() {
+ bookService().bookAddedInMandatoryTransactionSync();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testInnerTransactionWithSuppressedException() {
+ Exception e = assertThrows(Exception.class, () -> bookService().innerTransactionHasSuppressedException());
+ assertEquals("Transaction rolled back because it has been marked as rollback-only", e.getMessage());
+ }
+
+ @Test
+ void testInnerTransactionWithSuppressedExceptionSync() {
+ Exception e = assertThrows(Exception.class, () -> bookService().innerTransactionHasSuppressedExceptionSync());
+ assertEquals("Transaction rolled back because it has been marked as rollback-only", e.getMessage());
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ }
+
+ @Test
+ void testInnerTransactionWithSuppressedExceptionSync2() {
+ Exception e = assertThrows(Exception.class, () -> bookService().innerTransactionHasSuppressedExceptionSync2());
+ assertEquals("Transaction rolled back because it has been marked as rollback-only", e.getMessage());
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ }
+
+ @Test
+ void testInnerTransactionMarkedForRollback() {
+ Exception e = assertThrows(Exception.class, () -> bookService().innerTransactionMarkedForRollback(
+ () -> transactionOperations.findTransactionStatus().orElseThrow().setRollbackOnly()
+ ));
+ assertEquals("Transaction rolled back because it has been marked as rollback-only", e.getMessage());
+ }
+
+ @Test
+ void testTransactionMarkedForRollback() {
+ bookService().saveAndMarkedForRollback(() -> transactionOperations.findTransactionStatus().orElseThrow().setRollbackOnly());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testTransactionMarkedForRollback2() {
+ bookService().saveAndMarkedForRollback2(() -> transactionOperations.findTransactionStatus().orElseThrow().setRollbackOnly());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testInnerRequiresNewTransactionWithSuppressedException() {
+ bookService().innerRequiresNewTransactionHasSuppressedException();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInAnotherRequiresNewTx() {
+ bookService().bookIsAddedInAnotherRequiresNewTx();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInAnotherRequiresNewTxSync() {
+ bookService().bookIsAddedInAnotherRequiresNewTxSync();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInAnotherRequiresNewTxWhichIsFailing() {
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> bookService().bookIsAddedInAnotherRequiresNewTxWhichIsFailing());
+ assertEquals("Big fail!", e.getMessage());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInTheMainTxAndAnotherRequiresNewTxIsFailing() {
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> bookService().bookIsAddedAndAnotherRequiresNewTxIsFailing());
+ assertEquals("Big fail!", e.getMessage());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInTheMainTxAndAnotherRequiresNewTxIsFailingSync() {
+ IllegalStateException e = assertThrows(IllegalStateException.class, () -> bookService().bookIsAddedAndAnotherRequiresNewTxIsFailingSync());
+ assertEquals("Big fail!", e.getMessage());
+ assertEquals(0, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInNestedTx() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ bookService().bookAddedInNestedTransaction();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInNestedTxSync() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ bookService().bookAddedInNestedTransactionSync();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInAnotherNestedTx() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ bookService().bookAddedInAnotherNestedTransaction();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testBookIsAddedInAnotherNestedTxSync() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ bookService().bookAddedInAnotherNestedTransactionSync();
+ assertEquals(1, bookService().countBooksTransactional());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted1() {
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookIsAddedInTxMethod();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted2() {
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookIsAddedInAnotherRequiresNewTxSync();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted3() {
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().innerRequiresNewTransactionHasSuppressedException();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted4() {
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookAddedInMandatoryTransaction();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted5() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookAddedInInnerNoSupportedPropagation(noTxCheck());
+ }
+ assertEquals(supportsModificationInNonTransaction() ? CONNECTIONS : 0, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted6() {
+ if (!supportsNoTxProcessing()) {
+ return;
+ }
+
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookAddedInNeverPropagation(noTxCheck());
+ }
+ assertEquals(supportsModificationInNonTransaction() ? CONNECTIONS : 0, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted7() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookAddedInNestedTransaction();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted8() {
+ if (!supportsNestedTx()) {
+ return;
+ }
+
+ for (int i = 0; i < CONNECTIONS; i++) {
+ bookService().bookAddedInNestedTransactionSync();
+ }
+ assertEquals(CONNECTIONS, bookService().countBooks());
+ }
+
+ @Test
+ void testThatConnectionsAreNeverExhausted9() {
+ for (int i = 0; i < CONNECTIONS; i++) {
+ try {
+ bookService().innerTransactionHasSuppressedExceptionSync();
+ } catch (Exception e) {
+ assertEquals("Transaction rolled back because it has been marked as rollback-only", e.getMessage());
+ }
+ }
+ assertEquals(0, bookService().countBooks());
+ }
+
+ @Test
+ void testTransactionalEventsHandling() throws Exception {
+ txEventsService.insertWithTransaction();
+
+ assertEquals("The Stand", txEventsService.getLastEvent().title());
+ assertEquals(1, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "BEFORE COMMIT: false",
+ "BEFORE COMPLETION",
+ "AFTER COMMIT",
+ "AFTER COMPLETION: COMMITTED"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ RuntimeException runtime = assertThrows(RuntimeException.class, () -> txEventsService.insertAndRollback());
+ assertEquals("Bad things happened", runtime.getMessage());
+ assertNull(txEventsService.getLastEvent());
+ assertEquals(0, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "BEFORE COMPLETION",
+ "AFTER COMPLETION: ROLLED_BACK"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ runtime = assertThrows(RuntimeException.class, () -> txEventsService.insertAndRollbackWithOuterTransaction());
+ assertEquals("Bad things happened", runtime.getMessage());
+ assertNull(txEventsService.getLastEvent());
+ assertEquals(0, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "ENTER INNER",
+ "OUTER BEFORE COMPLETION",
+ "BEFORE COMPLETION",
+ "OUTER AFTER COMPLETION: ROLLED_BACK",
+ "AFTER COMPLETION: ROLLED_BACK"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ Exception checked = assertThrows(Exception.class, () -> txEventsService.insertAndRollbackChecked());
+ assertEquals("Bad things happened", checked.getMessage());
+ assertNull(txEventsService.getLastEvent());
+ assertEquals(0, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "BEFORE COMPLETION",
+ "AFTER COMPLETION: ROLLED_BACK"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ checked = assertThrows(Exception.class, () -> txEventsService.insertAndRollbackCheckedWithOuterTransaction());
+ assertEquals("Bad things happened", checked.getMessage());
+ assertNull(txEventsService.getLastEvent());
+ assertEquals(0, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "ENTER INNER",
+ "OUTER BEFORE COMPLETION",
+ "BEFORE COMPLETION",
+ "OUTER AFTER COMPLETION: ROLLED_BACK",
+ "AFTER COMPLETION: ROLLED_BACK"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ assertThrows(IOException.class, () -> txEventsService.insertAndRollbackDontRollbackOn());
+ if (supportsDontRollbackOn()) {
+ assertEquals(1, txEventsService.countBooksTransactional());
+ assertTrue(txEventsService.getLastEvent() != null);
+ } else {
+ assertEquals(0, txEventsService.countBooksTransactional());
+ assertNull(txEventsService.getLastEvent());
+ }
+
+ txEventsService.cleanup();
+ txEventsService.insertWithOuterTransaction();
+ assertEquals("The Stand", txEventsService.getLastEvent().title());
+ assertEquals(1, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "ENTER INNER",
+ "EXIT INNER",
+ "OUTER BEFORE COMMIT: false",
+ "BEFORE COMMIT: false",
+ "OUTER BEFORE COMPLETION",
+ "BEFORE COMPLETION",
+ "OUTER AFTER COMMIT",
+ "AFTER COMMIT",
+ "OUTER AFTER COMPLETION: COMMITTED",
+ "AFTER COMPLETION: COMMITTED"
+ ), txEventsService.getEvents());
+
+ txEventsService.cleanup();
+ txEventsService.insertWithOuterNewTransaction();
+ assertEquals("The Stand", txEventsService.getLastEvent().title());
+ assertEquals(1, txEventsService.countBooksTransactional());
+ assertEquals(List.of(
+ "ENTER INNER",
+ "BEFORE COMMIT: false",
+ "BEFORE COMPLETION",
+ "AFTER COMMIT",
+ "AFTER COMPLETION: COMMITTED",
+ "EXIT INNER",
+ "OUTER BEFORE COMMIT: false",
+ "OUTER BEFORE COMPLETION",
+ "OUTER AFTER COMMIT",
+ "OUTER AFTER COMPLETION: COMMITTED"
+ ), txEventsService.getEvents());
+ }
+
+ @Test
+ void testTxManaged() {
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ bookService().checkInTransaction(() -> assertTrue(transactionOperations.findTransactionStatus().isPresent()));
+ assertTrue(transactionOperations.findTransactionStatus().isEmpty());
+ }
+
+ private TxBookService bookService() {
+ txBookService.transactionManager = castTransactionManager(transactionOperations);
+ txBookService.connectionOperations = castConnectionOperations(connectionOperations);
+ return txBookService;
+ }
+
+ private Runnable noTxCheck() {
+ return () -> {
+ var status = connectionOperations.findConnectionStatus();
+ if (status.isEmpty()) {
+ return;
+ }
+ Connection connection = (Connection) status.get().getConnection();
+ try {
+ assertTrue(connection.getAutoCommit());
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+ private boolean supportsNoTxProcessing() {
+ return true;
+ }
+
+ private boolean supportsModificationInNonTransaction() {
+ return true;
+ }
+
+ private boolean supportsDontRollbackOn() {
+ return true;
+ }
+
+ private boolean supportsReadOnlyFlag() {
+ return false;
+ }
+
+ private boolean failsInsertInReadOnlyTx() {
+ return false;
+ }
+
+ private boolean supportsNestedTx() {
+ return true;
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqlitetransactions".toLowerCase(Locale.ENGLISH), ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("bookRepositoryClass", SQLiteBookRepository.class.getName());
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", List.of(
+ "io.micronaut.data.jdbc.sqlite",
+ "io.micronaut.data.tck.entities",
+ "io.micronaut.data.tck.jdbc.entities"
+ ));
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static SynchronousTransactionManager castTransactionManager(DataSourceTransactionManager transactionManager) {
+ return (SynchronousTransactionManager) transactionManager;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static ConnectionOperations castConnectionOperations(DefaultDataSourceConnectionOperations connectionOperations) {
+ return (ConnectionOperations) connectionOperations;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUnidirectionalToManyJoinTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUnidirectionalToManyJoinTest.java
new file mode 100644
index 00000000000..f2bda62e7be
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUnidirectionalToManyJoinTest.java
@@ -0,0 +1,72 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.Page;
+import io.micronaut.data.tck.entities.Shelf;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteUnidirectionalToManyJoinTest {
+
+ @Inject
+ SQLiteShelfRepository shelfRepository;
+
+ @Inject
+ SQLiteBookRepository bookRepository;
+
+ @Inject
+ SQLitePageRepository pageRepository;
+
+ @Inject
+ SQLiteShelfBookRepository shelfBookRepository;
+
+ @Inject
+ SQLiteBookPageRepository bookPageRepository;
+
+ @Test
+ void testUnidirectionalJoin() {
+ bookRepository.deleteAll();
+
+ Shelf shelf = new Shelf();
+ shelf.setShelfName("Some Shelf");
+
+ Book b1 = new Book();
+ b1.setTitle("The Stand");
+ b1.setTotalPages(1000);
+ Page p1 = new Page();
+ p1.setNum(10);
+ b1.getPages().add(p1);
+ Page p2 = new Page();
+ p2.setNum(20);
+ b1.getPages().add(p2);
+
+ Book b2 = new Book();
+ b2.setTitle("The Shining");
+ b2.setTotalPages(600);
+
+ shelf.getBooks().add(b1);
+ shelf.getBooks().add(b2);
+
+ shelf = shelfRepository.save(shelf);
+
+ for (Page page : b1.getPages()) {
+ assertNotNull(page.getId());
+ }
+ for (Book book : shelf.getBooks()) {
+ assertNotNull(book.getId());
+ }
+
+ shelf = shelfRepository.findById(shelf.getId()).orElse(null);
+
+ assertNotNull(shelf);
+ assertEquals("Some Shelf", shelf.getShelfName());
+ assertFalse(shelf.getBooks().isEmpty());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRepository.java
new file mode 100644
index 00000000000..4e50ca125e3
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.UserRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteUserRepository extends UserRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRoleRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRoleRepository.java
new file mode 100644
index 00000000000..2bbc055a0cb
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRoleRepository.java
@@ -0,0 +1,9 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.tck.repositories.UserRoleRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SQLiteUserRoleRepository extends UserRoleRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteValidationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteValidationTest.java
new file mode 100644
index 00000000000..bf2bdc87b17
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteValidationTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Food;
+import io.micronaut.data.tck.entities.Meal;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import jakarta.validation.ConstraintViolationException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties
+class SQLiteValidationTest {
+
+ @Inject
+ SQLiteMealRepository mealRepository;
+
+ @Inject
+ SQLiteFoodRepository foodRepository;
+
+ @Test
+ void testSaveValidObjects() {
+ Meal meal = new Meal(100);
+ mealRepository.save(meal);
+ Meal alternativeMeal = new Meal(50);
+ mealRepository.save(alternativeMeal);
+
+ Food food = new Food("test", 100, 100, meal);
+ food.setAlternativeMeal(alternativeMeal);
+ food = foodRepository.save(food);
+ Food retrieved = foodRepository.findById(food.getFid()).orElse(null);
+
+ assertNotNull(retrieved);
+ assertEquals(food.getKey(), retrieved.getKey());
+ assertEquals(food.getCarbohydrates(), retrieved.getCarbohydrates());
+ assertEquals(1, mealRepository.searchById(meal.getMid()).getFoods().size());
+ var foodId = food.getFid();
+ assertDoesNotThrow(() -> foodRepository.searchById(foodId));
+ }
+
+ @Test
+ void testSaveInvalidObjects() {
+ ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> mealRepository.save(new Meal(10000)));
+ assertTrue(e.getMessage().contains("currentBloodGlucose: must be less than or equal to 999"));
+ }
+
+ @Test
+ void testUpdateInvalidObjects() {
+ Meal meal = new Meal(100);
+ mealRepository.save(meal);
+
+ Food food = new Food("test", 100, 100, meal);
+ food = foodRepository.save(food);
+ Food retrieved = foodRepository.findById(food.getFid()).orElse(null);
+
+ assertNotNull(retrieved);
+ assertEquals(food.getKey(), retrieved.getKey());
+ assertEquals(food.getCarbohydrates(), retrieved.getCarbohydrates());
+
+ retrieved.getMeal().setCurrentBloodGlucose(10000);
+ var invalidMeal = retrieved.getMeal();
+ ConstraintViolationException e = assertThrows(ConstraintViolationException.class, () -> mealRepository.update(invalidMeal));
+ assertTrue(e.getMessage().contains("currentBloodGlucose: must be less than or equal to 999"));
+
+ retrieved.getMeal().setCurrentBloodGlucose(101);
+ foodRepository.update(retrieved);
+ retrieved = foodRepository.findById(food.getFid()).orElse(null);
+
+ assertNotNull(retrieved);
+ assertEquals(101, retrieved.getMeal().getCurrentBloodGlucose());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteWhereAnnotationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteWhereAnnotationTest.java
new file mode 100644
index 00000000000..ae23046b047
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteWhereAnnotationTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.tck.entities.Person;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+@MicronautTest
+@SQLiteDBProperties
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class SQLiteWhereAnnotationTest {
+
+ @Inject
+ SQLiteEnabledPersonRepository personRepository;
+
+ @BeforeAll
+ void setupSpec() {
+ personRepository.deleteAll();
+ }
+
+ @Test
+ void testReturnOnlyEnabledPeople() {
+ personRepository.saveAll(java.util.List.of(
+ person("Fred", 35, true),
+ person("Joe", 30, false),
+ person("Bob", 30, true)
+ ));
+
+ assertEquals(2, personRepository.count());
+ assertEquals(1, personRepository.countByNameLike("%e%"));
+ assertFalse(personRepository.findAll().stream().anyMatch(person -> "Joe".equals(person.getName())));
+ }
+
+ private static Person person(String name, int age, boolean enabled) {
+ Person person = new Person();
+ person.setName(name);
+ person.setAge(age);
+ person.setEnabled(enabled);
+ return person;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ShipmentRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ShipmentRepository.java
new file mode 100644
index 00000000000..9c1c33b7c4c
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ShipmentRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.CursoredPage;
+import io.micronaut.data.model.CursoredPageable;
+import io.micronaut.data.repository.PageableRepository;
+import io.micronaut.data.tck.entities.Shipment;
+import io.micronaut.data.tck.entities.ShipmentId;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+
+import java.util.List;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface ShipmentRepository extends PageableRepository {
+
+ Shipment findByShipmentIdCountry(String country);
+
+ Shipment findByShipmentIdCountryAndShipmentIdCity(String country, String city);
+
+ List findAllOrderByShipmentIdCityDesc();
+
+ List findAllOrderByShipmentIdCountryAndShipmentIdCityDesc();
+
+ CursoredPage findByShipmentIdCountry(String country, CursoredPageable pageable);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueEntity.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueEntity.java
new file mode 100644
index 00000000000..fe8dd110690
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueEntity.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+
+@MappedEntity("generated_value_entity")
+public class SqliteGeneratedValueEntity {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ private String name;
+
+ public SqliteGeneratedValueEntity(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueRepository.java
new file mode 100644
index 00000000000..481c86064c4
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SqliteGeneratedValueRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueTest.java
new file mode 100644
index 00000000000..487beb32d22
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@MicronautTest
+@SQLiteDBProperties
+class SqliteGeneratedValueTest {
+
+ @Inject
+ SqliteGeneratedValueRepository repository;
+
+ @AfterEach
+ void cleanup() {
+ repository.deleteAll();
+ }
+
+ @Test
+ void testSaveAndLoadGeneratedIdentity() {
+ SqliteGeneratedValueEntity saved = repository.save(new SqliteGeneratedValueEntity("alpha"));
+
+ assertNotNull(saved.getId());
+
+ SqliteGeneratedValueEntity reloaded = repository.findById(saved.getId()).orElse(null);
+
+ assertNotNull(reloaded);
+ assertEquals(saved.getId(), reloaded.getId());
+ assertEquals("alpha", reloaded.getName());
+ }
+
+ @Test
+ void testSaveAllAssignsGeneratedIdentities() {
+ List saved = repository.saveAll(List.of(
+ new SqliteGeneratedValueEntity("alpha"),
+ new SqliteGeneratedValueEntity("beta")
+ ));
+
+ saved.forEach(entity -> assertNotNull(entity.getId()));
+ assertEquals(2, saved.stream().map(SqliteGeneratedValueEntity::getId).distinct().count());
+
+ List reloaded = saved.stream()
+ .map(entity -> repository.findById(entity.getId()).orElse(null))
+ .toList();
+
+ Set names = reloaded.stream().map(SqliteGeneratedValueEntity::getName).collect(Collectors.toSet());
+ assertEquals(Set.of("alpha", "beta"), names);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteSchemaValidationTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteSchemaValidationTest.java
new file mode 100644
index 00000000000..8556f6ddb54
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteSchemaValidationTest.java
@@ -0,0 +1,70 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.ApplicationContext;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class SqliteSchemaValidationTest {
+
+ @Disabled
+ @Test
+ void validateSchema() {
+ Map props = createProperties();
+
+ ApplicationContext initialContext = ApplicationContext.run(props);
+ try {
+ props.put("datasources.default.schema-generate", "validate");
+ assertDoesNotThrow(() -> {
+ try (ApplicationContext validationContext = ApplicationContext.run(props)) {
+ }
+ });
+ } finally {
+ initialContext.close();
+ }
+ }
+
+ @Disabled
+ @Test
+ void validateSchemaForTckSchemaEntities() {
+ Map props = createProperties();
+ props.put("datasources.default.packages", "io.micronaut.data.tck.entities.schema");
+
+ ApplicationContext initialContext = ApplicationContext.run(props);
+ try {
+ props.put("datasources.default.schema-generate", "validate");
+ assertDoesNotThrow(() -> {
+ try (ApplicationContext validationContext = ApplicationContext.run(props)) {
+ }
+ });
+ } finally {
+ initialContext.close();
+ }
+ }
+
+ private Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("sqliteschema", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUUIDTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUUIDTest.java
new file mode 100644
index 00000000000..c0e9819de07
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUUIDTest.java
@@ -0,0 +1,82 @@
+package io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@MicronautTest
+@SQLiteDBProperties(packages = "io.micronaut.data.jdbc.sqlite")
+class SqliteUUIDTest {
+
+ @Inject
+ SqliteUuidRepository uuidRepository;
+
+ @AfterEach
+ void cleanup() {
+ uuidRepository.deleteAll();
+ }
+
+ @Test
+ void testInsertAndUpdateWithUUID() {
+ SqliteUuidEntity test = uuidRepository.save(new SqliteUuidEntity("Fred"));
+ UUID uuid = test.getUuid();
+
+ assertNotNull(uuid);
+
+ test = uuidRepository.findById(test.getUuid()).orElse(null);
+
+ assertNotNull(test);
+ assertEquals(uuid, test.getUuid());
+ assertEquals("Fred", test.getName());
+
+ test = uuidRepository.update(test);
+
+ assertEquals(uuid, test.getUuid());
+ assertEquals("Fred", test.getName());
+ }
+
+ @Test
+ void testInsertAndReturnUuid() {
+ SqliteUuidEntity test = uuidRepository.save(new SqliteUuidEntity("Fred"));
+ UUID uuid = test.getUuid();
+
+ assertNotNull(uuid);
+
+ test = uuidRepository.findById(test.getUuid()).orElse(null);
+ UUID foundUuid = uuidRepository.findUuidByName("Fred");
+
+ assertNotNull(test);
+ assertEquals(uuid, foundUuid);
+ }
+
+ @Test
+ void testInsertAndUpdateNullUuid() {
+ SqliteUuidEntity test = uuidRepository.save(new SqliteUuidEntity("Fred", UUID.randomUUID()));
+ UUID uuid = test.getUuid();
+
+ assertNotNull(uuid);
+ assertNotNull(test.getUuid());
+
+ test.setNullableValue(null);
+ SqliteUuidEntity updatedTest = uuidRepository.update(test);
+
+ assertNotNull(updatedTest);
+ assertNull(updatedTest.getNullableValue());
+ }
+
+ @Test
+ void testCriteriaWithNullValue() {
+ uuidRepository.save(new SqliteUuidEntity("Fred", null));
+ Collection result = uuidRepository.findByNullableValue(null);
+
+ assertEquals(1, result.size());
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidEntity.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidEntity.java
new file mode 100644
index 00000000000..ac18c6a9fbc
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidEntity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.AutoPopulated;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.MappedProperty;
+import io.micronaut.data.model.DataType;
+import jakarta.persistence.Column;
+
+import java.util.UUID;
+
+@MappedEntity("uuid_entity")
+public class SqliteUuidEntity {
+
+ @AutoPopulated
+ @Id
+ private UUID uuid;
+
+ private String name;
+
+ @Column(nullable = true)
+ @MappedProperty(type = DataType.UUID)
+ private UUID nullableValue;
+
+ public SqliteUuidEntity() {
+ }
+
+ public SqliteUuidEntity(String name) {
+ this.name = name;
+ }
+
+ public SqliteUuidEntity(String name, UUID nullableValue) {
+ this.name = name;
+ this.nullableValue = nullableValue;
+ }
+
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(UUID uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public UUID getNullableValue() {
+ return nullableValue;
+ }
+
+ public void setNullableValue(UUID nullableValue) {
+ this.nullableValue = nullableValue;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidRepository.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidRepository.java
new file mode 100644
index 00000000000..5407d10b10f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidRepository.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017-2025 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.context.annotation.Parameter;
+import io.micronaut.data.annotation.Query;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+
+import java.util.Collection;
+import java.util.UUID;
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+public interface SqliteUuidRepository extends CrudRepository {
+
+ UUID findUuidByName(String name);
+
+ @Query(value = "select * from uuid_entity where :param is null", nativeQuery = true)
+ Collection findByNullableValue(@Parameter("param") @io.micronaut.core.annotation.Nullable UUID param);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/TableRatings.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/TableRatings.java
new file mode 100644
index 00000000000..88f1f028b3f
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/TableRatings.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017-2020 original 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 io.micronaut.data.jdbc.sqlite;
+
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.MappedProperty;
+import io.micronaut.data.model.DataType;
+
+@MappedEntity(value = "T-Table-Ratings", escape = true)
+public class TableRatings {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @MappedProperty(value = "T-Rating", type = DataType.INTEGER)
+ private final int rating;
+
+ public TableRatings(int rating) {
+ this.rating = rating;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public int getRating() {
+ return rating;
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidCascadePersistTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidCascadePersistTest.java
new file mode 100644
index 00000000000..61c1559860a
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidCascadePersistTest.java
@@ -0,0 +1,194 @@
+package io.micronaut.data.jdbc.sqlite.assignedid;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import jakarta.annotation.Nullable;
+import jakarta.validation.constraints.NotBlank;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AssignedUuidCascadePersistTest {
+
+ @Test
+ void shouldPersistChildrenWithAssignedUuidsViaCascadePersist() {
+ try (ApplicationContext ctx = ApplicationContext.run(createProperties())) {
+ TenantRepository tenantRepository = ctx.getBean(TenantRepository.class);
+ RoleRepository roleRepository = ctx.getBean(RoleRepository.class);
+
+ UUID tenantId = UUID.randomUUID();
+ Tenant tenant = new Tenant();
+ tenant.setId(tenantId);
+ tenant.setName("Acme");
+ tenant.setHost(true);
+ tenant.setServiceProvider(true);
+ for (int i = 0; i < 3; i++) {
+ Role role = new Role();
+ role.setId(UUID.randomUUID());
+ role.setName("Role" + i);
+ role.setDescription("test role");
+ tenant.addRole(role);
+ }
+
+ tenantRepository.save(tenant);
+
+ assertTrue(tenantRepository.findById(tenantId).isPresent());
+ assertEquals(3, roleRepository.countByTenantId(tenantId));
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("assigneduuidcascadepersist", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite.assignedid");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@MappedEntity("tenant")
+class Tenant {
+
+ @Id
+ private UUID id;
+
+ @NotBlank
+ private String name;
+
+ private boolean host;
+ private boolean serviceProvider;
+
+ @Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "tenant", cascade = Relation.Cascade.ALL)
+ private List roles = new ArrayList<>();
+
+ UUID getId() {
+ return id;
+ }
+
+ void setId(UUID id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ boolean isHost() {
+ return host;
+ }
+
+ void setHost(boolean host) {
+ this.host = host;
+ }
+
+ boolean isServiceProvider() {
+ return serviceProvider;
+ }
+
+ void setServiceProvider(boolean serviceProvider) {
+ this.serviceProvider = serviceProvider;
+ }
+
+ List getRoles() {
+ return roles;
+ }
+
+ void setRoles(List roles) {
+ this.roles = roles;
+ }
+
+ void addRole(Role role) {
+ if (role != null) {
+ role.setTenant(this);
+ roles.add(role);
+ }
+ }
+}
+
+@MappedEntity("role")
+class Role {
+
+ @Id
+ private UUID id;
+
+ @NotBlank
+ private String name;
+
+ @Nullable
+ private String description;
+
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ private Tenant tenant;
+
+ UUID getId() {
+ return id;
+ }
+
+ void setId(UUID id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ @Nullable
+ String getDescription() {
+ return description;
+ }
+
+ void setDescription(@Nullable String description) {
+ this.description = description;
+ }
+
+ Tenant getTenant() {
+ return tenant;
+ }
+
+ void setTenant(Tenant tenant) {
+ this.tenant = tenant;
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface TenantRepository extends CrudRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface RoleRepository extends CrudRepository {
+ long countByTenantId(UUID tenantId);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidManyToManyPersistTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidManyToManyPersistTest.java
new file mode 100644
index 00000000000..06d37cef4ae
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidManyToManyPersistTest.java
@@ -0,0 +1,189 @@
+package io.micronaut.data.jdbc.sqlite.assignedid;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.CrudRepository;
+import jakarta.validation.constraints.NotBlank;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class AssignedUuidManyToManyPersistTest {
+
+ @Disabled("Cascade update does not remove existing link records, issue https://github.com/micronaut-projects/micronaut-data/issues/3722")
+ @Test
+ void shouldPersistJoinRowsWithAssignedUuidsViaCascadePersistAndSupportUpdate() {
+ try (ApplicationContext ctx = ApplicationContext.run(createProperties())) {
+ JdbcStudentRepository studentRepository = ctx.getBean(JdbcStudentRepository.class);
+ JdbcCourseRepository courseRepository = ctx.getBean(JdbcCourseRepository.class);
+
+ Student student = new Student();
+ student.setId(UUID.randomUUID());
+ student.setName("Denis");
+ Course course1 = new Course();
+ course1.setId(UUID.randomUUID());
+ course1.setName("Math");
+ Course course2 = new Course();
+ course2.setId(UUID.randomUUID());
+ course2.setName("Physics");
+
+ courseRepository.save(course1);
+ courseRepository.save(course2);
+ student.addCourse(course1);
+ student.addCourse(course2);
+
+ studentRepository.save(student);
+ Student saved = studentRepository.findById(student.getId()).orElse(null);
+
+ assertNotNull(saved);
+ assertEquals(Set.of(course1.getId(), course2.getId()), saved.getCourses().stream().map(Course::getId).collect(Collectors.toSet()));
+
+ Student student2 = new Student();
+ student2.setId(UUID.randomUUID());
+ student2.setName("John");
+ student2.addCourse(course1);
+ studentRepository.save(student2);
+ Student found = studentRepository.findById(student2.getId()).orElse(null);
+
+ assertNotNull(found);
+ assertEquals(Set.of(course1.getId()), found.getCourses().stream().map(Course::getId).collect(Collectors.toSet()));
+
+ course1.setName("Mathematics");
+ student2.setCourses(List.of(course1));
+ studentRepository.update(student2);
+ Student found2 = studentRepository.findById(student2.getId()).orElse(null);
+
+ assertNotNull(found2);
+ assertEquals(Set.of("Mathematics"), found2.getCourses().stream().map(Course::getName).collect(Collectors.toSet()));
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("assigneduuidmanytomanypersist", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite.assignedid");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@MappedEntity("student_assigned")
+class Student {
+
+ @Id
+ private UUID id;
+
+ @NotBlank
+ private String name;
+
+ @Relation(value = Relation.Kind.MANY_TO_MANY, cascade = {Relation.Cascade.PERSIST, Relation.Cascade.UPDATE})
+ private List courses = new ArrayList<>();
+
+ UUID getId() {
+ return id;
+ }
+
+ void setId(UUID id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ List getCourses() {
+ return courses;
+ }
+
+ void setCourses(List courses) {
+ this.courses = courses;
+ }
+
+ void addCourse(Course course) {
+ if (course != null) {
+ courses.add(course);
+ }
+ }
+}
+
+@MappedEntity("course_assigned")
+class Course {
+
+ @Id
+ private UUID id;
+
+ @NotBlank
+ private String name;
+
+ @Relation(value = Relation.Kind.MANY_TO_MANY, mappedBy = "courses")
+ private List students = new ArrayList<>();
+
+ UUID getId() {
+ return id;
+ }
+
+ void setId(UUID id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ List getStudents() {
+ return students;
+ }
+
+ void setStudents(List students) {
+ this.students = students;
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface JdbcStudentRepository extends CrudRepository {
+
+ @Join("courses")
+ Optional findById(UUID id);
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface JdbcCourseRepository extends CrudRepository {
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/AutoPopulateEmbeddedTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/AutoPopulateEmbeddedTest.java
new file mode 100644
index 00000000000..fbdfe75af81
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/AutoPopulateEmbeddedTest.java
@@ -0,0 +1,245 @@
+package io.micronaut.data.jdbc.sqlite.autopopulate;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.data.annotation.AutoPopulated;
+import io.micronaut.data.annotation.DateCreated;
+import io.micronaut.data.annotation.DateUpdated;
+import io.micronaut.data.annotation.Embeddable;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.Relation;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.serde.annotation.Serdeable;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class AutoPopulateEmbeddedTest {
+
+ @Test
+ void testEmbeddableFieldsAutoPopulated() {
+ try (ApplicationContext applicationContext = ApplicationContext.run(createProperties())) {
+ MyAuditableEntityRepository repository = applicationContext.getBean(MyAuditableEntityRepository.class);
+
+ MyAuditableEntity entity = new MyAuditableEntity();
+ entity.setId("id1");
+ entity.setFirstName("Peter");
+
+ MyAuditableEntity saved = repository.save(entity);
+ MyAuditableEntity loaded = repository.findById(saved.getId()).orElse(null);
+
+ assertNotNull(loaded);
+ assertEquals(saved.getId(), loaded.getId());
+ assertEquals("Peter", loaded.getFirstName());
+ assertNotNull(loaded.getCreatedAt());
+ assertNotNull(loaded.getUpdatedAt());
+ assertNotNull(loaded.getGuid());
+ assertNotNull(loaded.getAuditFields());
+ assertNotNull(loaded.getAuditFields().getInnerCreatedAt());
+ assertNotNull(loaded.getAuditFields().getInnerUpdatedAt());
+ assertNotNull(loaded.getAuditFields().getInnerGuid());
+ assertNotNull(loaded.getAuditFields().getInnerFields());
+ assertNotNull(loaded.getAuditFields().getInnerFields().subInnerCreatedAt());
+ assertNotNull(loaded.getAuditFields().getInnerFields().subInnerGuid());
+ assertNull(loaded.getOtherAuditFields());
+ }
+ }
+
+ private static Map createProperties() {
+ try {
+ var databaseFile = Files.createTempFile("autopopulateembedded", ".sqlite").toFile();
+ databaseFile.deleteOnExit();
+ Map properties = new HashMap<>();
+ properties.put("datasources.default.url", "jdbc:sqlite:" + databaseFile.getAbsolutePath());
+ properties.put("datasources.default.schema-generate", "CREATE");
+ properties.put("datasources.default.dialect", "SQLITE");
+ properties.put("datasources.default.db-type", "sqlite");
+ properties.put("datasources.default.username", "");
+ properties.put("datasources.default.password", "");
+ properties.put("datasources.default.packages", "io.micronaut.data.jdbc.sqlite.autopopulate");
+ properties.put("datasources.default.driverClassName", "org.sqlite.JDBC");
+ return properties;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unable to create SQLite test database", e);
+ }
+ }
+}
+
+@Embeddable
+class AuditFields {
+
+ @DateCreated
+ private LocalDateTime innerCreatedAt;
+
+ @DateUpdated
+ private LocalDateTime innerUpdatedAt;
+
+ @AutoPopulated
+ private UUID innerGuid;
+
+ @Relation(Relation.Kind.EMBEDDED)
+ private InnerFields innerFields;
+
+ LocalDateTime getInnerCreatedAt() {
+ return innerCreatedAt;
+ }
+
+ void setInnerCreatedAt(LocalDateTime innerCreatedAt) {
+ this.innerCreatedAt = innerCreatedAt;
+ }
+
+ LocalDateTime getInnerUpdatedAt() {
+ return innerUpdatedAt;
+ }
+
+ void setInnerUpdatedAt(LocalDateTime innerUpdatedAt) {
+ this.innerUpdatedAt = innerUpdatedAt;
+ }
+
+ UUID getInnerGuid() {
+ return innerGuid;
+ }
+
+ void setInnerGuid(UUID innerGuid) {
+ this.innerGuid = innerGuid;
+ }
+
+ InnerFields getInnerFields() {
+ return innerFields;
+ }
+
+ void setInnerFields(InnerFields innerFields) {
+ this.innerFields = innerFields;
+ }
+}
+
+@Embeddable
+class OtherAuditFields {
+
+ @DateCreated
+ private LocalDateTime otherInnerCreatedAt;
+
+ @DateUpdated
+ private LocalDateTime otherInnerUpdatedAt;
+
+ @AutoPopulated
+ private UUID otherInnerGuid;
+
+ OtherAuditFields(LocalDateTime otherInnerCreatedAt, LocalDateTime otherInnerUpdatedAt, UUID otherInnerGuid) {
+ this.otherInnerCreatedAt = otherInnerCreatedAt;
+ this.otherInnerUpdatedAt = otherInnerUpdatedAt;
+ this.otherInnerGuid = otherInnerGuid;
+ }
+
+ LocalDateTime getOtherInnerCreatedAt() {
+ return otherInnerCreatedAt;
+ }
+
+ LocalDateTime getOtherInnerUpdatedAt() {
+ return otherInnerUpdatedAt;
+ }
+
+ UUID getOtherInnerGuid() {
+ return otherInnerGuid;
+ }
+}
+
+@Serdeable
+@MappedEntity("my_auditable_entity")
+class MyAuditableEntity {
+ @Id
+ private String id;
+ private String firstName;
+
+ @DateCreated
+ private LocalDateTime createdAt;
+
+ @DateUpdated
+ private LocalDateTime updatedAt;
+
+ @AutoPopulated
+ private UUID guid;
+
+ @Relation(Relation.Kind.EMBEDDED)
+ private AuditFields auditFields;
+
+ @Relation(Relation.Kind.EMBEDDED)
+ private OtherAuditFields otherAuditFields;
+
+ String getId() {
+ return id;
+ }
+
+ void setId(String id) {
+ this.id = id;
+ }
+
+ String getFirstName() {
+ return firstName;
+ }
+
+ void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ UUID getGuid() {
+ return guid;
+ }
+
+ void setGuid(UUID guid) {
+ this.guid = guid;
+ }
+
+ AuditFields getAuditFields() {
+ return auditFields;
+ }
+
+ void setAuditFields(AuditFields auditFields) {
+ this.auditFields = auditFields;
+ }
+
+ OtherAuditFields getOtherAuditFields() {
+ return otherAuditFields;
+ }
+
+ void setOtherAuditFields(OtherAuditFields otherAuditFields) {
+ this.otherAuditFields = otherAuditFields;
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface MyAuditableEntityRepository extends GenericRepository {
+
+ MyAuditableEntity save(MyAuditableEntity entity);
+
+ Optional findById(String id);
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/InnerFields.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/InnerFields.java
new file mode 100644
index 00000000000..635b36f2ec2
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/InnerFields.java
@@ -0,0 +1,20 @@
+package io.micronaut.data.jdbc.sqlite.autopopulate;
+
+import io.micronaut.data.annotation.AutoPopulated;
+import io.micronaut.data.annotation.DateCreated;
+import io.micronaut.data.annotation.Embeddable;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Embeddable
+record InnerFields(
+ @DateCreated
+ LocalDateTime subInnerCreatedAt,
+ @AutoPopulated
+ UUID subInnerGuid
+) {
+ public InnerFields() {
+ this(null, null);
+ }
+}
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/package-info.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/package-info.java
new file mode 100644
index 00000000000..0a37759206a
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Tests with {@link io.micronaut.data.annotation.DateCreated} and {@link io.micronaut.data.annotation.DateUpdated} autopopulated fields.
+ */
+package io.micronaut.data.jdbc.sqlite.autopopulate;
diff --git a/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/composite/CompositeTest.java b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/composite/CompositeTest.java
new file mode 100644
index 00000000000..2da8d9452eb
--- /dev/null
+++ b/test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/composite/CompositeTest.java
@@ -0,0 +1,761 @@
+package io.micronaut.data.jdbc.sqlite.composite;
+
+import io.micronaut.data.annotation.Embeddable;
+import io.micronaut.data.annotation.EmbeddedId;
+import io.micronaut.data.annotation.GeneratedValue;
+import io.micronaut.data.annotation.Id;
+import io.micronaut.data.annotation.Join;
+import io.micronaut.data.annotation.MappedEntity;
+import io.micronaut.data.annotation.MappedProperty;
+import io.micronaut.data.annotation.Relation;
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.jdbc.sqlite.SQLiteDBProperties;
+import io.micronaut.data.model.Pageable;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
+import io.micronaut.data.repository.CrudRepository;
+import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
+import io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder;
+import io.micronaut.data.runtime.criteria.RuntimeCriteriaBuilder;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import org.jspecify.annotations.NonNull;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@MicronautTest
+@SQLiteDBProperties(packages = "io.micronaut.data.jdbc.sqlite.composite")
+class CompositeTest {
+
+ @Inject
+ SettlementRepository settlementRepository;
+
+ @Inject
+ SettlementTypeRepository settlementTypeRepository;
+
+ @Inject
+ ZoneRepository zoneRepository;
+
+ @Inject
+ CountryRepository countryRepository;
+
+ @Inject
+ CitizenRepository citizenRepository;
+
+ @Inject
+ RuntimeCriteriaBuilder builder;
+
+ @Disabled("citizenRepository.save(citizen) should create join table entries without any cascade")
+ @Test
+ void testInsert() {
+ Settlement settlement = createSettlement();
+
+ settlementTypeRepository.save(settlement.getSettlementType());
+ zoneRepository.save(settlement.getZone());
+ settlementRepository.save(settlement);
+ settlement = settlementRepository.findById(settlement.getId()).orElseThrow();
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement", null, true);
+ assertEquals(1L, settlement.getZone().getId());
+ assertEquals(1L, settlement.getSettlementType().getId());
+
+ settlement.setDescription("New settlement MODIFIED");
+ settlementRepository.update(settlement);
+ settlement = settlementRepository.findById(settlement.getId()).orElseThrow();
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement MODIFIED", null, true);
+
+ settlement.getId().getCounty().setCountyName("Czech Republic");
+ settlement.getId().getCounty().setEnabled(true);
+ countryRepository.save(settlement.getId().getCounty());
+ settlement = settlementRepository.queryById(settlement.getId()).orElseThrow();
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement MODIFIED", "Czech Republic", true);
+
+ Citizen citizen = new Citizen();
+ citizen.setName("Jack");
+ citizen.setSettlements(List.of(settlement));
+ citizenRepository.save(citizen);
+
+ assertNotNull(citizen.getId());
+ assertEquals("Jack", citizen.getName());
+
+ citizenRepository.queryById(citizen.getId()).orElseThrow();
+ citizen = citizenRepository.findById(citizen.getId()).orElseThrow();
+
+ assertNotNull(citizen.getId());
+ assertSettlement(citizen.getSettlements().getFirst(), "Some", "Danger", "New settlement MODIFIED", "Czech Republic", true);
+
+ citizenRepository.update(citizen);
+ citizen = citizenRepository.queryById(citizen.getId()).orElseThrow();
+
+ assertNotNull(citizen.getId());
+ assertEquals("Jack", citizen.getName());
+ assertNull(citizen.getSettlements());
+
+ citizenRepository.update(citizen);
+ citizen = citizenRepository.findById(citizen.getId()).orElseThrow();
+
+ assertNotNull(citizen.getId());
+ assertSettlement(citizen.getSettlements().getFirst(), "Some", "Danger", "New settlement MODIFIED", "Czech Republic", true);
+
+ List settlements = settlementRepository.findAll(Pageable.from(0, 10));
+
+ assertEquals(1, settlements.size());
+ assertSettlement(settlements.getFirst(), "Some", "Danger", "New settlement MODIFIED", "Czech Republic", true);
+ }
+
+ @Test
+ void testCriteria() {
+ citizenRepository.deleteAll();
+ settlementRepository.deleteAll();
+ countryRepository.deleteAll();
+ zoneRepository.deleteAll();
+ settlementTypeRepository.deleteAll();
+
+ Settlement settlement = createSettlement();
+
+ settlementTypeRepository.save(settlement.getSettlementType());
+ zoneRepository.save(settlement.getZone());
+ settlementRepository.save(settlement);
+ settlement = settlementRepository.findById(settlement.getId()).orElseThrow();
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement", null, true);
+ assertNotNull(settlement.getZone().getId());
+ assertNotNull(settlement.getSettlementType().getId());
+
+ settlement.setDescription("New settlement MODIFIED");
+ settlementRepository.update(settlement);
+ settlement = settlementRepository.findById(settlement.getId()).orElseThrow();
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement MODIFIED", null, true);
+
+ SettlementPk settlementId = settlement.getId();
+ settlementId.getCounty().setCountyName("Czech Republic");
+ settlementId.getCounty().setEnabled(true);
+ countryRepository.save(settlementId.getCounty());
+ settlement = settlementRepository.findOne(new CriteriaQueryBuilder<>() {
+ @Override
+ public CriteriaQuery build(CriteriaBuilder criteriaBuilder) {
+ CriteriaQuery query = criteriaBuilder.createQuery(Settlement.class);
+ var root = query.from(Settlement.class);
+ root.fetch("settlementType");
+ root.fetch("zone");
+ root.fetch("id.county");
+ return query.where(criteriaBuilder.equal(root.get("id"), settlementId));
+ }
+ });
+
+ assertSettlement(settlement, "Some", "Danger", "New settlement MODIFIED", "Czech Republic", true);
+ }
+
+ @Test
+ void testBuildCreateSettlement() {
+ SqlQueryBuilder encoder = new SqlQueryBuilder(Dialect.SQLITE);
+ String[] statements = encoder.buildCreateTableStatements(builder.getRuntimeEntityRegistry().getEntity(Settlement.class));
+
+ assertEquals("CREATE TABLE \"comp_settlement\" (\"code\" VARCHAR(255) NOT NULL,\"code_id\" INT NOT NULL,\"id_county_id_id\" INT NOT NULL,\"id_county_id_state_id\" INT NOT NULL,\"description\" VARCHAR(255) NOT NULL,\"settlement_type_id\" BIGINT NOT NULL,\"zone_id\" BIGINT NOT NULL,\"is_enabled\" BOOLEAN NOT NULL, PRIMARY KEY(\"code\",\"code_id\",\"id_county_id_id\",\"id_county_id_state_id\"));", String.join("\n", statements));
+ }
+
+ @Test
+ void testBuildCreateCitizen() {
+ SqlQueryBuilder encoder = new SqlQueryBuilder(Dialect.SQLITE);
+ String[] statements = encoder.buildCreateTableStatements(builder.getRuntimeEntityRegistry().getEntity(Citizen.class));
+
+ assertEquals(2, statements.length);
+ assertEquals("CREATE TABLE \"citizen_settlement\" (\"citizen_id\" BIGINT NOT NULL,\"settlement_id_code\" VARCHAR(255) NOT NULL,\"settlement_id_code_id\" INT NOT NULL,\"settlement_id_county_id_id\" INT NOT NULL,\"settlement_id_county_id_state_id\" INT NOT NULL, PRIMARY KEY(\"citizen_id\",\"settlement_id_code\",\"settlement_id_code_id\",\"settlement_id_county_id_id\",\"settlement_id_county_id_state_id\"));", statements[0]);
+ assertEquals("CREATE TABLE \"comp_citizen\" (\"id\" INTEGER PRIMARY KEY,\"name\" VARCHAR(255) NOT NULL);", statements[1]);
+ }
+
+ @Test
+ void testBuildInsert() {
+ var res = builder.createCriteriaInsert(Settlement.class).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("INSERT INTO \"comp_settlement\" (\"description\",\"settlement_type_id\",\"zone_id\",\"is_enabled\",\"code\",\"code_id\",\"id_county_id_id\",\"id_county_id_state_id\") VALUES (?,?,?,?,?,?,?,?)", res.getQuery());
+ assertEquals(List.of("description", "settlementType.id", "zone.id", "enabled", "id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ res.getParameters().get("1"),
+ res.getParameters().get("2"),
+ res.getParameters().get("3"),
+ res.getParameters().get("4"),
+ res.getParameters().get("5"),
+ res.getParameters().get("6"),
+ res.getParameters().get("7"),
+ res.getParameters().get("8")
+ ));
+ }
+
+ @Test
+ void testUpdateInsert() {
+ var query = builder.createCriteriaUpdate(Settlement.class);
+ query = query.where(builder.equal(query.getRoot().id(), builder.parameter(Object.class)));
+ for (String prop : query.getRoot().getPersistentEntity().getPersistentPropertyNames()) {
+ query.set(prop, builder.parameter(Object.class));
+ }
+ var res = query.build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("UPDATE \"comp_settlement\" SET \"code\"=?,\"code_id\"=?,\"id_county_id_id\"=?,\"id_county_id_state_id\"=?,\"description\"=?,\"settlement_type_id\"=?,\"zone_id\"=?,\"is_enabled\"=? WHERE (\"code\" = ? AND \"code_id\" = ? AND \"id_county_id_id\" = ? AND \"id_county_id_state_id\" = ?)", res.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id", "description", "settlementType.id", "zone.id", "enabled", "id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ res.getParameters().get("1"),
+ res.getParameters().get("2"),
+ res.getParameters().get("3"),
+ res.getParameters().get("4"),
+ res.getParameters().get("5"),
+ res.getParameters().get("6"),
+ res.getParameters().get("7"),
+ res.getParameters().get("8"),
+ res.getParameters().get("9"),
+ res.getParameters().get("10"),
+ res.getParameters().get("11"),
+ res.getParameters().get("12")
+ ));
+ }
+
+ @Test
+ void testBuildQueryByIdParameter() {
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ var q = query.where(builder.equal(root.id(), builder.parameter(SettlementPk.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\" FROM \"comp_settlement\" settlement_ WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ }
+
+ @Test
+ void testBuildQueryByIdValue() {
+ var settlementPk = new SettlementPk();
+ settlementPk.setCode("Kode");
+ settlementPk.setCodeId(123);
+
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ var q = query.where(builder.equal(root.id(), settlementPk)).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\" FROM \"comp_settlement\" settlement_ WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ assertEquals("Kode", q.getParameterBindings().get(0).getValue());
+ assertEquals(123, q.getParameterBindings().get(1).getValue());
+ }
+
+ @Test
+ void testBuildQuery2() {
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ root.join("settlementType", Join.Type.FETCH);
+ root.join("zone", Join.Type.FETCH);
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\",settlement_settlement_type_.\"name\" AS settlement_type_name,settlement_zone_.\"name\" AS zone_name FROM \"comp_settlement\" settlement_ INNER JOIN \"comp_zone\" settlement_zone_ ON settlement_.\"zone_id\"=settlement_zone_.\"id\" INNER JOIN \"comp_sett_type\" settlement_settlement_type_ ON settlement_.\"settlement_type_id\"=settlement_settlement_type_.\"id\" WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ }
+
+ @Test
+ void testBuildQuery2Fetch() {
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ root.fetch("settlementType");
+ root.fetch("zone");
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\",settlement_settlement_type_.\"name\" AS settlement_type_name,settlement_zone_.\"name\" AS zone_name FROM \"comp_settlement\" settlement_ INNER JOIN \"comp_zone\" settlement_zone_ ON settlement_.\"zone_id\"=settlement_zone_.\"id\" INNER JOIN \"comp_sett_type\" settlement_settlement_type_ ON settlement_.\"settlement_type_id\"=settlement_settlement_type_.\"id\" WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ }
+
+ @Test
+ void testBuildQuery3() {
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ root.join("settlementType", Join.Type.FETCH);
+ root.join("zone", Join.Type.FETCH);
+ root.join("id.county", Join.Type.FETCH);
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\",settlement_settlement_type_.\"name\" AS settlement_type_name,settlement_id_county_.\"county_name\" AS id_county_county_name,settlement_id_county_.\"is_enabled\" AS id_county_is_enabled,settlement_zone_.\"name\" AS zone_name FROM \"comp_settlement\" settlement_ INNER JOIN \"comp_zone\" settlement_zone_ ON settlement_.\"zone_id\"=settlement_zone_.\"id\" INNER JOIN \"comp_country\" settlement_id_county_ ON settlement_.\"id_county_id_id\"=settlement_id_county_.\"id\" AND settlement_.\"id_county_id_state_id\"=settlement_id_county_.\"state_id\" INNER JOIN \"comp_sett_type\" settlement_settlement_type_ ON settlement_.\"settlement_type_id\"=settlement_settlement_type_.\"id\" WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ }
+
+ @Test
+ void testBuildQuery3Fetch() {
+ var query = builder.createQuery();
+ var root = query.from(Settlement.class);
+ root.fetch("settlementType");
+ root.fetch("zone");
+ root.fetch("id.county");
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT settlement_.\"code\",settlement_.\"code_id\",settlement_.\"id_county_id_id\",settlement_.\"id_county_id_state_id\",settlement_.\"description\",settlement_.\"settlement_type_id\",settlement_.\"zone_id\",settlement_.\"is_enabled\",settlement_settlement_type_.\"name\" AS settlement_type_name,settlement_id_county_.\"county_name\" AS id_county_county_name,settlement_id_county_.\"is_enabled\" AS id_county_is_enabled,settlement_zone_.\"name\" AS zone_name FROM \"comp_settlement\" settlement_ INNER JOIN \"comp_zone\" settlement_zone_ ON settlement_.\"zone_id\"=settlement_zone_.\"id\" INNER JOIN \"comp_country\" settlement_id_county_ ON settlement_.\"id_county_id_id\"=settlement_id_county_.\"id\" AND settlement_.\"id_county_id_state_id\"=settlement_id_county_.\"state_id\" INNER JOIN \"comp_sett_type\" settlement_settlement_type_ ON settlement_.\"settlement_type_id\"=settlement_settlement_type_.\"id\" WHERE (settlement_.\"code\" = ? AND settlement_.\"code_id\" = ? AND settlement_.\"id_county_id_id\" = ? AND settlement_.\"id_county_id_state_id\" = ?)", q.getQuery());
+ assertEquals(List.of("id.code", "id.codeId", "id.county.id.id", "id.county.id.state.id"), List.of(
+ q.getParameters().get("1"),
+ q.getParameters().get("2"),
+ q.getParameters().get("3"),
+ q.getParameters().get("4")
+ ));
+ }
+
+ @Test
+ void testBuildQuery4() {
+ var query = builder.createQuery();
+ var root = query.from(Citizen.class);
+ root.join("settlements", Join.Type.FETCH);
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT citizen_.\"id\",citizen_.\"name\",citizen_settlements_.\"code\" AS settlements_code,citizen_settlements_.\"code_id\" AS settlements_code_id,citizen_settlements_.\"id_county_id_id\" AS settlements_id_county_id_id,citizen_settlements_.\"id_county_id_state_id\" AS settlements_id_county_id_state_id,citizen_settlements_.\"description\" AS settlements_description,citizen_settlements_.\"settlement_type_id\" AS settlements_settlement_type_id,citizen_settlements_.\"zone_id\" AS settlements_zone_id,citizen_settlements_.\"is_enabled\" AS settlements_is_enabled FROM \"comp_citizen\" citizen_ INNER JOIN \"citizen_settlement\" citizen_settlements_citizen_settlement_ ON citizen_.\"id\"=citizen_settlements_citizen_settlement_.\"citizen_id\" INNER JOIN \"comp_settlement\" citizen_settlements_ ON citizen_settlements_citizen_settlement_.\"settlement_id_code\"=citizen_settlements_.\"code\" AND citizen_settlements_citizen_settlement_.\"settlement_id_code_id\"=citizen_settlements_.\"code_id\" AND citizen_settlements_citizen_settlement_.\"settlement_id_county_id_id\"=citizen_settlements_.\"id_county_id_id\" AND citizen_settlements_citizen_settlement_.\"settlement_id_county_id_state_id\"=citizen_settlements_.\"id_county_id_state_id\" WHERE (citizen_.\"id\" = ?)", q.getQuery());
+ assertEquals("id", q.getParameters().get("1"));
+ }
+
+ @Test
+ void testBuildQuery4Fetch() {
+ var query = builder.createQuery();
+ var root = query.from(Citizen.class);
+ root.fetch("settlements");
+ var q = query.where(builder.equal(root.id(), builder.parameter(Object.class))).build(new SqlQueryBuilder(Dialect.SQLITE));
+
+ assertEquals("SELECT citizen_.\"id\",citizen_.\"name\",citizen_settlements_.\"code\" AS settlements_code,citizen_settlements_.\"code_id\" AS settlements_code_id,citizen_settlements_.\"id_county_id_id\" AS settlements_id_county_id_id,citizen_settlements_.\"id_county_id_state_id\" AS settlements_id_county_id_state_id,citizen_settlements_.\"description\" AS settlements_description,citizen_settlements_.\"settlement_type_id\" AS settlements_settlement_type_id,citizen_settlements_.\"zone_id\" AS settlements_zone_id,citizen_settlements_.\"is_enabled\" AS settlements_is_enabled FROM \"comp_citizen\" citizen_ INNER JOIN \"citizen_settlement\" citizen_settlements_citizen_settlement_ ON citizen_.\"id\"=citizen_settlements_citizen_settlement_.\"citizen_id\" INNER JOIN \"comp_settlement\" citizen_settlements_ ON citizen_settlements_citizen_settlement_.\"settlement_id_code\"=citizen_settlements_.\"code\" AND citizen_settlements_citizen_settlement_.\"settlement_id_code_id\"=citizen_settlements_.\"code_id\" AND citizen_settlements_citizen_settlement_.\"settlement_id_county_id_id\"=citizen_settlements_.\"id_county_id_id\" AND citizen_settlements_citizen_settlement_.\"settlement_id_county_id_state_id\"=citizen_settlements_.\"id_county_id_state_id\" WHERE (citizen_.\"id\" = ?)", q.getQuery());
+ assertEquals("id", q.getParameters().get("1"));
+ }
+
+ private Settlement createSettlement() {
+ Settlement settlement = new Settlement();
+ State state = new State();
+ state.setId(12);
+ SettlementType type = new SettlementType();
+ type.setName("Some");
+ County county = new County();
+ CountyPk countyPk = new CountyPk();
+ countyPk.setId(44);
+ countyPk.setState(state);
+ county.setId(countyPk);
+ county.setCountyName("Costa Rica");
+ Zone zone = new Zone();
+ zone.setName("Danger");
+ SettlementPk setPk = new SettlementPk();
+ setPk.setCode("20010");
+ setPk.setCodeId(9);
+ setPk.setCounty(county);
+ settlement.setId(setPk);
+ settlement.setZone(zone);
+ settlement.setSettlementType(type);
+ settlement.setDescription("New settlement");
+ settlement.setEnabled(true);
+ return settlement;
+ }
+
+ private void assertSettlement(Settlement settlement, String typeName, String zoneName, String description, String countyName, boolean enabled) {
+ assertNotNull(settlement.getId());
+ assertEquals("20010", settlement.getId().getCode());
+ assertEquals(9, settlement.getId().getCodeId());
+ assertNotNull(settlement.getId().getCounty().getId());
+ assertEquals(44, settlement.getId().getCounty().getId().getId());
+ assertEquals(12, settlement.getId().getCounty().getId().getState().getId());
+ if (countyName == null) {
+ assertNull(settlement.getId().getCounty().getCountyName());
+ } else {
+ assertEquals(countyName, settlement.getId().getCounty().getCountyName());
+ }
+ assertEquals(zoneName, settlement.getZone().getName());
+ assertEquals(typeName, settlement.getSettlementType().getName());
+ assertEquals(description, settlement.getDescription());
+ assertEquals(enabled, settlement.getEnabled());
+ }
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SettlementRepository extends CrudRepository, JpaSpecificationExecutor {
+
+ @Join(value = "settlementType", type = Join.Type.FETCH)
+ @Join(value = "zone", type = Join.Type.FETCH)
+ @Override
+ Optional findById(@NonNull SettlementPk settlementPk);
+
+ @Join(value = "settlementType", type = Join.Type.FETCH)
+ @Join(value = "zone", type = Join.Type.FETCH)
+ @Join(value = "id.county", type = Join.Type.FETCH)
+ Optional queryById(@NonNull SettlementPk settlementPk);
+
+ @Join(value = "settlementType", type = Join.Type.FETCH)
+ @Join(value = "zone", type = Join.Type.FETCH)
+ @Join(value = "id.county", type = Join.Type.FETCH)
+ List findAll(Pageable pageable);
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface SettlementTypeRepository extends CrudRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface ZoneRepository extends CrudRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface CountryRepository extends CrudRepository {
+}
+
+@JdbcRepository(dialect = Dialect.SQLITE)
+interface CitizenRepository extends CrudRepository {
+
+ @Join(value = "settlements", type = Join.Type.FETCH)
+ @Override
+ Optional findById(@NonNull Long id);
+
+ Optional queryById(@NonNull Long id);
+}
+
+@MappedEntity("comp_state")
+class State {
+
+ @Id
+ private Integer id;
+
+ @MappedProperty
+ private String stateName;
+
+ @MappedProperty("is_enabled")
+ private Boolean enabled;
+
+ Integer getId() {
+ return id;
+ }
+
+ void setId(Integer id) {
+ this.id = id;
+ }
+
+ String getStateName() {
+ return stateName;
+ }
+
+ void setStateName(String stateName) {
+ this.stateName = stateName;
+ }
+
+ Boolean getEnabled() {
+ return enabled;
+ }
+
+ void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+}
+
+@Embeddable
+class CountyPk {
+
+ @MappedProperty("id")
+ private Integer id;
+
+ @MappedProperty("state_id")
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ private State state;
+
+ Integer getId() {
+ return id;
+ }
+
+ void setId(Integer id) {
+ this.id = id;
+ }
+
+ State getState() {
+ return state;
+ }
+
+ void setState(State state) {
+ this.state = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CountyPk countyPk)) {
+ return false;
+ }
+ return Objects.equals(id, countyPk.id) && Objects.equals(state != null ? state.getId() : null, countyPk.state != null ? countyPk.state.getId() : null);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, state != null ? state.getId() : null);
+ }
+}
+
+@MappedEntity("comp_country")
+class County {
+
+ @EmbeddedId
+ @MappedProperty("id")
+ private CountyPk id;
+
+ @MappedProperty
+ private String countyName;
+
+ @MappedProperty("is_enabled")
+ private Boolean enabled;
+
+ CountyPk getId() {
+ return id;
+ }
+
+ void setId(CountyPk id) {
+ this.id = id;
+ }
+
+ String getCountyName() {
+ return countyName;
+ }
+
+ void setCountyName(String countyName) {
+ this.countyName = countyName;
+ }
+
+ Boolean getEnabled() {
+ return enabled;
+ }
+
+ void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+}
+
+@Embeddable
+class SettlementPk {
+
+ @MappedProperty("code")
+ private String code;
+
+ @MappedProperty("code_id")
+ private Integer codeId;
+
+ @Relation(value = Relation.Kind.MANY_TO_ONE)
+ private County county;
+
+ String getCode() {
+ return code;
+ }
+
+ void setCode(String code) {
+ this.code = code;
+ }
+
+ Integer getCodeId() {
+ return codeId;
+ }
+
+ void setCodeId(Integer codeId) {
+ this.codeId = codeId;
+ }
+
+ County getCounty() {
+ return county;
+ }
+
+ void setCounty(County county) {
+ this.county = county;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SettlementPk that)) {
+ return false;
+ }
+ return Objects.equals(code, that.code)
+ && Objects.equals(codeId, that.codeId)
+ && Objects.equals(county != null ? county.getId() : null, that.county != null ? that.county.getId() : null);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(code, codeId, county != null ? county.getId() : null);
+ }
+}
+
+@MappedEntity("comp_settlement")
+class Settlement {
+
+ @EmbeddedId
+ @MappedProperty("id")
+ private SettlementPk id;
+
+ @MappedProperty
+ private String description;
+
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ private SettlementType settlementType;
+
+ @Relation(Relation.Kind.MANY_TO_ONE)
+ private Zone zone;
+
+ @MappedProperty("is_enabled")
+ private Boolean enabled;
+
+ SettlementPk getId() {
+ return id;
+ }
+
+ void setId(SettlementPk id) {
+ this.id = id;
+ }
+
+ String getDescription() {
+ return description;
+ }
+
+ void setDescription(String description) {
+ this.description = description;
+ }
+
+ SettlementType getSettlementType() {
+ return settlementType;
+ }
+
+ void setSettlementType(SettlementType settlementType) {
+ this.settlementType = settlementType;
+ }
+
+ Zone getZone() {
+ return zone;
+ }
+
+ void setZone(Zone zone) {
+ this.zone = zone;
+ }
+
+ Boolean getEnabled() {
+ return enabled;
+ }
+
+ void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+}
+
+@MappedEntity("comp_sett_type")
+class SettlementType {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @MappedProperty
+ private String name;
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+}
+
+@MappedEntity("comp_zone")
+class Zone {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @MappedProperty
+ private String name;
+
+ Long getId() {
+ return id;
+ }
+
+ void setId(Long id) {
+ this.id = id;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+}
+
+@MappedEntity("comp_citizen")
+class Citizen {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+ private String name;
+
+ @OneToMany
+ private List