diff --git a/README.md b/README.md index 15cbf916..70bfcfed 100644 --- a/README.md +++ b/README.md @@ -434,6 +434,14 @@ Before you use Liquibase, you have to add the following Maven dependency: Given that Liquibase does not offer an analogy to the `@FlywayTest` annotation, you may consider using the [refresh mode](#refreshing-the-database-during-tests) to refresh an embedded database during tests. +### Spring Boot SQL Init + +The library also includes optimized processing of Spring Boot's `spring.sql.init` scripts. If this optimization causes any issues, it can be disabled: + +```properties +zonky.test.database.spring.optimized-sql-init.enabled=false +``` + ## Database Providers The library can be combined with different database providers. diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java index 782f61ef..e18fe3a8 100644 --- a/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java @@ -18,6 +18,7 @@ import io.zonky.test.db.flyway.FlywayDatabaseExtension; import io.zonky.test.db.flyway.FlywayPropertiesPostProcessor; +import io.zonky.test.db.init.DataSourceScriptDatabaseExtension; import io.zonky.test.db.init.EmbeddedDatabaseInitializer; import io.zonky.test.db.init.ScriptDatabasePreparer; import io.zonky.test.db.liquibase.LiquibaseDatabaseExtension; @@ -289,6 +290,14 @@ public BeanPostProcessor liquibasePropertiesPostProcessor() { return new LiquibasePropertiesPostProcessor(); } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnClass(name = "org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer") + @ConditionalOnMissingBean(name = "dataSourceScriptDatabaseExtension") + public DataSourceScriptDatabaseExtension dataSourceScriptDatabaseExtension(Environment environment) { + return new DataSourceScriptDatabaseExtension(environment); + } + @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean(name = "embeddedDatabaseInitializer") diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabaseExtension.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabaseExtension.java new file mode 100644 index 00000000..beba71ca --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabaseExtension.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db.init; + +import io.zonky.test.db.context.DatabaseContext; +import io.zonky.test.db.util.AopProxyUtils; +import io.zonky.test.db.util.ReflectionUtils; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; + +public class DataSourceScriptDatabaseExtension implements BeanPostProcessor, Ordered { + + private final boolean enabled; + + public DataSourceScriptDatabaseExtension(Environment environment) { + this.enabled = environment.getProperty("zonky.test.database.spring.optimized-sql-init.enabled", boolean.class, true); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + if (enabled && bean instanceof DataSourceScriptDatabaseInitializer) { + DataSourceScriptDatabaseInitializer initializer = (DataSourceScriptDatabaseInitializer) bean; + DataSource dataSource = ReflectionUtils.getField(initializer, "dataSource"); + DatabaseContext context = AopProxyUtils.getDatabaseContext(dataSource); + + if (context != null) { + context.apply(new DataSourceScriptDatabasePreparer(initializer)); + return new SuppressedInitializerWrapper(initializer); + } + } + + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + return bean; + } + + public static class SuppressedInitializerWrapper { + + private final DataSourceScriptDatabaseInitializer initializer; + + public SuppressedInitializerWrapper(DataSourceScriptDatabaseInitializer initializer) { + this.initializer = initializer; + } + + @Override + public String toString() { + return "SuppressedInitializerWrapper{initializer=" + initializer.getClass().getName() + "}"; + } + } +} diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabasePreparer.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabasePreparer.java new file mode 100644 index 00000000..43d8b0de --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabasePreparer.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db.init; + +import com.cedarsoftware.util.DeepEquals; +import io.zonky.test.db.preparer.DatabasePreparer; +import io.zonky.test.db.util.ReflectionUtils; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; + +import org.springframework.util.ReflectionUtils.FieldFilter; + +import javax.sql.DataSource; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.springframework.util.ReflectionUtils.makeAccessible; + +public class DataSourceScriptDatabasePreparer implements DatabasePreparer { + + private static final Set EXCLUDED_FIELDS = new HashSet<>(Arrays.asList("dataSource", "resourceLoader")); + + private static final FieldFilter FIELD_FILTER = + field -> !Modifier.isStatic(field.getModifiers()) && !EXCLUDED_FIELDS.contains(field.getName()); + + private final DataSourceScriptDatabaseInitializer initializer; + private final ThreadLocalDataSource threadLocalDataSource; + + public DataSourceScriptDatabasePreparer(DataSourceScriptDatabaseInitializer initializer) { + this.initializer = initializer; + this.threadLocalDataSource = new ThreadLocalDataSource(); + ReflectionUtils.setField(initializer, "dataSource", threadLocalDataSource); + } + + @Override + public long estimatedDuration() { + return 10; + } + + @Override + public void prepare(DataSource dataSource) { + threadLocalDataSource.set(dataSource); + try { + initializer.initializeDatabase(); + } finally { + threadLocalDataSource.clear(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DataSourceScriptDatabasePreparer that = (DataSourceScriptDatabasePreparer) o; + if (initializer.getClass() != that.initializer.getClass()) return false; + AtomicBoolean equal = new AtomicBoolean(true); + org.springframework.util.ReflectionUtils.doWithFields(initializer.getClass(), + field -> { + if (!equal.get()) return; + makeAccessible(field); + if (!DeepEquals.deepEquals(field.get(initializer), field.get(that.initializer))) { + equal.set(false); + } + }, + FIELD_FILTER); + return equal.get(); + } + + @Override + public int hashCode() { + AtomicInteger result = new AtomicInteger(initializer.getClass().hashCode()); + org.springframework.util.ReflectionUtils.doWithFields(initializer.getClass(), + field -> { + makeAccessible(field); + result.set(31 * result.get() + DeepEquals.deepHashCode(field.get(initializer))); + }, + FIELD_FILTER); + return result.get(); + } +} diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ThreadLocalDataSource.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ThreadLocalDataSource.java new file mode 100644 index 00000000..449682bb --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ThreadLocalDataSource.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db.init; + +import io.zonky.test.db.provider.support.AbstractDelegatingDataSource; + +import javax.sql.DataSource; + +class ThreadLocalDataSource extends AbstractDelegatingDataSource { + + private final ThreadLocal current = new ThreadLocal<>(); + + void set(DataSource dataSource) { + current.set(dataSource); + } + + void clear() { + current.remove(); + } + + @Override + protected DataSource getDataSource() { + return current.get(); + } +} diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractDelegatingDataSource.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractDelegatingDataSource.java new file mode 100644 index 00000000..db8ebbb4 --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractDelegatingDataSource.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db.provider.support; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +public abstract class AbstractDelegatingDataSource implements DataSource { + + protected abstract DataSource getDataSource(); + + @Override + public Connection getConnection() throws SQLException { + return getDataSource().getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return getDataSource().getConnection(username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return getDataSource().getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + getDataSource().setLogWriter(out); + } + + @Override + public int getLoginTimeout() throws SQLException { + return getDataSource().getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + getDataSource().setLoginTimeout(seconds); + } + + @Override + public T unwrap(Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + if (iface.isAssignableFrom(getDataSource().getClass())) { + return iface.cast(getDataSource()); + } + return getDataSource().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return true; + } + if (iface.isAssignableFrom(getDataSource().getClass())) { + return true; + } + return getDataSource().isWrapperFor(iface); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return getDataSource().getParentLogger(); + } +} diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractEmbeddedDatabase.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractEmbeddedDatabase.java index 178ef821..e861b7bf 100644 --- a/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractEmbeddedDatabase.java +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/provider/support/AbstractEmbeddedDatabase.java @@ -2,14 +2,7 @@ import io.zonky.test.db.provider.EmbeddedDatabase; -import javax.sql.DataSource; -import java.io.PrintWriter; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.logging.Logger; - -public abstract class AbstractEmbeddedDatabase implements EmbeddedDatabase { +public abstract class AbstractEmbeddedDatabase extends AbstractDelegatingDataSource implements EmbeddedDatabase { private final Runnable closeCallback; @@ -17,65 +10,6 @@ protected AbstractEmbeddedDatabase(Runnable closeCallback) { this.closeCallback = closeCallback; } - protected abstract DataSource getDataSource(); - - @Override - public Connection getConnection() throws SQLException { - return getDataSource().getConnection(); - } - - @Override - public Connection getConnection(String username, String password) throws SQLException { - return getDataSource().getConnection(username, password); - } - - @Override - public PrintWriter getLogWriter() throws SQLException { - return getDataSource().getLogWriter(); - } - - @Override - public void setLogWriter(PrintWriter out) throws SQLException { - getDataSource().setLogWriter(out); - } - - @Override - public int getLoginTimeout() throws SQLException { - return getDataSource().getLoginTimeout(); - } - - @Override - public void setLoginTimeout(int seconds) throws SQLException { - getDataSource().setLoginTimeout(seconds); - } - - @Override - public T unwrap(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return iface.cast(this); - } - if (iface.isAssignableFrom(getDataSource().getClass())) { - return iface.cast(getDataSource()); - } - return getDataSource().unwrap(iface); - } - - @Override - public boolean isWrapperFor(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return true; - } - if (iface.isAssignableFrom(getDataSource().getClass())) { - return true; - } - return getDataSource().isWrapperFor(iface); - } - - @Override - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - return getDataSource().getParentLogger(); - } - @Override public synchronized void close() { closeCallback.run(); diff --git a/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json b/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json index 0a57781d..b9a3fd90 100644 --- a/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json @@ -47,6 +47,10 @@ { "name": "zonky.test.database.mariadb.docker", "description": "Additional configuration properties for MariaDB database running in a Docker container. Only available if database provider is set to Docker." + }, + { + "name": "zonky.test.database.spring.optimized-sql-init", + "description": "Configuration properties for optimized integration with Spring Boot's DataSourceScriptDatabaseInitializer." } ], "properties": [ @@ -215,6 +219,12 @@ "type": "java.lang.String", "description": "Mount options used to configure the tmpfs filesystem.", "defaultValue": "rw,noexec,nosuid" + }, + { + "name": "zonky.test.database.spring.optimized-sql-init.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable optimized processing of Spring Boot's SQL initialization scripts.", + "defaultValue": true } ], "hints": [ @@ -259,17 +269,33 @@ "value": "docker", "description": "Run the embedded database in Docker as a container." }, + { + "value": "embedded", + "description": "Use an embedded database provider." + }, { "value": "zonky", - "description": "Use Zonky's fork of OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/zonkyio/embedded-postgres)." + "description": "Use Zonky's fork of OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/zonkyio/embedded-postgres).", + "deprecation": { + "replacement": "embedded", + "reason": "Zonky provider has been renamed to the Embedded Postgres provider and is scheduled to be removed in the next major version." + } }, { "value": "opentable", - "description": "Use OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/opentable/otj-pg-embedded)." + "description": "Use OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/opentable/otj-pg-embedded).", + "deprecation": { + "replacement": "embedded", + "reason": "OpenTable provider has been deprecated in favor of Zonky provider and is scheduled to be removed in the next major version." + } }, { "value": "yandex", - "description": "Use Yandex's Embedded PostgreSQL Server to create the embedded database (https://github.com/yandex-qatools/postgresql-embedded)." + "description": "Use Yandex's Embedded PostgreSQL Server to create the embedded database (https://github.com/yandex-qatools/postgresql-embedded).", + "deprecation": { + "replacement": "embedded", + "reason": "Yandex provider has been deprecated in favor of Zonky provider and is scheduled to be removed in the next major version." + } } ], "providers": [ diff --git a/embedded-database-spring-test/src/test/java/io/zonky/test/db/SpringBootSqlInitIntegrationTest.java b/embedded-database-spring-test/src/test/java/io/zonky/test/db/SpringBootSqlInitIntegrationTest.java new file mode 100644 index 00000000..a12ae000 --- /dev/null +++ b/embedded-database-spring-test/src/test/java/io/zonky/test/db/SpringBootSqlInitIntegrationTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db; + +import io.zonky.test.category.FlywayTestSuite; +import io.zonky.test.support.ConditionalTestRule; +import io.zonky.test.support.TestAssumptions; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Map; + +import static io.zonky.test.db.AutoConfigureEmbeddedDatabase.DatabaseType.POSTGRES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@RunWith(SpringRunner.class) +@Category(FlywayTestSuite.class) +@AutoConfigureEmbeddedDatabase(type = POSTGRES) +@JdbcTest +@TestPropertySource(properties = { + "spring.sql.init.mode=always", + "spring.sql.init.schema-locations=" + + "classpath:/db/schema/init-schema.sql," + + "classpath:/db/migration/V0001_1__create_person_table.sql," + + "classpath:/db/migration/V0002_1__rename_surname_column.sql", + + "flyway.enabled=false", + "spring.flyway.enabled=false", + "liquibase.enabled=false", + "spring.liquibase.enabled=false" +}) +public class SpringBootSqlInitIntegrationTest { + + @ClassRule + public static ConditionalTestRule conditionalTestRule = new ConditionalTestRule(() -> { + TestAssumptions.assumeSpringBootSupportsJdbcTestAnnotation(); + TestAssumptions.assumeSpringBootSupportsSqlInit(); + }); + + private static final String SQL_SELECT_PERSONS = "select * from test.person"; + + @Configuration + static class Config { + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + } + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + public void testSqlInitScripts() { + assertThat(jdbcTemplate).isNotNull(); + + List> persons = jdbcTemplate.queryForList(SQL_SELECT_PERSONS); + assertThat(persons).isNotNull().hasSize(1); + + Map person = persons.get(0); + assertThat(person).containsExactly( + entry("id", 1L), + entry("first_name", "Dave"), + entry("last_name", "Syer")); + } +} diff --git a/embedded-database-spring-test/src/test/java/io/zonky/test/db/sb4/SpringBootSqlInitIntegrationTest.java b/embedded-database-spring-test/src/test/java/io/zonky/test/db/sb4/SpringBootSqlInitIntegrationTest.java new file mode 100644 index 00000000..7b4ec237 --- /dev/null +++ b/embedded-database-spring-test/src/test/java/io/zonky/test/db/sb4/SpringBootSqlInitIntegrationTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.zonky.test.db.sb4; + +import io.zonky.test.category.FlywayTestSuite; +import io.zonky.test.db.AutoConfigureEmbeddedDatabase; +import io.zonky.test.support.ConditionalTestRule; +import io.zonky.test.support.TestAssumptions; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.test.autoconfigure.JdbcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Map; + +import static io.zonky.test.db.AutoConfigureEmbeddedDatabase.DatabaseType.POSTGRES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@RunWith(SpringRunner.class) +@Category(FlywayTestSuite.class) +@AutoConfigureEmbeddedDatabase(type = POSTGRES) +@JdbcTest +@TestPropertySource(properties = { + "spring.sql.init.mode=always", + "spring.sql.init.schema-locations=" + + "classpath:/db/schema/init-schema.sql," + + "classpath:/db/migration/V0001_1__create_person_table.sql," + + "classpath:/db/migration/V0002_1__rename_surname_column.sql", + + "flyway.enabled=false", + "spring.flyway.enabled=false", + "liquibase.enabled=false", + "spring.liquibase.enabled=false" +}) +public class SpringBootSqlInitIntegrationTest { + + @ClassRule + public static ConditionalTestRule conditionalTestRule = new ConditionalTestRule(() -> { + TestAssumptions.assumeSpringBoot4SupportsJdbcTestAnnotation(); + TestAssumptions.assumeSpringBootSupportsSqlInit(); + }); + + private static final String SQL_SELECT_PERSONS = "select * from test.person"; + + @Configuration + static class Config { + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + } + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + public void testSqlInitScripts() { + assertThat(jdbcTemplate).isNotNull(); + + List> persons = jdbcTemplate.queryForList(SQL_SELECT_PERSONS); + assertThat(persons).isNotNull().hasSize(1); + + Map person = persons.get(0); + assertThat(person).containsExactly( + entry("id", 1L), + entry("first_name", "Dave"), + entry("last_name", "Syer")); + } +} diff --git a/embedded-database-spring-test/src/test/java/io/zonky/test/support/TestAssumptions.java b/embedded-database-spring-test/src/test/java/io/zonky/test/support/TestAssumptions.java index 76d8fda1..3d996a2c 100644 --- a/embedded-database-spring-test/src/test/java/io/zonky/test/support/TestAssumptions.java +++ b/embedded-database-spring-test/src/test/java/io/zonky/test/support/TestAssumptions.java @@ -46,6 +46,10 @@ public static void assumeSpringBoot4SupportsJdbcTestAnnotation() { assumeTrue(ClassUtils.isPresent("org.springframework.boot.jdbc.test.autoconfigure.JdbcTest", TestAssumptions.class.getClassLoader())); } + public static void assumeSpringBootSupportsSqlInit() { + assumeTrue(ClassUtils.isPresent("org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer", TestAssumptions.class.getClassLoader())); + } + public static void assumeLicenceAcceptance() { assumeTrue(TestAssumptions.class.getResource("/container-license-acceptance.txt") != null); } diff --git a/spring-boot-4-stubs/src/main/java/org/springframework/boot/jdbc/init/DataSourceScriptDatabaseInitializer.java b/spring-boot-4-stubs/src/main/java/org/springframework/boot/jdbc/init/DataSourceScriptDatabaseInitializer.java new file mode 100644 index 00000000..cf84c7be --- /dev/null +++ b/spring-boot-4-stubs/src/main/java/org/springframework/boot/jdbc/init/DataSourceScriptDatabaseInitializer.java @@ -0,0 +1,17 @@ +/* + * Compile-only stub of a Spring Boot class. + * Allows the library to provide implementations for Spring Boot 4 + * while still being compiled against Spring Boot 3.x. + * + * Original source: org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer + * https://github.com/spring-projects/spring-boot + */ + +package org.springframework.boot.jdbc.init; + +public class DataSourceScriptDatabaseInitializer { + + public boolean initializeDatabase() { + return false; + } +} diff --git a/spring-boot-4-stubs/src/main/java/org/springframework/boot/sql/init/DatabaseInitializationSettings.java b/spring-boot-4-stubs/src/main/java/org/springframework/boot/sql/init/DatabaseInitializationSettings.java new file mode 100644 index 00000000..c70833b0 --- /dev/null +++ b/spring-boot-4-stubs/src/main/java/org/springframework/boot/sql/init/DatabaseInitializationSettings.java @@ -0,0 +1,14 @@ +/* + * Compile-only stub of a Spring Boot class. + * Allows the library to provide implementations for Spring Boot 4 + * while still being compiled against Spring Boot 3.x. + * + * Original source: org.springframework.boot.sql.init.DatabaseInitializationSettings + * https://github.com/spring-projects/spring-boot + */ + +package org.springframework.boot.sql.init; + +public class DatabaseInitializationSettings { + +}