From 03571394d39c81fb91fde0abcad918f6d7be5ade Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 11:39:15 +0200 Subject: [PATCH 01/36] Add Capability BATCH_INSERT This change adds a new entry to `Capability`. BATCH_INSERT. It also modifies the API of `ConnectionCapablities`, adding the method `public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier)`. So that the API can be invoked with a `java.sql.Connection` or a `io.r2dbc.spi.Connection`. This change modifies the `DefaultConnectionCapabilities` implementation to specify that SQL Server does not support BATCH_INSERT. This business logic was already specified in `AbstractSqlRepositoryOperations::isSupportBatchIsert(PersistentEntity, Dialect)`. SQLite does not support batch inserts either. This change modifies `SqliteConnectionCapabilities` to specify that and adds a test which verifies that with the new Capability check, a `saveAll` method invocation is possible with SQLite. --- data-connection/build.gradle | 1 + .../connection/ConnectionCapabilities.java | 27 ++++- .../DefaultConnectionCapabilities.java | 8 +- .../example/CustomConnectionCapabilities.java | 5 +- .../DefaultConnectionCapabilitiesTest.java | 103 ++++++++++++++++++ .../DefaultJdbcRepositoryOperations.java | 3 +- data-r2dbc/build.gradle | 1 + .../DefaultR2dbcRepositoryOperations.java | 5 +- doc-examples/jdbc-sqlite/build.gradle | 1 + .../src/main/java/example/BookRepository.java | 4 +- .../src/main/java/example/Person.java | 11 ++ .../main/java/example/PersonRepository.java | 9 ++ .../example/SqliteConnectionCapabilities.java | 22 ++-- .../test/java/example/BookRepositoryTest.java | 22 +--- .../java/example/PersonRepositoryTest.java | 38 +++++++ .../SqliteConnectionCapabilitiesTest.java | 102 +++++++++++++++++ 16 files changed, 318 insertions(+), 44 deletions(-) create mode 100644 data-connection/src/test/java/io/micronaut/data/connection/DefaultConnectionCapabilitiesTest.java create mode 100644 doc-examples/jdbc-sqlite/src/main/java/example/Person.java create mode 100644 doc-examples/jdbc-sqlite/src/main/java/example/PersonRepository.java create mode 100644 doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java create mode 100644 doc-examples/jdbc-sqlite/src/test/java/example/SqliteConnectionCapabilitiesTest.java 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..bcf8e22ca20 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,8 +20,10 @@ 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}. @@ -48,7 +50,11 @@ enum Capability { /** * Whether the connection supports invoking {@link Connection#setReadOnly(boolean)}. */ - READ_ONLY + READ_ONLY, + /** + * Whether the connection supports JDBC batch inserts. + */ + BATCH_INSERT } /** @@ -59,6 +65,15 @@ enum Capability { */ ConnectionCapabilities INSTANCE = loadInstance(); + /** + * Determines whether the given database supports the requested capability. + * + * @param capability The capability to evaluate + * @param databaseProductNameSupplier supplier of the database product name + * @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 +81,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..5d2da6d727f 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,7 @@ 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 +25,11 @@ */ @Internal final class DefaultConnectionCapabilities implements ConnectionCapabilities { + private static final String MICROSOFT_SQL_SERVER = "Microsoft SQL Server"; @Override - public boolean supports(ConnectionCapabilities.Capability capability, Connection connection) { - return true; + public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) { + String dbProductName = databaseProductNameSupplier.get(); + return capability != Capability.BATCH_INSERT || !dbProductName.equalsIgnoreCase(MICROSOFT_SQL_SERVER); } } 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..2b0f7a24931 --- /dev/null +++ b/data-connection/src/test/java/io/micronaut/data/connection/DefaultConnectionCapabilitiesTest.java @@ -0,0 +1,103 @@ +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))); + } + + @DisabledInNativeImage + @ParameterizedTest + @CsvSource({ + "MySQL, true", + "MariaDB, true", + "PostgreSQL, true", + "H2, true", + "SQLite, true", + "Oracle, true", + "'Microsoft SQL Server', false" + }) + void supportsBatchInsertForDifferentDatabaseProducts(String databaseProductName, boolean expected) { + assertEquals(expected, capabilities.supports(ConnectionCapabilities.Capability.BATCH_INSERT, connection(databaseProductName))); + } + + @DisabledInNativeImage + @Test + void supportsBatchInsertWhenDatabaseMetadataCannotBeRead() { + assertTrue(capabilities.supports(ConnectionCapabilities.Capability.BATCH_INSERT, connectionThrowingMetadataException())); + } + + 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 connectionThrowingMetadataException() { + DatabaseMetaData metaData = (DatabaseMetaData) Proxy.newProxyInstance( + DatabaseMetaData.class.getClassLoader(), + new Class[]{DatabaseMetaData.class}, + (proxy, method, args) -> { + if ("getDatabaseProductName".equals(method.getName())) { + throw new SQLException("metadata unavailable"); + } + if ("unwrap".equals(method.getName())) { + return null; + } + if ("isWrapperFor".equals(method.getName())) { + return false; + } + 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-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index 9c42c65d9ea..be941234113 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -21,6 +21,7 @@ import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.data.connection.ConnectionCapabilities; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import io.micronaut.core.beans.BeanProperty; @@ -853,7 +854,7 @@ public Iterable persistAll(@NonNull InsertBatchOperation operation) { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); - if (!isSupportsBatchInsert(persistentEntity, storedQuery)) { + if (!ConnectionCapabilities.INSTANCE.supports(ConnectionCapabilities.Capability.BATCH_INSERT, ctx.connection) || !isSupportsBatchInsert(persistentEntity, storedQuery)) { return operation.split().stream() .map(persistOp -> { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, persistentEntity, persistOp.getEntity(), true); diff --git a/data-r2dbc/build.gradle b/data-r2dbc/build.gradle index e777426c0eb..3c5e536baf7 100644 --- a/data-r2dbc/build.gradle +++ b/data-r2dbc/build.gradle @@ -27,6 +27,7 @@ dependencies { testImplementation projects.micronautDataTck testImplementation mnTest.micronaut.test.spock + testImplementation(mnTest.junit.jupiter.params) testImplementation mnR2dbc.r2dbc.pool testImplementation mnRxjava2.micronaut.rxjava2 diff --git a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java index eae61e33c27..c93140b7d6c 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java @@ -21,6 +21,7 @@ import io.micronaut.context.annotation.Parameter; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.data.connection.ConnectionCapabilities; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; @@ -125,6 +126,7 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; @@ -918,7 +920,8 @@ public Flux persistAll(@NonNull InsertBatchOperation operation) { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); final R2dbcOperationContext ctx = createContext(operation, status, storedQuery); - if (!isSupportsBatchInsert(persistentEntity, storedQuery)) { + if (!ConnectionCapabilities.INSTANCE.supports(ConnectionCapabilities.Capability.BATCH_INSERT, () -> ctx.connection.getMetadata().getDatabaseProductName()) || + !isSupportsBatchInsert(persistentEntity, storedQuery)) { return concatMono( operation.split().stream() .map(persistOp -> { diff --git a/doc-examples/jdbc-sqlite/build.gradle b/doc-examples/jdbc-sqlite/build.gradle index 5cceee1231f..4c47b66d8a0 100644 --- a/doc-examples/jdbc-sqlite/build.gradle +++ b/doc-examples/jdbc-sqlite/build.gradle @@ -14,4 +14,5 @@ dependencies { implementation(mnSql.micronaut.jdbc.hikari) runtimeOnly(mnLogging.logback.classic) testRuntimeOnly(mnSql.sqlite.jdbc) + 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..694cf6a16b8 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.ANSI) 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..28bf2505d33 --- /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.ANSI) +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 index 10b706f291f..031353b9a29 100644 --- a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java +++ b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java @@ -18,32 +18,32 @@ import io.micronaut.data.connection.ConnectionCapabilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import java.sql.Connection; -import java.sql.SQLException; +import java.util.function.Supplier; /** * {@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"; + public static final String SQLITE = "SQLite"; + private final static String MICROSOFT_SQL_SERVER = "Microsoft SQL Server"; /** * 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. + * SQLite generated keys are also not reliable for JDBC batch inserts, so + * {@link ConnectionCapabilities.Capability#BATCH_INSERT} is reported as unsupported. * 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); - } + public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) { + String name = databaseProductNameSupplier.get(); + if (name.equalsIgnoreCase(SQLITE) && (capability == Capability.READ_ONLY || capability == Capability.BATCH_INSERT)) { + return false; + } else if (name.equalsIgnoreCase(MICROSOFT_SQL_SERVER) && capability == Capability.BATCH_INSERT) { + return false; } return true; } 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..a5d4a597d9f 100644 --- a/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java +++ b/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java @@ -16,29 +16,11 @@ @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 = "ANSI") +@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..5c844279551 --- /dev/null +++ b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java @@ -0,0 +1,38 @@ +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 java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@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") +@Property(name = "datasources.default.dialect", value = "ANSI") +@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/doc-examples/jdbc-sqlite/src/test/java/example/SqliteConnectionCapabilitiesTest.java b/doc-examples/jdbc-sqlite/src/test/java/example/SqliteConnectionCapabilitiesTest.java new file mode 100644 index 00000000000..efaa3afdecc --- /dev/null +++ b/doc-examples/jdbc-sqlite/src/test/java/example/SqliteConnectionCapabilitiesTest.java @@ -0,0 +1,102 @@ +package example; + +import io.micronaut.data.connection.ConnectionCapabilities; +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 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 SqliteConnectionCapabilitiesTest { + + private final ConnectionCapabilities capabilities = new SqliteConnectionCapabilities(); + + @DisabledInNativeImage + @ParameterizedTest + @CsvSource({ + "MySQL, true", + "MariaDB, true", + "PostgreSQL, true", + "H2, true", + "SQLite, false", + "Oracle, true", + "'Microsoft SQL Server', true" + }) + void supportsReadOnlyForDifferentDatabaseProducts(String databaseProductName, boolean expected) { + assertEquals(expected, capabilities.supports(ConnectionCapabilities.Capability.READ_ONLY, connection(databaseProductName))); + } + + @DisabledInNativeImage + @ParameterizedTest + @CsvSource({ + "MySQL, true", + "MariaDB, true", + "PostgreSQL, true", + "H2, true", + "SQLite, false", + "Oracle, true", + "'Microsoft SQL Server', false" + }) + void supportsBatchInsertForDifferentDatabaseProducts(String databaseProductName, boolean expected) { + assertEquals(expected, capabilities.supports(ConnectionCapabilities.Capability.BATCH_INSERT, connection(databaseProductName))); + } + + @DisabledInNativeImage + @Test + void supportsBatchInsertWhenDatabaseMetadataCannotBeRead() { + assertTrue(capabilities.supports(ConnectionCapabilities.Capability.BATCH_INSERT, connectionThrowingMetadataException())); + } + + 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 connectionThrowingMetadataException() { + DatabaseMetaData metaData = (DatabaseMetaData) Proxy.newProxyInstance( + DatabaseMetaData.class.getClassLoader(), + new Class[]{DatabaseMetaData.class}, + (proxy, method, args) -> { + if ("getDatabaseProductName".equals(method.getName())) { + throw new SQLException("metadata unavailable"); + } + if ("unwrap".equals(method.getName())) { + return null; + } + if ("isWrapperFor".equals(method.getName())) { + return false; + } + 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()); + } + ); + } +} From 3ccd33b42d1677807267bb18e6922d0a7a669e24 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 14:30:51 +0200 Subject: [PATCH 02/36] Update data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java | 1 - 1 file changed, 1 deletion(-) diff --git a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java index c93140b7d6c..e11ff6a59dd 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java @@ -126,7 +126,6 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; From 2b2091de5ef7af25b67b5dd2be07b41b8d6d65e9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 14:32:49 +0200 Subject: [PATCH 03/36] Update doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/test/java/example/PersonRepositoryTest.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java index 5c844279551..0bf9065d5ea 100644 --- a/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java +++ b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java @@ -1,19 +1,10 @@ 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 java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @Property(name = "datasources.default.url", value = "jdbc:sqlite:file:mydb?mode=memory&cache=shared") From 902359810104b9a2ce6a82f4d681e7bbfea51750 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:40:39 +0000 Subject: [PATCH 04/36] Remove unused Logger/LoggerFactory imports and make SQLITE constant private Agent-Logs-Url: https://github.com/micronaut-projects/micronaut-data/sessions/00ad2232-317f-4181-bb07-7c1afd70e3f5 Co-authored-by: sdelamo <864788+sdelamo@users.noreply.github.com> --- .../main/java/example/SqliteConnectionCapabilities.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java index 031353b9a29..1f8bf63cf8a 100644 --- a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java +++ b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java @@ -16,17 +16,14 @@ package example; import io.micronaut.data.connection.ConnectionCapabilities; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.function.Supplier; /** * {@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 SQLITE = "SQLite"; - private final static String MICROSOFT_SQL_SERVER = "Microsoft SQL Server"; + private static final String SQLITE = "SQLite"; + private static final String MICROSOFT_SQL_SERVER = "Microsoft SQL Server"; /** * Connection capabilities implementation for the SQLite JDBC example. From cc695af164e1aedaa3c5fa94b91da6dfab0e5e19 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 15:14:10 +0200 Subject: [PATCH 05/36] remove unused imports --- .../src/test/java/example/BookRepositoryTest.java | 8 -------- 1 file changed, 8 deletions(-) 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 a5d4a597d9f..0aea0747a1f 100644 --- a/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java +++ b/doc-examples/jdbc-sqlite/src/test/java/example/BookRepositoryTest.java @@ -1,17 +1,9 @@ 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") From b0503d668b0c48fce37030a69ea4e5e67b949081 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 16:02:05 +0200 Subject: [PATCH 06/36] Update doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java index 0bf9065d5ea..2efa9460e18 100644 --- a/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java +++ b/doc-examples/jdbc-sqlite/src/test/java/example/PersonRepositoryTest.java @@ -7,7 +7,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; -@Property(name = "datasources.default.url", value = "jdbc:sqlite:file:mydb?mode=memory&cache=shared") +@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 = "ANSI") @Property(name = "datasources.default.schema-generate", value = "CREATE_DROP") From 6e973cbb8000eeab9f4a0acda6d4fe9b27702b50 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 16:02:46 +0200 Subject: [PATCH 07/36] Update doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/example/SqliteConnectionCapabilities.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java index 1f8bf63cf8a..f694c206722 100644 --- a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java +++ b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java @@ -32,7 +32,8 @@ public final class SqliteConnectionCapabilities implements ConnectionCapabilitie * {@link ConnectionCapabilities.Capability#READ_ONLY} is reported as unsupported for SQLite URLs. * SQLite generated keys are also not reliable for JDBC batch inserts, so * {@link ConnectionCapabilities.Capability#BATCH_INSERT} is reported as unsupported. - * Other capabilities are treated as supported. + * In addition, {@link ConnectionCapabilities.Capability#BATCH_INSERT} is reported as unsupported + * for Microsoft SQL Server. All other capabilities are treated as supported. */ @Override public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) { From e8290a79dbb2ed4e6008efc71ae06bf1a86caf0b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 16:03:31 +0200 Subject: [PATCH 08/36] Update data-connection/src/main/java/io/micronaut/data/connection/DefaultConnectionCapabilities.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../data/connection/DefaultConnectionCapabilities.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 5d2da6d727f..daaf134b922 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 @@ -29,7 +29,11 @@ final class DefaultConnectionCapabilities implements ConnectionCapabilities { @Override public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) { - String dbProductName = databaseProductNameSupplier.get(); - return capability != Capability.BATCH_INSERT || !dbProductName.equalsIgnoreCase(MICROSOFT_SQL_SERVER); + try { + String dbProductName = databaseProductNameSupplier.get(); + return capability != Capability.BATCH_INSERT || !dbProductName.equalsIgnoreCase(MICROSOFT_SQL_SERVER); + } catch (RuntimeException e) { + return true; + } } } From d446860cf4f209038d0013ba4dbadb15b7310c20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:12:09 +0000 Subject: [PATCH 09/36] Update ConnectionCapabilities Javadoc to document supplier-based contract and R2DBC support Agent-Logs-Url: https://github.com/micronaut-projects/micronaut-data/sessions/a230870b-d7db-4c4a-aa5a-b953944a12f2 Co-authored-by: sdelamo <864788+sdelamo@users.noreply.github.com> --- .../connection/ConnectionCapabilities.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 bcf8e22ca20..43440187590 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 @@ -26,7 +26,17 @@ 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 @@ -66,10 +76,20 @@ enum Capability { ConnectionCapabilities INSTANCE = loadInstance(); /** - * Determines whether the given database supports the requested capability. + * 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 + * @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); From 7cb5a4372315ab92de3a4cb6f4ad8d89a7d0656a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 24 Apr 2026 16:36:30 +0200 Subject: [PATCH 10/36] sqlite --- .../DefaultJdbcRepositoryOperations.java | 5 +- data-model/build.gradle | 1 + .../data/model/query/builder/sql/Dialect.java | 29 +- .../query/builder/sql/SqlQueryBuilder.java | 10 +- .../model/schema/sql/SqlColumnMapping.java | 5 + .../model/query/builder/sql/DialectTest.java | 129 + .../DefaultR2dbcRepositoryOperations.java | 3 +- .../example/SqliteConnectionCapabilities.java | 12 +- settings.gradle | 2 + test-suite-data-jdbc-sqlite/build.gradle.kts | 56 + .../jdbc/AbstractJdbcMultitenancyTest.java | 70 + .../jdbc/AbstractJdbcTransactionTest.java | 50 + .../data/jdbc/AbstractManualSchemaTest.java | 152 + .../CallableStatementTupleMapperTest.java | 61 + .../data/jdbc/SchemaCreateDropTest.java | 39 + .../AbstractSQLiteRepositoryBehaviorTest.java | 291 ++ .../data/jdbc/sqlite/CascadeEntity.java | 17 + .../jdbc/sqlite/CascadeEntityRepository.java | 11 + .../data/jdbc/sqlite/CascadeSubEntityA.java | 16 + .../data/jdbc/sqlite/CascadeSubEntityB.java | 16 + .../data/jdbc/sqlite/ChallengeRepository.java | 22 + .../jdbc/sqlite/ChallengeRepositoryTest.java | 20 + .../jdbc/sqlite/EscapeIdentifiersTest.java | 94 + .../jdbc/sqlite/JpaTransientPropertyTest.java | 23 + .../data/jdbc/sqlite/LocalDateTimeTest.java | 57 + .../jdbc/sqlite/MultipleDataSourceTest.java | 225 ++ .../data/jdbc/sqlite/NoIdEntity.java | 39 + .../jdbc/sqlite/OrganizationRepository.java | 9 + .../sqlite/SQLiteAccountRecordRepository.java | 25 + .../jdbc/sqlite/SQLiteAccountRepository.java | 27 + .../sqlite/SQLiteArraysEntityRepository.java | 24 + .../data/jdbc/sqlite/SQLiteArraysTest.java | 12 + .../sqlite/SQLiteAsyncBookRepository.java | 25 + .../sqlite/SQLiteAsyncPersonRepository.java | 24 + .../sqlite/SQLiteAsyncRepositoryTest.java | 40 + .../jdbc/sqlite/SQLiteAuthorRepository.java | 54 + .../sqlite/SQLiteBasicTypesRepository.java | 24 + .../jdbc/sqlite/SQLiteBasicTypesTest.java | 91 + .../jdbc/sqlite/SQLiteBookDtoRepository.java | 26 + .../sqlite/SQLiteBookEntityRepository.java | 9 + .../jdbc/sqlite/SQLiteBookPageRepository.java | 23 + .../jdbc/sqlite/SQLiteBookRepository.java | 44 + .../data/jdbc/sqlite/SQLiteBookService.java | 55 + .../data/jdbc/sqlite/SQLiteCarRepository.java | 24 + .../data/jdbc/sqlite/SQLiteCascadeTest.java | 33 + .../jdbc/sqlite/SQLiteCityRepository.java | 24 + .../jdbc/sqlite/SQLiteCompanyRepository.java | 25 + .../sqlite/SQLiteCompositePrimaryKeyTest.java | 67 + .../SQLiteCountryRegionCityRepository.java | 24 + .../jdbc/sqlite/SQLiteCountryRepository.java | 24 + .../sqlite/SQLiteCursoredPaginationTest.java | 356 +++ .../data/jdbc/sqlite/SQLiteCustomIdTest.java | 73 + .../data/jdbc/sqlite/SQLiteDBProperties.java | 44 + ...PropertiesTestPropertyProviderFactory.java | 44 + .../sqlite/SQLiteDisabledDataSourceTest.java | 33 + ...teDiscriminatorMultitenancyRecordTest.java | 312 +++ .../SQLiteDiscriminatorMultitenancyTest.java | 117 + .../SQLiteDomainEventsReactiveRepository.java | 9 + .../sqlite/SQLiteDomainEventsRepository.java | 9 + .../SQLiteDoubleImplement1Repository.java | 25 + .../SQLiteDoubleImplement2Repository.java | 24 + .../SQLiteDoubleImplement3Repository.java | 24 + .../data/jdbc/sqlite/SQLiteDtoTest.java | 185 ++ .../jdbc/sqlite/SQLiteEagerContextTest.java | 81 + .../data/jdbc/sqlite/SQLiteEmbedded2Test.java | 144 + .../sqlite/SQLiteEmbeddedCascadeTest.java | 189 ++ .../jdbc/sqlite/SQLiteEmbeddedIdTest.java | 444 +++ .../data/jdbc/sqlite/SQLiteEmbeddedTest.java | 74 + .../sqlite/SQLiteEnabledPersonRepository.java | 29 + .../SQLiteEntityWithIdClass2Repository.java | 24 + .../SQLiteEntityWithIdClassRepository.java | 24 + .../jdbc/sqlite/SQLiteEnumsMappingTest.java | 328 +++ .../data/jdbc/sqlite/SQLiteEventsTest.java | 119 + .../sqlite/SQLiteExampleEntityRepository.java | 9 + .../jdbc/sqlite/SQLiteFaceRepository.java | 24 + .../jdbc/sqlite/SQLiteFoodRepository.java | 24 + .../jdbc/sqlite/SQLiteGenreRepository.java | 12 + .../sqlite/SQLiteHouseEntityRepository.java | 9 + .../jdbc/sqlite/SQLiteIntervalRepository.java | 9 + .../data/jdbc/sqlite/SQLiteJSONTest.java | 317 +++ .../jdbc/sqlite/SQLiteJakartaDataTest.java | 38 + .../data/jdbc/sqlite/SQLiteJoinFetchTest.java | 142 + .../sqlite/SQLiteJsonEntityRepository.java | 15 + .../jdbc/sqlite/SQLiteManualSchemaTest.java | 123 + .../jdbc/sqlite/SQLiteMappedEntityTest.java | 43 + .../jdbc/sqlite/SQLiteMealRepository.java | 28 + .../jdbc/sqlite/SQLiteMultitenancyTest.java | 95 + .../sqlite/SQLiteNoIdEntityRepository.java | 9 + .../SQLiteNoIdEntityRepositoryTest.java | 35 + .../SQLiteNoTxOpsRepositoryBehaviorTest.java | 30 + .../sqlite/SQLiteNoTxOpsRepositoryTest.java | 30 + .../jdbc/sqlite/SQLiteNoseRepository.java | 24 + .../sqlite/SQLiteNullableConstructorTest.java | 46 + .../data/jdbc/sqlite/SQLiteOrderTest.java | 55 + .../sqlite/SQLiteOrganizationRepository.java | 8 + .../jdbc/sqlite/SQLitePageRepository.java | 23 + .../jdbc/sqlite/SQLitePaginationTest.java | 195 ++ .../jdbc/sqlite/SQLitePatientRepository.java | 19 + .../jdbc/sqlite/SQLitePersonRepository.java | 103 + .../jdbc/sqlite/SQLitePlantRepository.java | 24 + .../sqlite/SQLiteProductDtoRepository.java | 24 + .../jdbc/sqlite/SQLiteProjectRepository.java | 27 + .../data/jdbc/sqlite/SQLiteQueryTest.java | 115 + .../sqlite/SQLiteReactiveBookRepository.java | 24 + .../SQLiteReactivePersonRepository.java | 24 + .../sqlite/SQLiteReactiveRepositoryTest.java | 46 + .../jdbc/sqlite/SQLiteRegionRepository.java | 24 + .../sqlite/SQLiteRepositoryBehaviorTest.java | 19 + .../sqlite/SQLiteRepositoryScopeTest.java | 124 + .../jdbc/sqlite/SQLiteRepositoryTest.java | 222 ++ .../sqlite/SQLiteRestaurantRepository.java | 29 + .../jdbc/sqlite/SQLiteRoleRepository.java | 9 + .../jdbc/sqlite/SQLiteSaleItemRepository.java | 24 + .../jdbc/sqlite/SQLiteSaleRepository.java | 24 + .../sqlite/SQLiteSchemaCreateDropTest.java | 56 + .../sqlite/SQLiteSchemaGenerationTest.java | 22 + .../sqlite/SQLiteShelfBookRepository.java | 23 + .../jdbc/sqlite/SQLiteShelfRepository.java | 23 + .../sqlite/SQLiteStreamingStatementTest.java | 53 + .../SQLiteStudentReactiveRepository.java | 24 + .../jdbc/sqlite/SQLiteStudentRepository.java | 24 + .../sqlite/SQLiteTableRatingsRepository.java | 31 + .../SQLiteTaskGenericEntity2Repository.java | 26 + .../SQLiteTaskGenericEntityRepository.java | 27 + .../jdbc/sqlite/SQLiteTaskRepository.java | 25 + .../sqlite/SQLiteTestingPropertyProvider.java | 72 + .../SQLiteTimezoneBasicTypesRepository.java | 24 + .../jdbc/sqlite/SQLiteTrainRepository.java | 24 + .../jdbc/sqlite/SQLiteTrainsRepository.java | 24 + .../jdbc/sqlite/SQLiteTransactionsTest.java | 640 +++++ .../SQLiteUnidirectionalToManyJoinTest.java | 72 + .../jdbc/sqlite/SQLiteUserRepository.java | 9 + .../jdbc/sqlite/SQLiteUserRoleRepository.java | 9 + .../jdbc/sqlite/SQLiteValidationTest.java | 92 + .../sqlite/SQLiteWhereAnnotationTest.java | 61 + .../data/jdbc/sqlite/ShipmentRepository.java | 40 + .../sqlite/SqliteConnectionCapabilities.java | 42 + .../sqlite/SqliteGeneratedValueEntity.java | 46 + .../SqliteGeneratedValueRepository.java | 24 + .../jdbc/sqlite/SqliteGeneratedValueTest.java | 72 + .../sqlite/SqliteSchemaValidationTest.java | 70 + .../data/jdbc/sqlite/SqliteUUIDTest.java | 82 + .../data/jdbc/sqlite/SqliteUuidEntity.java | 75 + .../jdbc/sqlite/SqliteUuidRepository.java | 34 + .../data/jdbc/sqlite/TableRatings.java | 48 + .../AssignedUuidCascadePersistTest.java | 194 ++ .../AssignedUuidManyToManyPersistTest.java | 189 ++ .../AutoPopulateEmbeddedTest.java | 245 ++ .../jdbc/sqlite/autopopulate/InnerFields.java | 20 + .../sqlite/autopopulate/package-info.java | 4 + .../jdbc/sqlite/composite/CompositeTest.java | 761 +++++ .../EmbeddedAssociationJoinTest.java | 563 ++++ .../CustomEmbeddedNameMappingTest.java | 242 ++ .../groovy_static_repo/GTestEntity.java | 33 + .../groovy_static_repo/MyCrudRepository.java | 11 + ...iteGroovyStaticExtendedRepositoryTest.java | 27 + .../TestEntityRepository.java | 10 + .../data/jdbc/sqlite/identity/MyBook.java | 30 + .../data/jdbc/sqlite/identity/MyBookDto.java | 10 + .../data/jdbc/sqlite/identity/MyBookDto2.java | 10 + .../sqlite/identity/MyBookRepository.java | 32 + .../identity/SameIdentityRepositoryTest.java | 44 + .../jdbc/sqlite/jakarta_data/entity/Box.java | 30 + .../sqlite/jakarta_data/entity/Boxes.java | 13 + .../jakarta_data/entity/Coordinate.java | 32 + .../jakarta_data/entity/EntityTests.java | 2455 +++++++++++++++++ .../entity/MultipleEntityRepo.java | 44 + .../jakarta_data/persistence/Catalog.java | 122 + .../persistence/CatalogProduct.java | 108 + .../persistence/PersistenceEntityTests.java | 399 +++ .../read/only/AsciiCharacter.java | 63 + .../read/only/AsciiCharacters.java | 100 + .../read/only/AsciiCharactersPopulator.java | 38 + .../read/only/CustomRepository.java | 29 + .../jakarta_data/read/only/IdOperations.java | 16 + .../jakarta_data/read/only/NaturalNumber.java | 79 + .../read/only/NaturalNumbers.java | 68 + .../read/only/NaturalNumbersPopulator.java | 66 + .../jakarta_data/read/only/Populator.java | 60 + .../read/only/PositiveIntegers.java | 67 + .../read/only/ReadOnlyRepository.java | 33 + .../jakarta_data/read/only/_AsciiChar.java | 31 + .../read/only/_AsciiCharacter.java | 51 + .../sqlite/jakarta_data/simple/Address.java | 19 + .../sqlite/jakarta_data/simple/Customer.java | 24 + .../jakarta_data/simple/CustomerDaoTest.java | 65 + .../CustomerOnlyDeleteQueryRepository.java | 29 + .../simple/CustomerRepository.java | 53 + .../simple/CustomerRepository2.java | 32 + .../jakarta_data/utilities/DatabaseType.java | 30 + .../jakarta_data/utilities/TestProperty.java | 157 ++ .../utilities/TestPropertyHandler.java | 47 + .../data/jdbc/sqlite/joinissue/Author.java | 18 + .../sqlite/joinissue/AuthorRepository.java | 39 + .../jdbc/sqlite/joinissue/AuthorTest.java | 67 + .../data/jdbc/sqlite/joinissue/Book.java | 24 + .../data/jdbc/sqlite/joinissue/Director.java | 62 + .../sqlite/joinissue/DirectorRepository.java | 23 + .../jdbc/sqlite/joinissue/DirectorTest.java | 47 + .../data/jdbc/sqlite/joinissue/Movie.java | 60 + .../many2many/ManyToManyJoinTableTest.java | 517 ++++ .../many2many/MultiManyToManyJoinTest.java | 151 + .../many2one/MultiManyToOneJoinTest.java | 423 +++ .../jdbc/sqlite/multitenancy/TenancyBook.java | 19 + .../multitenancy/TenancyBookController.java | 26 + .../TenancyBookControllerTest.java | 73 + .../multitenancy/TenancyBookRepository.java | 17 + .../sqlite/multitenancy/TenancyPerson.java | 17 + .../multitenancy/TenancyPersonRepository.java | 39 + .../multitenancy/TenancyPersonService.java | 55 + .../TenancyPersonServiceTest.java | 124 + .../one2many/DoubleOneToManyJoinTest.java | 230 ++ .../one2many/MultiOneToManyJoinTest.java | 256 ++ .../one2many/MultipleOneToManyTest.java | 257 ++ .../one2many/OneToManyChildrenTest.java | 151 + .../one2many/OneToManyHierarchicalTest.java | 190 ++ .../one2many/OneToManyJoinColumnTest.java | 72 + .../sqlite/one2one/MultiOneToOneJoinTest.java | 155 ++ .../jdbc/sqlite/one2one/OneToOneTest.java | 186 ++ .../sqlite/one2one/select/MyEmbedded.java | 33 + .../jdbc/sqlite/one2one/select/MyOrder.java | 37 + .../one2one/select/MyOrderRepository.java | 24 + .../select/OneToOneProjectionTest.java | 28 + .../data/jdbc/sqlite/remap/Course.java | 24 + .../jdbc/sqlite/remap/CourseRepository.java | 16 + .../sqlite/remap/ManyToManyAttributeTest.java | 53 + .../data/jdbc/sqlite/remap/Student.java | 26 + .../data/jdbc/sqlite/remap/StudentId.java | 21 + .../jdbc/sqlite/remap/StudentRepository.java | 9 + ...aut.data.connection.ConnectionCapabilities | 1 + ...t.test.support.TestPropertyProviderFactory | 1 + .../src/test/resources/logback.xml | 15 + 232 files changed, 19373 insertions(+), 15 deletions(-) create mode 100644 data-model/src/test/java/io/micronaut/data/model/query/builder/sql/DialectTest.java create mode 100644 test-suite-data-jdbc-sqlite/build.gradle.kts create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcMultitenancyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractJdbcTransactionTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/AbstractManualSchemaTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/CallableStatementTupleMapperTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/SchemaCreateDropTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/AbstractSQLiteRepositoryBehaviorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntity.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityA.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/CascadeSubEntityB.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ChallengeRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/EscapeIdentifiersTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/JpaTransientPropertyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/LocalDateTimeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/MultipleDataSourceTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/NoIdEntity.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/OrganizationRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRecordRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAccountRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteArraysTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncPersonRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAsyncRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteAuthorRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBasicTypesTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookDtoRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookPageRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteBookService.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCarRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCascadeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompanyRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCompositePrimaryKeyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRegionCityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCountryRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCursoredPaginationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteCustomIdTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBProperties.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDBPropertiesTestPropertyProviderFactory.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDisabledDataSourceTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyRecordTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDiscriminatorMultitenancyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsReactiveRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDomainEventsRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement1Repository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement2Repository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDoubleImplement3Repository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteDtoTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEagerContextTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbedded2Test.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedCascadeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedIdTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEmbeddedTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnabledPersonRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClass2Repository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEntityWithIdClassRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEnumsMappingTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteEventsTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteExampleEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFaceRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteFoodRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteGenreRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteHouseEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteIntervalRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJSONTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJakartaDataTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJoinFetchTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteJsonEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteManualSchemaTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMappedEntityTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMealRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteMultitenancyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoIdEntityRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryBehaviorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoTxOpsRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNoseRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteNullableConstructorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrderTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteOrganizationRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePageRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePaginationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePatientRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePersonRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLitePlantRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProductDtoRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteProjectRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteQueryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactivePersonRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteReactiveRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRegionRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryBehaviorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryScopeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRestaurantRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteRoleRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleItemRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSaleRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaCreateDropTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteSchemaGenerationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteShelfRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStreamingStatementTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentReactiveRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteStudentRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTableRatingsRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntity2Repository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskGenericEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTaskRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTestingPropertyProvider.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTimezoneBasicTypesRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTrainsRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteTransactionsTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUnidirectionalToManyJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteUserRoleRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteValidationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SQLiteWhereAnnotationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/ShipmentRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteConnectionCapabilities.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueEntity.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteGeneratedValueTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteSchemaValidationTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUUIDTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidEntity.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/SqliteUuidRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/TableRatings.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidCascadePersistTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/assignedid/AssignedUuidManyToManyPersistTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/AutoPopulateEmbeddedTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/InnerFields.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/autopopulate/package-info.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/composite/CompositeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/embeddedAssociation/EmbeddedAssociationJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/embeddedNameMapping/CustomEmbeddedNameMappingTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/groovy_static_repo/GTestEntity.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/groovy_static_repo/MyCrudRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/groovy_static_repo/SQLiteGroovyStaticExtendedRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/groovy_static_repo/TestEntityRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/identity/MyBook.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/identity/MyBookDto.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/identity/MyBookDto2.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/identity/MyBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/identity/SameIdentityRepositoryTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/entity/Box.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/entity/Boxes.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/entity/Coordinate.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/entity/EntityTests.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/entity/MultipleEntityRepo.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/persistence/Catalog.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/persistence/CatalogProduct.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/persistence/PersistenceEntityTests.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/AsciiCharacter.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/AsciiCharacters.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/AsciiCharactersPopulator.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/CustomRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/IdOperations.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/NaturalNumber.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/NaturalNumbers.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/NaturalNumbersPopulator.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/Populator.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/PositiveIntegers.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/ReadOnlyRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/_AsciiChar.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/read/only/_AsciiCharacter.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/Address.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/Customer.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/CustomerDaoTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/CustomerOnlyDeleteQueryRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/CustomerRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/simple/CustomerRepository2.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/utilities/DatabaseType.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/utilities/TestProperty.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/jakarta_data/utilities/TestPropertyHandler.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/Author.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/AuthorRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/AuthorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/Book.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/Director.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/DirectorRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/DirectorTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/joinissue/Movie.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/many2many/ManyToManyJoinTableTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/many2many/MultiManyToManyJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/many2one/MultiManyToOneJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyBook.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyBookController.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyBookControllerTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyBookRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyPerson.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyPersonRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyPersonService.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/multitenancy/TenancyPersonServiceTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/DoubleOneToManyJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/MultiOneToManyJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/MultipleOneToManyTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/OneToManyChildrenTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/OneToManyHierarchicalTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2many/OneToManyJoinColumnTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/MultiOneToOneJoinTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/OneToOneTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/select/MyEmbedded.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/select/MyOrder.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/select/MyOrderRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/one2one/select/OneToOneProjectionTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/Course.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/CourseRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/ManyToManyAttributeTest.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/Student.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/StudentId.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/java/io/micronaut/data/jdbc/sqlite/remap/StudentRepository.java create mode 100644 test-suite-data-jdbc-sqlite/src/test/resources/META-INF/services/io.micronaut.data.connection.ConnectionCapabilities create mode 100644 test-suite-data-jdbc-sqlite/src/test/resources/META-INF/services/io.micronaut.test.support.TestPropertyProviderFactory create mode 100644 test-suite-data-jdbc-sqlite/src/test/resources/logback.xml diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index be941234113..115ad671c1a 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -854,7 +854,7 @@ public Iterable persistAll(@NonNull InsertBatchOperation operation) { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); JdbcOperationContext ctx = createContext(operation, connection, storedQuery); - if (!ConnectionCapabilities.INSTANCE.supports(ConnectionCapabilities.Capability.BATCH_INSERT, ctx.connection) || !isSupportsBatchInsert(persistentEntity, storedQuery)) { + if (!ctx.dialect.allowBatch() || !isSupportsBatchInsert(persistentEntity, storedQuery)) { return operation.split().stream() .map(persistOp -> { JdbcEntityOperations op = new JdbcEntityOperations<>(ctx, storedQuery, persistentEntity, persistOp.getEntity(), true); @@ -1161,6 +1161,9 @@ private DataAccessException sqlExceptionToDataAccessException(SQLException sqlEx @Override public boolean isSupportsBatchInsert(JdbcOperationContext jdbcOperationContext, RuntimePersistentEntity persistentEntity) { + if (!jdbcOperationContext.dialect.allowBatch()) { + return false; + } return isSupportsBatchInsert(persistentEntity, jdbcOperationContext.dialect); } diff --git a/data-model/build.gradle b/data-model/build.gradle index bf3d178f18e..66585c3e755 100644 --- a/data-model/build.gradle +++ b/data-model/build.gradle @@ -24,4 +24,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..009a563dbe1 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,8 @@ 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 +99,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 +112,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 +121,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 +130,7 @@ public enum Dialect { this.supportsUpdateReturning = supportsUpdateReturning; this.supportsInsertReturning = supportsInsertReturning; this.supportsDeleteReturning = supportsDeleteReturning; + this.supportsReadOnly = supportsReadOnly; } /** @@ -210,4 +219,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 93f2bc41747..f78b890648b 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 @@ -1229,9 +1229,13 @@ 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()) { + 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..b76873b75bb 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 @@ -250,6 +250,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-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java index e11ff6a59dd..926e983b373 100644 --- a/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java +++ b/data-r2dbc/src/main/java/io/micronaut/data/r2dbc/operations/DefaultR2dbcRepositoryOperations.java @@ -919,8 +919,7 @@ public Flux persistAll(@NonNull InsertBatchOperation operation) { final SqlStoredQuery storedQuery = getSqlStoredQuery(operation.getStoredQuery()); final RuntimePersistentEntity persistentEntity = storedQuery.getPersistentEntity(); final R2dbcOperationContext ctx = createContext(operation, status, storedQuery); - if (!ConnectionCapabilities.INSTANCE.supports(ConnectionCapabilities.Capability.BATCH_INSERT, () -> ctx.connection.getMetadata().getDatabaseProductName()) || - !isSupportsBatchInsert(persistentEntity, storedQuery)) { + if (!ctx.dialect.allowBatch() || !isSupportsBatchInsert(persistentEntity, storedQuery)) { return concatMono( operation.split().stream() .map(persistOp -> { diff --git a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java index f694c206722..a9610b11bf2 100644 --- a/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java +++ b/doc-examples/jdbc-sqlite/src/main/java/example/SqliteConnectionCapabilities.java @@ -16,13 +16,16 @@ package example; import io.micronaut.data.connection.ConnectionCapabilities; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.function.Supplier; /** * {@link ConnectionCapabilities} implementation used by the SQLite JDBC example. */ public final class SqliteConnectionCapabilities implements ConnectionCapabilities { - private static final String SQLITE = "SQLite"; + private static final Logger LOG = LoggerFactory.getLogger(SqliteConnectionCapabilities.class); + public static final String SQLITE = "SQLite"; private static final String MICROSOFT_SQL_SERVER = "Microsoft SQL Server"; /** @@ -37,10 +40,11 @@ public final class SqliteConnectionCapabilities implements ConnectionCapabilitie */ @Override public boolean supports(ConnectionCapabilities.Capability capability, Supplier databaseProductNameSupplier) { - String name = databaseProductNameSupplier.get(); - if (name.equalsIgnoreCase(SQLITE) && (capability == Capability.READ_ONLY || capability == Capability.BATCH_INSERT)) { + String dbProductName = databaseProductNameSupplier.get(); + if (capability == Capability.BATCH_INSERT && dbProductName.equals(MICROSOFT_SQL_SERVER)) { return false; - } else if (name.equalsIgnoreCase(MICROSOFT_SQL_SERVER) && capability == Capability.BATCH_INSERT) { + } + if ((capability == Capability.BATCH_INSERT || capability == Capability.READ_ONLY) && dbProductName.equals(SQLITE)) { return false; } return true; diff --git a/settings.gradle b/settings.gradle index 817e9a640de..a328ff57af1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -143,6 +143,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/test-suite-data-jdbc-sqlite/build.gradle.kts b/test-suite-data-jdbc-sqlite/build.gradle.kts new file mode 100644 index 00000000000..cc4d6f0b23a --- /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.sqlite.jdbc) + + // CONNECTION POOL +// testRuntimeOnly(mnSql.micronaut.jdbc.hikari) + testRuntimeOnly(mnSql.micronaut.jdbc.tomcat) + + // MULTITENANCY + testImplementation(mnMultitenancy.micronaut.multitenancy) + + // REACTIVE + testImplementation(mnReactor.micronaut.reactor) + testImplementation(mnRxjava2.micronaut.rxjava2) + + // 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..f7164ac7ab7 --- /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 { + assert 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