diff --git a/.github/workflows/spring-data-mybatis-sample.yaml b/.github/workflows/spring-data-mybatis-sample.yaml
index b19a17290..e7dfbc7b3 100644
--- a/.github/workflows/spring-data-mybatis-sample.yaml
+++ b/.github/workflows/spring-data-mybatis-sample.yaml
@@ -25,6 +25,9 @@ jobs:
with:
distribution: temurin
java-version: 17
- - name: Run tests
+ - name: Run GoogleSQL sample tests
run: mvn test
- working-directory: samples/spring-data-mybatis
+ working-directory: samples/spring-data-mybatis/googlesql
+ - name: Run PostgreSQL sample tests
+ run: mvn test
+ working-directory: samples/spring-data-mybatis/postgresql
diff --git a/samples/spring-data-mybatis/README.md b/samples/spring-data-mybatis/README.md
index 61dc46f48..04e1b0950 100644
--- a/samples/spring-data-mybatis/README.md
+++ b/samples/spring-data-mybatis/README.md
@@ -1,100 +1,9 @@
-# Spring Data MyBatis Sample Application with Cloud Spanner PostgreSQL
+# Spring Data MyBatis
-This sample application shows how to develop portable applications using Spring Data MyBatis in
-combination with Cloud Spanner PostgreSQL. This application can be configured to run on either a
-[Cloud Spanner PostgreSQL](https://cloud.google.com/spanner/docs/postgresql-interface) database or
-an open-source PostgreSQL database. The only change that is needed to switch between the two is
-changing the active Spring profile that is used by the application.
+This directory contains two sample applications for using Spring Data MyBatis
+with the Spanner JDBC driver.
-The application uses the Cloud Spanner JDBC driver to connect to Cloud Spanner PostgreSQL, and it
-uses the PostgreSQL JDBC driver to connect to open-source PostgreSQL. Spring Data MyBatis works with
-both drivers and offers a single consistent API to the application developer, regardless of the
-actual database or JDBC driver being used.
-
-This sample shows:
-
-1. How to use Spring Data MyBatis with Cloud Spanner PostgreSQL.
-2. How to develop a portable application that runs on both Google Cloud Spanner PostgreSQL and
- open-source PostgreSQL with the same code base.
-3. How to use bit-reversed sequences to automatically generate primary key values for entities.
-4. How to use the Spanner Emulator for development in combination with Spring Data.
-
-__NOTE__: This application does __not require PGAdapter__. Instead, it connects to Cloud Spanner
-PostgreSQL using the Cloud Spanner JDBC driver.
-
-## Cloud Spanner PostgreSQL
-
-Cloud Spanner PostgreSQL provides language support by expressing Spanner database functionality
-through a subset of open-source PostgreSQL language constructs, with extensions added to support
-Spanner functionality like interleaved tables and hinting.
-
-The PostgreSQL interface makes the capabilities of Spanner —__fully managed, unlimited scale, strong
-consistency, high performance, and up to 99.999% global availability__— accessible using the
-PostgreSQL dialect. Unlike other services that manage actual PostgreSQL database instances, Spanner
-uses PostgreSQL-compatible syntax to expose its existing scale-out capabilities. This provides
-familiarity for developers and portability for applications, but not 100% PostgreSQL compatibility.
-The SQL syntax that Spanner supports is semantically equivalent PostgreSQL, meaning schemas
-and queries written against the PostgreSQL interface can be easily ported to another PostgreSQL
-environment.
-
-This sample showcases this portability with an application that works on both Cloud Spanner PostgreSQL
-and open-source PostgreSQL with the same code base.
-
-## MyBatis Spring
-[MyBatis Spring](http://mybatis.org/spring/) integrates MyBatis with the popular Java Spring
-framework. This allows MyBatis to participate in Spring transactions and to automatically inject
-MyBatis mappers into other beans.
-
-## Sample Application
-
-This sample shows how to create a portable application using Spring Data MyBatis and the Cloud Spanner
-PostgreSQL dialect. The application works on both Cloud Spanner PostgreSQL and open-source
-PostgreSQL. You can switch between the two by changing the active Spring profile:
-* Profile `cs` runs the application on Cloud Spanner PostgreSQL.
-* Profile `pg` runs the application on open-source PostgreSQL.
-
-The default profile is `cs`. You can change the default profile by modifying the
-[application.properties](src/main/resources/application.properties) file.
-
-### Running the Application
-
-1. Choose the database system that you want to use by choosing a profile. The default profile is
- `cs`, which runs the application on Cloud Spanner PostgreSQL.
-2. The sample by default starts an instance of the Spanner Emulator together with the application and
- runs the application against the emulator.
-3. Modify the default profile in the [application.properties](src/main/resources/application.properties)
- file to run the sample on an open-source PostgreSQL database.
-4. Modify either [application-cs.properties](src/main/resources/application-cs.properties) or
- [application-pg.properties](src/main/resources/application-pg.properties) to point to an existing
- database. If you use Cloud Spanner, the database that the configuration file references must be a
- database that uses the PostgreSQL dialect.
-5. Run the application with `mvn spring-boot:run`.
-
-### Main Application Components
-
-The main application components are:
-* [DatabaseSeeder.java](src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java): This
- class is responsible for creating the database schema and inserting some initial test data. The
- schema is created from the [create_schema.sql](src/main/resources/create_schema.sql) file. The
- `DatabaseSeeder` class loads this file into memory and executes it on the active database using
- standard JDBC APIs. The class also removes Cloud Spanner-specific extensions to the PostgreSQL
- dialect when the application runs on open-source PostgreSQL.
-* [JdbcConfiguration.java](src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java):
- This utility class is used to determine whether the application is running on Cloud Spanner
- PostgreSQL or open-source PostgreSQL. This can be used if you have specific features that should
- only be executed on one of the two systems.
-* [EmulatorInitializer.java](src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java):
- This ApplicationListener automatically starts the Spanner emulator as a Docker container if the
- sample has been configured to run on the emulator.
-* [AbstractEntity.java](src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java):
- This is the shared base class for all entities in this sample application. It defines a number of
- standard attributes, such as the identifier (primary key). The primary key is automatically
- generated using a (bit-reversed) sequence. [Bit-reversed sequential values](https://cloud.google.com/spanner/docs/schema-design#bit_reverse_primary_key)
- are considered a good choice for primary keys on Cloud Spanner.
-* [Application.java](src/main/java/com/google/cloud/spanner/sample/Application.java): The starter
- class of the application. It contains a command-line runner that executes a selection of queries
- and updates on the database.
-* [SingerService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) and
- [AlbumService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) are
- standard Spring service beans that contain business logic that can be executed as transactions.
- This includes both read/write and read-only transactions.
+- [GoogleSQL](googlesql): This sample uses the Spanner GoogleSQL dialect.
+- [PostgreSQL](postgresql): This sample uses the Spanner PostgreSQL dialect and the Spanner JDBC
+ driver. It does not use PGAdapter. The sample application can also be configured to run on open
+ source PostgreSQL, and shows how a portable application be developed using this setup.
diff --git a/samples/spring-data-mybatis/googlesql/README.md b/samples/spring-data-mybatis/googlesql/README.md
new file mode 100644
index 000000000..7badfb698
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/README.md
@@ -0,0 +1,52 @@
+# Spring Data MyBatis Sample Application with Spanner GoogleSQL
+
+This sample application shows how to develop applications using Spring Data MyBatis in
+combination with Spanner GoogleSQL.
+
+This sample shows:
+
+1. How to use Spring Data MyBatis with a Spanner GoogleSQL database.
+2. How to use bit-reversed identity columns to automatically generate primary key values for entities.
+3. How to set the transaction isolation level that is used by the Spanner JDBC driver.
+4. How to use the Spanner Emulator for development in combination with Spring Data.
+
+## MyBatis Spring
+[MyBatis Spring](http://mybatis.org/spring/) integrates MyBatis with the popular Java Spring
+framework. This allows MyBatis to participate in Spring transactions and to automatically inject
+MyBatis mappers into other beans.
+
+### Running the Application
+
+1. The sample by default starts an instance of the Spanner Emulator together with the application and
+ runs the application against the emulator.
+2. To run the sample on a real Spanner database, modify
+ [application.properties](src/main/resources/application.properties) and set the
+ `spanner.emulator` property to `false`. Modify the `spanner.project`, `spanner.instance`, and
+ `spanner.database` properties to point to an existing Spanner database.
+ The database must use the GoogleSQL dialect.
+3. Run the application with `mvn spring-boot:run`.
+
+### Main Application Components
+
+The main application components are:
+* [DatabaseSeeder.java](src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java): This
+ class is responsible for creating the database schema and inserting some initial test data. The
+ schema is created from the [create_schema.sql](src/main/resources/create_schema.sql) file. The
+ `DatabaseSeeder` class loads this file into memory and executes it on the active database using
+ standard JDBC APIs.
+* [EmulatorInitializer.java](src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java):
+ This ApplicationListener automatically starts the Spanner emulator as a Docker container if the
+ sample has been configured to run on the emulator. You can disable this with the `spanner.emulator`
+ property in `application.properties`.
+* [AbstractEntity.java](src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java):
+ This is the shared base class for all entities in this sample application. It defines a number of
+ standard attributes, such as the identifier (primary key). The primary key is automatically
+ generated using a (bit-reversed) identity column. [Bit-reversed sequential values](https://cloud.google.com/spanner/docs/schema-design#bit_reverse_primary_key)
+ are considered a good choice for primary keys in Spanner.
+* [Application.java](src/main/java/com/google/cloud/spanner/sample/Application.java): The starter
+ class of the application. It contains a command-line runner that executes a selection of queries
+ and updates on the database.
+* [SingerService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) and
+ [AlbumService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) are
+ standard Spring service beans that contain business logic that can be executed as transactions.
+ This includes both read/write and read-only transactions.
diff --git a/samples/spring-data-mybatis/googlesql/pom.xml b/samples/spring-data-mybatis/googlesql/pom.xml
new file mode 100644
index 000000000..47fb7b0a4
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/pom.xml
@@ -0,0 +1,130 @@
+
+
+ 4.0.0
+
+ org.example
+ cloud-spanner-spring-data-mybatis-googlesql-example
+ 1.0-SNAPSHOT
+
+ Sample application showing how to use Spring Data MyBatis with Spanner GoogleSQL.
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.5
+
+
+
+ 17
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+ org.springframework.data
+ spring-data-bom
+ 2024.1.5
+ import
+ pom
+
+
+ com.google.cloud
+ libraries-bom
+ 26.59.0
+ import
+ pom
+
+
+ org.testcontainers
+ testcontainers-bom
+ 1.21.0
+ import
+ pom
+
+
+
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 3.0.4
+
+
+ org.mybatis.dynamic-sql
+ mybatis-dynamic-sql
+ 1.5.2
+
+
+
+
+
+ com.google.cloud
+ google-cloud-spanner
+ 6.91.1
+
+
+ com.google.cloud
+ google-cloud-spanner-jdbc
+ 2.29.1
+
+
+ com.google.api.grpc
+ proto-google-cloud-spanner-executor-v1
+
+
+
+
+ org.testcontainers
+ testcontainers
+
+
+
+ com.google.collections
+ google-collections
+ 1.0
+
+
+
+
+ com.google.cloud
+ google-cloud-spanner
+ 6.91.1
+ test-jar
+ test
+
+
+ com.google.api
+ gax-grpc
+ testlib
+ test
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+
+ com.spotify.fmt
+ fmt-maven-plugin
+ 2.25
+
+
+
+ format
+
+
+
+
+
+
+
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/Application.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/Application.java
new file mode 100644
index 000000000..cf9ab71d3
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/Application.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample;
+
+import com.google.cloud.spanner.connection.SpannerPool;
+import com.google.cloud.spanner.sample.entities.Album;
+import com.google.cloud.spanner.sample.entities.Singer;
+import com.google.cloud.spanner.sample.entities.Track;
+import com.google.cloud.spanner.sample.mappers.AlbumMapper;
+import com.google.cloud.spanner.sample.mappers.SingerMapper;
+import com.google.cloud.spanner.sample.service.AlbumService;
+import com.google.cloud.spanner.sample.service.SingerService;
+import java.util.concurrent.ThreadLocalRandom;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application implements CommandLineRunner {
+ private static final Logger logger = LoggerFactory.getLogger(Application.class);
+
+ public static void main(String[] args) {
+ // Start the Spanner emulator in a Docker container if the `spanner.auto_start_emulator`
+ // property has been set to true. If not, then this is a no-op.
+ EmulatorInitializer emulatorInitializer = new EmulatorInitializer();
+ try {
+ SpringApplication application = new SpringApplication(Application.class);
+ application.addListeners(emulatorInitializer);
+ application.run(args).close();
+ } finally {
+ SpannerPool.closeSpannerPool();
+ emulatorInitializer.stopEmulator();
+ }
+ }
+
+ private final DatabaseSeeder databaseSeeder;
+
+ private final SingerService singerService;
+
+ private final AlbumService albumService;
+
+ private final SingerMapper singerMapper;
+
+ private final AlbumMapper albumMapper;
+
+ public Application(
+ SingerService singerService,
+ AlbumService albumService,
+ DatabaseSeeder databaseSeeder,
+ SingerMapper singerMapper,
+ AlbumMapper albumMapper) {
+ this.databaseSeeder = databaseSeeder;
+ this.singerService = singerService;
+ this.albumService = albumService;
+ this.singerMapper = singerMapper;
+ this.albumMapper = albumMapper;
+ }
+
+ @Override
+ public void run(String... args) {
+ // Set the system property 'drop_schema' to true to drop any existing database
+ // schema when the application is executed.
+ if (Boolean.parseBoolean(System.getProperty("drop_schema", "false"))) {
+ logger.info("Dropping existing schema if it exists");
+ databaseSeeder.dropDatabaseSchemaIfExists();
+ }
+
+ logger.info("Creating database schema if it does not already exist");
+ databaseSeeder.createDatabaseSchemaIfNotExists();
+ logger.info("Deleting existing test data");
+ databaseSeeder.deleteTestData();
+ logger.info("Inserting fresh test data");
+ databaseSeeder.insertTestData();
+
+ Iterable allSingers = singerMapper.findAll();
+ for (Singer singer : allSingers) {
+ logger.info(
+ "Found singer: {} with {} albums",
+ singer,
+ albumMapper.countAlbumsBySingerId(singer.getId()));
+ for (Album album : albumMapper.findAlbumsBySingerId(singer.getId())) {
+ logger.info("\tAlbum: {}, released at {}", album, album.getReleaseDate());
+ }
+ }
+
+ // Create a new singer and three albums in a transaction.
+ Singer insertedSinger =
+ singerService.createSingerAndAlbums(
+ new Singer("Amethyst", "Jiang"),
+ new Album(DatabaseSeeder.randomTitle()),
+ new Album(DatabaseSeeder.randomTitle()),
+ new Album(DatabaseSeeder.randomTitle()));
+ logger.info(
+ "Inserted singer {} {} {}",
+ insertedSinger.getId(),
+ insertedSinger.getFirstName(),
+ insertedSinger.getLastName());
+
+ // Create a new Album and some Tracks in a read/write transaction.
+ // Track is an interleaved table.
+ Album album = new Album(DatabaseSeeder.randomTitle());
+ album.setSingerId(insertedSinger.getId());
+ albumService.createAlbumAndTracks(
+ album,
+ new Track(album, 1, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 2, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 3, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 4, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 5, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 6, DatabaseSeeder.randomTitle(), 3.14d),
+ new Track(album, 7, DatabaseSeeder.randomTitle(), 3.14d));
+ logger.info("Inserted album {}", album.getTitle());
+
+ // List all singers that have a last name starting with an 'J'.
+ logger.info("All singers with a last name starting with an 'J':");
+ for (Singer singer : singerMapper.findSingersByLastNameStartingWith("J")) {
+ logger.info("\t{}", singer.getFullName());
+ }
+
+ // The singerService.listSingersWithLastNameStartingWith(..) method uses a read-only
+ // transaction. You should prefer read-only transactions to read/write transactions whenever
+ // possible, as read-only transactions do not take locks.
+ logger.info("All singers with a last name starting with an 'A', 'B', or 'C'.");
+ for (Singer singer : singerService.listSingersWithLastNameStartingWith("A", "B", "C")) {
+ logger.info("\t{}", singer.getFullName());
+ }
+
+ // Execute an insert-or-update for a Singer record.
+ // For this, we either get a random Singer from the database, or create a new Singer entity
+ // and assign it a random ID.
+ logger.info("Executing an insert-or-update statement for a Singer record");
+ Singer singer;
+ if (ThreadLocalRandom.current().nextBoolean()) {
+ singer = singerMapper.getRandom();
+ } else {
+ singer = new Singer();
+ singer.setId(ThreadLocalRandom.current().nextLong());
+ }
+ singer.setFirstName("Beatriz");
+ singer.setLastName("Russel");
+ singer.setActive(true);
+ // This executes an INSERT OR UPDATE statement.
+ singerMapper.insertOrUpdate(singer);
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
new file mode 100644
index 000000000..73898784e
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.cloud.spanner.sample.entities.Singer;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import javax.annotation.Nonnull;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.FileCopyUtils;
+
+/** This component creates the database schema and seeds it with some random test data. */
+@Component
+public class DatabaseSeeder {
+
+ /** Randomly generated names. */
+ public static final ImmutableList INITIAL_SINGERS =
+ ImmutableList.of(
+ new Singer("Aaliyah", "Smith"),
+ new Singer("Benjamin", "Jones"),
+ new Singer("Chloe", "Brown"),
+ new Singer("David", "Williams"),
+ new Singer("Elijah", "Johnson"),
+ new Singer("Emily", "Miller"),
+ new Singer("Gabriel", "Garcia"),
+ new Singer("Hannah", "Rodriguez"),
+ new Singer("Isabella", "Hernandez"),
+ new Singer("Jacob", "Perez"));
+
+ private static final Random RANDOM = new Random();
+
+ private final JdbcTemplate jdbcTemplate;
+
+ @Value("classpath:create_schema.sql")
+ private Resource createSchemaFile;
+
+ @Value("classpath:drop_schema.sql")
+ private Resource dropSchemaFile;
+
+ public DatabaseSeeder(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ /** Reads a resource file into a string. */
+ private static String resourceAsString(Resource resource) {
+ try (Reader reader = new InputStreamReader(resource.getInputStream(), UTF_8)) {
+ return FileCopyUtils.copyToString(reader);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /** Removes all empty statements in the DDL script. */
+ private String[] updateDdlStatements(String[] statements) {
+ // Remove any empty statements from the script.
+ return Arrays.stream(statements)
+ .filter(statement -> !statement.isBlank())
+ .toArray(String[]::new);
+ }
+
+ /** Creates the database schema if it does not yet exist. */
+ public void createDatabaseSchemaIfNotExists() {
+ // We can safely just split the script based on ';', as we know that there are no literals or
+ // other strings that contain semicolons in the script.
+ String[] statements = updateDdlStatements(resourceAsString(createSchemaFile).split(";"));
+ // Execute all the DDL statements as a JDBC batch. That ensures that Spanner will apply all
+ // statements in a single DDL batch, which again is a lot more efficient than executing them
+ // one-by-one.
+ jdbcTemplate.batchUpdate(statements);
+ }
+
+ /** Drops the database schema if it exists. */
+ public void dropDatabaseSchemaIfExists() {
+ // We can safely just split the script based on ';', as we know that there are no literals or
+ // other strings that contain semicolons in the script.
+ String[] statements = updateDdlStatements(resourceAsString(dropSchemaFile).split(";"));
+ // Execute all the DDL statements as a JDBC batch. That ensures that Spanner will apply all
+ // statements in a single DDL batch, which again is a lot more efficient than executing them
+ // one-by-one.
+ jdbcTemplate.batchUpdate(statements);
+ }
+
+ /** Deletes all data currently in the sample tables. */
+ public void deleteTestData() {
+ // Delete all data in one batch.
+ jdbcTemplate.batchUpdate(
+ "delete from concerts where true",
+ "delete from venues where true",
+ "delete from tracks where true",
+ "delete from albums where true",
+ "delete from singers where true");
+ }
+
+ /** Inserts some initial test data into the database. */
+ public void insertTestData() {
+ jdbcTemplate.batchUpdate(
+ "insert into singers (first_name, last_name) values (?, ?)",
+ new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(@Nonnull PreparedStatement preparedStatement, int i)
+ throws SQLException {
+ preparedStatement.setString(1, INITIAL_SINGERS.get(i).getFirstName());
+ preparedStatement.setString(2, INITIAL_SINGERS.get(i).getLastName());
+ }
+
+ @Override
+ public int getBatchSize() {
+ return INITIAL_SINGERS.size();
+ }
+ });
+
+ List singerIds =
+ jdbcTemplate.query(
+ "select id from singers",
+ resultSet -> {
+ ImmutableList.Builder builder = ImmutableList.builder();
+ while (resultSet.next()) {
+ builder.add(resultSet.getLong(1));
+ }
+ return builder.build();
+ });
+ jdbcTemplate.batchUpdate(
+ "insert into albums (title, marketing_budget, release_date, cover_picture, singer_id) values (?, ?, ?, ?, ?)",
+ new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(@Nonnull PreparedStatement preparedStatement, int i)
+ throws SQLException {
+ preparedStatement.setString(1, randomTitle());
+ preparedStatement.setBigDecimal(2, randomBigDecimal());
+ preparedStatement.setObject(3, randomDate());
+ preparedStatement.setBytes(4, randomBytes());
+ preparedStatement.setLong(5, randomElement(singerIds));
+ }
+
+ @Override
+ public int getBatchSize() {
+ return INITIAL_SINGERS.size() * 20;
+ }
+ });
+ }
+
+ /** Generates a random title for an album or a track. */
+ static String randomTitle() {
+ return randomElement(ADJECTIVES) + " " + randomElement(NOUNS);
+ }
+
+ /** Returns a random element from the given list. */
+ static T randomElement(List list) {
+ return list.get(RANDOM.nextInt(list.size()));
+ }
+
+ /** Generates a random {@link BigDecimal}. */
+ BigDecimal randomBigDecimal() {
+ return BigDecimal.valueOf(RANDOM.nextDouble()).setScale(9, RoundingMode.HALF_UP);
+ }
+
+ /** Generates a random {@link LocalDate}. */
+ static LocalDate randomDate() {
+ return LocalDate.of(RANDOM.nextInt(200) + 1800, RANDOM.nextInt(12) + 1, RANDOM.nextInt(28) + 1);
+ }
+
+ /** Generates a random byte array with a length between 4 and 1024 bytes. */
+ static byte[] randomBytes() {
+ int size = RANDOM.nextInt(1020) + 4;
+ byte[] res = new byte[size];
+ RANDOM.nextBytes(res);
+ return res;
+ }
+
+ /** Some randomly generated nouns that are used to generate random titles. */
+ private static final ImmutableList NOUNS =
+ ImmutableList.of(
+ "apple",
+ "banana",
+ "cherry",
+ "dog",
+ "elephant",
+ "fish",
+ "grass",
+ "house",
+ "key",
+ "lion",
+ "monkey",
+ "nail",
+ "orange",
+ "pen",
+ "queen",
+ "rain",
+ "shoe",
+ "tree",
+ "umbrella",
+ "van",
+ "whale",
+ "xylophone",
+ "zebra");
+
+ /** Some randomly generated adjectives that are used to generate random titles. */
+ private static final ImmutableList ADJECTIVES =
+ ImmutableList.of(
+ "able",
+ "angelic",
+ "artistic",
+ "athletic",
+ "attractive",
+ "autumnal",
+ "calm",
+ "careful",
+ "cheerful",
+ "clever",
+ "colorful",
+ "confident",
+ "courageous",
+ "creative",
+ "curious",
+ "daring",
+ "determined",
+ "different",
+ "dreamy",
+ "efficient",
+ "elegant",
+ "energetic",
+ "enthusiastic",
+ "exciting",
+ "expressive",
+ "faithful",
+ "fantastic",
+ "funny",
+ "gentle",
+ "gifted",
+ "great",
+ "happy",
+ "helpful",
+ "honest",
+ "hopeful",
+ "imaginative",
+ "intelligent",
+ "interesting",
+ "inventive",
+ "joyful",
+ "kind",
+ "knowledgeable",
+ "loving",
+ "loyal",
+ "magnificent",
+ "mature",
+ "mysterious",
+ "natural",
+ "nice",
+ "optimistic",
+ "peaceful",
+ "perfect",
+ "pleasant",
+ "powerful",
+ "proud",
+ "quick",
+ "relaxed",
+ "reliable",
+ "responsible",
+ "romantic",
+ "safe",
+ "sensitive",
+ "sharp",
+ "simple",
+ "sincere",
+ "skillful",
+ "smart",
+ "sociable",
+ "strong",
+ "successful",
+ "sweet",
+ "talented",
+ "thankful",
+ "thoughtful",
+ "unique",
+ "upbeat",
+ "valuable",
+ "victorious",
+ "vivacious",
+ "warm",
+ "wealthy",
+ "wise",
+ "wonderful",
+ "worthy",
+ "youthful");
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java
new file mode 100644
index 000000000..6c6130be6
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample;
+
+import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.PullPolicy;
+import org.testcontainers.utility.DockerImageName;
+
+public class EmulatorInitializer
+ implements ApplicationListener {
+ private GenericContainer> emulator;
+
+ @Override
+ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+ ConfigurableEnvironment environment = event.getEnvironment();
+ boolean useEmulator =
+ Boolean.TRUE.equals(environment.getProperty("spanner.emulator", Boolean.class));
+ boolean autoStartEmulator =
+ Boolean.TRUE.equals(environment.getProperty("spanner.auto_start_emulator", Boolean.class));
+ if (!(useEmulator && autoStartEmulator)) {
+ return;
+ }
+
+ emulator =
+ new GenericContainer<>(DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator"));
+ emulator.withImagePullPolicy(PullPolicy.alwaysPull());
+ emulator.addExposedPort(9010);
+ emulator.setWaitStrategy(Wait.forListeningPorts(9010));
+ emulator.start();
+
+ System.setProperty("spanner.endpoint", "//localhost:" + emulator.getMappedPort(9010));
+ }
+
+ public void stopEmulator() {
+ if (this.emulator != null) {
+ this.emulator.stop();
+ }
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
new file mode 100644
index 000000000..dcf64a22d
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+import java.time.OffsetDateTime;
+
+public abstract class AbstractEntity {
+
+ /** This ID is generated using a (bit-reversed) identity column. */
+ private Long id;
+
+ private OffsetDateTime createdAt;
+
+ private OffsetDateTime updatedAt;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AbstractEntity)) {
+ return false;
+ }
+ AbstractEntity other = (AbstractEntity) o;
+ if (this == other) {
+ return true;
+ }
+ return this.getClass().equals(other.getClass())
+ && this.id != null
+ && other.id != null
+ && this.id.equals(other.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.id == null ? 0 : this.id.hashCode();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public OffsetDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ protected void setCreatedAt(OffsetDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public OffsetDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ protected void setUpdatedAt(OffsetDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Album.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
new file mode 100644
index 000000000..9ea238506
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class Album extends AbstractEntity {
+
+ private String title;
+
+ private BigDecimal marketingBudget;
+
+ private LocalDate releaseDate;
+
+ private byte[] coverPicture;
+
+ private Long singerId;
+
+ public Album() {}
+
+ public Album(String title) {
+ this.title = title;
+ }
+
+ @Override
+ public String toString() {
+ return getTitle();
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public BigDecimal getMarketingBudget() {
+ return marketingBudget;
+ }
+
+ public void setMarketingBudget(BigDecimal marketingBudget) {
+ this.marketingBudget = marketingBudget;
+ }
+
+ public LocalDate getReleaseDate() {
+ return releaseDate;
+ }
+
+ public void setReleaseDate(LocalDate releaseDate) {
+ this.releaseDate = releaseDate;
+ }
+
+ public byte[] getCoverPicture() {
+ return coverPicture;
+ }
+
+ public void setCoverPicture(byte[] coverPicture) {
+ this.coverPicture = coverPicture;
+ }
+
+ public Long getSingerId() {
+ return singerId;
+ }
+
+ public void setSingerId(Long singerId) {
+ this.singerId = singerId;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
new file mode 100644
index 000000000..ac13102af
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+import java.time.OffsetDateTime;
+
+public class Concert extends AbstractEntity {
+
+ private Long venueId;
+
+ private Long singerId;
+
+ private String name;
+
+ private OffsetDateTime startTime;
+
+ private OffsetDateTime endTime;
+
+ public Concert(Venue venue, Singer singer, String name) {
+ this.venueId = venue.getId();
+ this.singerId = singer.getId();
+ this.name = name;
+ }
+
+ public Long getVenueId() {
+ return venueId;
+ }
+
+ public void setVenueId(Long venueId) {
+ this.venueId = venueId;
+ }
+
+ public Long getSingerId() {
+ return singerId;
+ }
+
+ public void setSingerId(Long singerId) {
+ this.singerId = singerId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public OffsetDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(OffsetDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public OffsetDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(OffsetDateTime endTime) {
+ this.endTime = endTime;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
new file mode 100644
index 000000000..b3f6d1c4f
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+public class Singer extends AbstractEntity {
+
+ private String firstName;
+
+ private String lastName;
+
+ /** The full name is generated by the database using a generated column. */
+ private String fullName;
+
+ private Boolean active;
+
+ public Singer() {}
+
+ public Singer(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return getFullName();
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ public Boolean getActive() {
+ return active;
+ }
+
+ public void setActive(Boolean active) {
+ this.active = active;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Track.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
new file mode 100644
index 000000000..8191c696c
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+/**
+ * The "tracks" table is interleaved in "albums". That means that the first part of the primary key
+ * (the "id" column) references the Album that this Track belongs to. That again means that we do
+ * not auto-generate the id for this entity.
+ */
+public class Track extends AbstractEntity {
+
+ /**
+ * This is the second part of the primary key of a Track. The first part, the 'id' column is
+ * defined in the {@link AbstractEntity} super class.
+ */
+ private int trackNumber;
+
+ private String title;
+
+ private Double sampleRate;
+
+ public Track(Album album, int trackNumber, String title, Double sampleRate) {
+ setId(album.getId());
+ this.trackNumber = trackNumber;
+ this.title = title;
+ this.sampleRate = sampleRate;
+ }
+
+ public int getTrackNumber() {
+ return trackNumber;
+ }
+
+ public void setTrackNumber(int trackNumber) {
+ this.trackNumber = trackNumber;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public Double getSampleRate() {
+ return sampleRate;
+ }
+
+ public void setSampleRate(Double sampleRate) {
+ this.sampleRate = sampleRate;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
new file mode 100644
index 000000000..f5eb4443d
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.entities;
+
+public class Venue extends AbstractEntity {
+ private String name;
+
+ private String description;
+
+ public Venue(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
new file mode 100644
index 000000000..f39b08a33
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.mappers;
+
+import com.google.cloud.spanner.sample.entities.Album;
+import java.util.List;
+import java.util.Optional;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface AlbumMapper {
+
+ @Select("SELECT * FROM albums WHERE id = #{albumId}")
+ Album get(@Param("albumId") long albumId);
+
+ @Select("SELECT * FROM albums LIMIT 1")
+ Optional getFirst();
+
+ @Select("SELECT COUNT(1) FROM albums WHERE singer_id = #{singerId}")
+ long countAlbumsBySingerId(@Param("singerId") long singerId);
+
+ @Select("SELECT * FROM albums WHERE singer_id = #{singerId}")
+ List findAlbumsBySingerId(@Param("singerId") long singerId);
+
+ @Insert(
+ "INSERT INTO albums (title, marketing_budget, release_date, cover_picture, singer_id) "
+ + "VALUES (#{title}, #{marketingBudget}, #{releaseDate}, #{coverPicture}, #{singerId})")
+ @Options(useGeneratedKeys = true, keyProperty = "id")
+ int insert(Album album);
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
new file mode 100644
index 000000000..1b52c603f
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.mappers;
+
+import com.google.cloud.spanner.sample.entities.Venue;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface ConcertMapper {
+
+ @Select("SELECT * FROM concerts WHERE id = #{concertId}")
+ Venue get(@Param("concertId") long concertId);
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
new file mode 100644
index 000000000..65ddb72ed
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.mappers;
+
+import com.google.cloud.spanner.sample.entities.Singer;
+import java.util.List;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+@Mapper
+public interface SingerMapper {
+
+ @Select("SELECT * FROM singers WHERE id = #{singerId}")
+ Singer get(@Param("singerId") long singerId);
+
+ @Select("SELECT * FROM singers TABLESAMPLE RESERVOIR (1 ROWS)")
+ Singer getRandom();
+
+ @Select("SELECT * FROM singers ORDER BY last_name, first_name, id")
+ List findAll();
+
+ @Select("SELECT * FROM singers WHERE starts_with(last_name, #{lastName})")
+ List findSingersByLastNameStartingWith(@Param("lastName") String lastName);
+
+ /**
+ * Inserts a new singer record and returns both the generated primary key value and the generated
+ * full name.
+ */
+ @Insert(
+ "INSERT INTO singers (first_name, last_name, active) "
+ + "VALUES (#{firstName}, #{lastName}, #{active})")
+ @Options(useGeneratedKeys = true, keyProperty = "id,fullName")
+ int insert(Singer singer);
+
+ /**
+ * Executes an insert-or-update statement for a Singer record. Note that the id must have been set
+ * manually on the Singer entity before calling this method. The statement only returns the
+ * 'fullName' property, because the 'id' is already known.
+ */
+ @Insert(
+ "INSERT OR UPDATE singers (id, first_name, last_name, active) "
+ + "VALUES (#{id}, #{firstName}, #{lastName}, #{active})")
+ @Options(useGeneratedKeys = true, keyProperty = "fullName")
+ int insertOrUpdate(Singer singer);
+
+ /** Updates an existing singer and returns the generated full name. */
+ @Update(
+ "UPDATE singers SET "
+ + "first_name=#{first_name}, "
+ + "last_name=#{last_name}, "
+ + "active=#{active} "
+ + "WHERE id=#{id}")
+ @Options(useGeneratedKeys = true, keyProperty = "fullName")
+ int update(Singer singer);
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
new file mode 100644
index 000000000..729c56fa6
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.mappers;
+
+import com.google.cloud.spanner.sample.entities.Track;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface TrackMapper {
+
+ @Select("SELECT * FROM tracks WHERE id = #{albumId} AND track_number = #{trackNumber}")
+ Track get(@Param("albumId") long albumId, @Param("trackNumber") long trackNumber);
+
+ @Insert(
+ "INSERT INTO tracks (id, track_number, title, sample_rate) "
+ + "VALUES (#{id}, #{trackNumber}, #{title}, #{sampleRate})")
+ @Options(useGeneratedKeys = true, keyProperty = "id")
+ int insert(Track track);
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
new file mode 100644
index 000000000..be220f867
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.mappers;
+
+import com.google.cloud.spanner.sample.entities.Venue;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface VenueMapper {
+
+ @Select("SELECT * FROM venues WHERE id = #{venueId}")
+ Venue get(@Param("venueId") long venueId);
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
new file mode 100644
index 000000000..1a7e125f0
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.service;
+
+import com.google.cloud.spanner.sample.entities.Album;
+import com.google.cloud.spanner.sample.entities.Track;
+import com.google.cloud.spanner.sample.mappers.AlbumMapper;
+import com.google.cloud.spanner.sample.mappers.TrackMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AlbumService {
+ private final AlbumMapper albumMapper;
+
+ private final TrackMapper trackMapper;
+
+ public AlbumService(AlbumMapper albumMapper, TrackMapper trackMapper) {
+ this.albumMapper = albumMapper;
+ this.trackMapper = trackMapper;
+ }
+
+ /** Creates an album and a set of tracks in a read/write transaction. */
+ @Transactional
+ public Album createAlbumAndTracks(Album album, Track... tracks) {
+ // Saving an album will update the album entity with the generated primary key.
+ albumMapper.insert(album);
+ for (Track track : tracks) {
+ // Set the id that was generated on the Album before saving it.
+ track.setId(album.getId());
+ trackMapper.insert(track);
+ }
+ return album;
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
new file mode 100644
index 000000000..c56893a1b
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample.service;
+
+import com.google.cloud.spanner.sample.entities.Album;
+import com.google.cloud.spanner.sample.entities.Singer;
+import com.google.cloud.spanner.sample.mappers.AlbumMapper;
+import com.google.cloud.spanner.sample.mappers.SingerMapper;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class SingerService {
+ private final SingerMapper singerRepository;
+
+ private final AlbumMapper albumRepository;
+
+ public SingerService(SingerMapper singerRepository, AlbumMapper albumRepository) {
+ this.singerRepository = singerRepository;
+ this.albumRepository = albumRepository;
+ }
+
+ /** Creates a singer and a list of albums in a read/write transaction. */
+ @Transactional
+ public Singer createSingerAndAlbums(Singer singer, Album... albums) {
+ // Saving a singer will update the singer entity with the generated primary key.
+ singerRepository.insert(singer);
+ for (Album album : albums) {
+ // Set the singerId that was generated on the Album before saving it.
+ album.setSingerId(singer.getId());
+ albumRepository.insert(album);
+ }
+ return singer;
+ }
+
+ /**
+ * Searches for all singers that have a last name starting with any of the given prefixes. This
+ * method uses a read-only transaction. Read-only transactions should be preferred to read/write
+ * transactions whenever possible, as read-only transactions do not take locks.
+ */
+ @Transactional(readOnly = true)
+ public List listSingersWithLastNameStartingWith(String... prefixes) {
+ ImmutableList.Builder result = ImmutableList.builder();
+ // This is not the most efficient way to search for this, but the main purpose of this method is
+ // to show how to use read-only transactions.
+ for (String prefix : prefixes) {
+ result.addAll(singerRepository.findSingersByLastNameStartingWith(prefix));
+ }
+ return result.build();
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/main/resources/application.properties b/samples/spring-data-mybatis/googlesql/src/main/resources/application.properties
new file mode 100644
index 000000000..bbe258dbf
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/resources/application.properties
@@ -0,0 +1,36 @@
+
+# Map column names with an underscore to property names in camel case.
+# E.g. column 'full_name' maps to Java property 'fullName'.
+mybatis.configuration.map-underscore-to-camel-case=true
+
+# The sample by default uses the Spanner emulator.
+# Disable this flag to run the sample on a real Spanner instance.
+spanner.emulator=true
+
+# The sample by default starts an emulator instance in Docker.
+# Disable this flag to run the sample on an Emulator instance that
+# you start manually, for example if you don't have Docker installed
+# on your local machine. Keep the 'spanner.emulator=true' line above
+# to connect to the emulator that you have started.
+spanner.auto_start_emulator=true
+
+# Update these properties to match your project, instance, and database.
+spanner.project=my-project
+spanner.instance=my-instance
+spanner.database=mybatis-sample
+
+# Sets the isolation level that will be used by default for read/write transactions.
+# Spanner supports the isolation levels SERIALIZABLE and REPEATABLE READ.
+spanner.default_isolation_level=REPEATABLE_READ
+
+spring.datasource.url=jdbc:cloudspanner:${spanner.endpoint}/projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database};default_isolation_level=${spanner.default_isolation_level};autoConfigEmulator=${spanner.emulator};${spanner.additional_properties}
+spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
+
+
+# These properties are only used for testing.
+
+# This property is automatically set to point to the Spanner emulator that is automatically
+# started together with the application. It remains empty if the application is executed
+# against a real Spanner instance.
+spanner.endpoint=
+spanner.additional_properties=
diff --git a/samples/spring-data-mybatis/googlesql/src/main/resources/create_schema.sql b/samples/spring-data-mybatis/googlesql/src/main/resources/create_schema.sql
new file mode 100644
index 000000000..f54ef6492
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/main/resources/create_schema.sql
@@ -0,0 +1,59 @@
+
+-- This script creates the database schema for this sample application.
+-- The script is executed by the DatabaseSeeder class.
+
+CREATE TABLE IF NOT EXISTS singers (
+ id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) PRIMARY KEY,
+ first_name STRING(MAX),
+ last_name STRING(MAX),
+ full_name STRING(MAX) AS (CASE WHEN first_name IS NULL THEN last_name
+ WHEN last_name IS NULL THEN first_name
+ ELSE first_name || ' ' || last_name END) STORED,
+ active BOOL DEFAULT (TRUE),
+ created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+);
+
+CREATE TABLE IF NOT EXISTS albums (
+ id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) PRIMARY KEY,
+ title STRING(MAX) NOT NULL,
+ marketing_budget NUMERIC,
+ release_date DATE,
+ cover_picture BYTES(MAX),
+ singer_id INT64 NOT NULL,
+ created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ CONSTRAINT fk_albums_singers FOREIGN KEY (singer_id) REFERENCES singers (id)
+);
+
+CREATE TABLE IF NOT EXISTS tracks (
+ id INT64 NOT NULL,
+ track_number INT64 NOT NULL,
+ title STRING(MAX) NOT NULL,
+ sample_rate FLOAT64 NOT NULL,
+ created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+) PRIMARY KEY (id, track_number), INTERLEAVE IN PARENT albums ON DELETE CASCADE
+;
+
+CREATE TABLE IF NOT EXISTS venues (
+ id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) PRIMARY KEY,
+ name STRING(MAX) NOT NULL,
+ description JSON NOT NULL,
+ created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+);
+
+CREATE TABLE IF NOT EXISTS concerts (
+ id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) PRIMARY KEY,
+ venue_id INT64 NOT NULL,
+ singer_id INT64 NOT NULL,
+ name STRING(MAX) NOT NULL,
+ start_time TIMESTAMP NOT NULL,
+ end_time TIMESTAMP NOT NULL,
+ created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP),
+ CONSTRAINT fk_concerts_venues FOREIGN KEY (venue_id) REFERENCES venues (id),
+ CONSTRAINT fk_concerts_singers FOREIGN KEY (singer_id) REFERENCES singers (id),
+ CONSTRAINT chk_end_time_after_start_time CHECK (end_time > start_time)
+);
diff --git a/samples/spring-data-mybatis/src/main/resources/drop_schema.sql b/samples/spring-data-mybatis/googlesql/src/main/resources/drop_schema.sql
similarity index 100%
rename from samples/spring-data-mybatis/src/main/resources/drop_schema.sql
rename to samples/spring-data-mybatis/googlesql/src/main/resources/drop_schema.sql
diff --git a/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java b/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java
new file mode 100644
index 000000000..abdfdcdef
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.testcontainers.DockerClientFactory;
+
+@RunWith(JUnit4.class)
+public class ApplicationEmulatorTest {
+
+ @BeforeClass
+ public static void checkDocker() {
+ assumeTrue(
+ "Docker is required for this test", DockerClientFactory.instance().isDockerAvailable());
+ }
+
+ @Test
+ public void testRunApplicationOnEmulator() {
+ System.setProperty("spanner.emulator", "true");
+ System.setProperty("spanner.auto_start_emulator", "true");
+ Application.main(new String[] {});
+ }
+}
diff --git a/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java b/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
new file mode 100644
index 000000000..ade5d85c6
--- /dev/null
+++ b/samples/spring-data-mybatis/googlesql/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
@@ -0,0 +1,1001 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.cloud.spanner.sample;
+
+import static com.google.cloud.spanner.sample.DatabaseSeeder.INITIAL_SINGERS;
+import static com.google.cloud.spanner.sample.DatabaseSeeder.randomDate;
+import static com.google.cloud.spanner.sample.DatabaseSeeder.randomTitle;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractMockServerTest;
+import com.google.cloud.spanner.sample.entities.Singer;
+import com.google.common.collect.Streams;
+import com.google.longrunning.Operation;
+import com.google.protobuf.Any;
+import com.google.protobuf.Empty;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.NullValue;
+import com.google.protobuf.Value;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import com.google.spanner.v1.BeginTransactionRequest;
+import com.google.spanner.v1.CommitRequest;
+import com.google.spanner.v1.ExecuteBatchDmlRequest;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.ResultSet;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.ResultSetStats;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeAnnotationCode;
+import com.google.spanner.v1.TypeCode;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ApplicationTest extends AbstractMockServerTest {
+
+ @BeforeClass
+ public static void setupQueryResults() {
+ // Add a DDL response to the server.
+ addDdlResponseToSpannerAdmin();
+
+ // Set up results for the 'delete all test data' operations.
+ mockSpanner.putStatementResult(
+ StatementResult.update(Statement.of("delete from concerts where true"), 0L));
+ mockSpanner.putStatementResult(
+ StatementResult.update(Statement.of("delete from venues where true"), 0L));
+ mockSpanner.putStatementResult(
+ StatementResult.update(Statement.of("delete from tracks where true"), 0L));
+ mockSpanner.putStatementResult(
+ StatementResult.update(Statement.of("delete from albums where true"), 0L));
+ mockSpanner.putStatementResult(
+ StatementResult.update(Statement.of("delete from singers where true"), 0L));
+
+ // Set up results for inserting test data.
+ for (Singer singer : INITIAL_SINGERS) {
+ mockSpanner.putStatementResult(
+ StatementResult.update(
+ Statement.newBuilder("insert into singers (first_name, last_name) values (@p1, @p2)")
+ .bind("p1")
+ .to(singer.getFirstName())
+ .bind("p2")
+ .to(singer.getLastName())
+ .build(),
+ 1L));
+ }
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of("select id from singers"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .build())
+ .build())
+ .addAllRows(
+ LongStream.rangeClosed(1L, INITIAL_SINGERS.size())
+ .mapToObj(
+ id ->
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(id)))
+ .build())
+ .build())
+ .collect(Collectors.toList()))
+ .build()));
+ mockSpanner.putPartialStatementResult(
+ StatementResult.update(
+ Statement.of(
+ "insert into albums (title, marketing_budget, release_date, cover_picture, singer_id) values (@p1, @p2, @p3, @p4, @p5)"),
+ 1L));
+
+ // Set up results for the queries that the application runs.
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of("SELECT * FROM singers ORDER BY last_name, first_name, id"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("active")
+ .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("last_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("full_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("first_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .build())
+ .build())
+ .addAllRows(
+ Streams.mapWithIndex(
+ INITIAL_SINGERS.stream(),
+ (singer, index) ->
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(index + 1)))
+ .build())
+ .addValues(Value.newBuilder().setBoolValue(true).build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(singer.getLastName())
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ singer.getFirstName() + " " + singer.getLastName())
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(singer.getFirstName())
+ .build())
+ .build())
+ .collect(Collectors.toList()))
+ .build()));
+ mockSpanner.putPartialStatementResult(
+ StatementResult.query(
+ Statement.of("SELECT COUNT(1) FROM albums WHERE singer_id = @p1"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("c")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(Value.newBuilder().setStringValue("10").build())
+ .build())
+ .build()));
+ for (long singerId : LongStream.rangeClosed(1L, INITIAL_SINGERS.size()).toArray()) {
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.newBuilder("SELECT * FROM albums WHERE singer_id = @p1")
+ .bind("p1")
+ .to(Long.reverse(singerId))
+ .build(),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("title")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("singer_id")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("release_date")
+ .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("cover_picture")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.BYTES).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("marketing_budget")
+ .setType(
+ Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)
+ .build())
+ .build())
+ .build())
+ .build())
+ .addAllRows(
+ IntStream.rangeClosed(1, 10)
+ .mapToObj(
+ albumId ->
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ String.valueOf(Long.reverse(albumId * singerId)))
+ .build())
+ .addValues(Value.newBuilder().setStringValue(randomTitle()))
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ String.valueOf(Long.reverse(singerId))))
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(randomDate().toString())
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .build())
+ .collect(Collectors.toList()))
+ .build()));
+ }
+ int singerIndex = ThreadLocalRandom.current().nextInt(INITIAL_SINGERS.size());
+ Singer randomSinger = INITIAL_SINGERS.get(singerIndex);
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of("SELECT * FROM singers TABLESAMPLE RESERVOIR (1 ROWS)"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("active")
+ .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("last_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("full_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("first_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(singerIndex + 1)))
+ .build())
+ .addValues(Value.newBuilder().setBoolValue(true).build())
+ .addValues(
+ Value.newBuilder().setStringValue(randomSinger.getLastName()).build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ randomSinger.getFirstName() + " " + randomSinger.getLastName())
+ .build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(
+ Value.newBuilder().setStringValue(randomSinger.getFirstName()).build())
+ .build())
+ .build()));
+
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.newBuilder(
+ "INSERT INTO singers (first_name, last_name, active) VALUES (@p1, @p2, @p3)\n"
+ + "THEN RETURN *")
+ .bind("p1")
+ .to("Amethyst")
+ .bind("p2")
+ .to("Jiang")
+ .bind("p3")
+ .to((com.google.cloud.spanner.Value) null)
+ .build(),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("active")
+ .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("last_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("full_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("first_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ String.valueOf(Long.reverse(INITIAL_SINGERS.size() + 2)))
+ .build())
+ .addValues(Value.newBuilder().setBoolValue(true).build())
+ .addValues(Value.newBuilder().setStringValue("Amethyst").build())
+ .addValues(Value.newBuilder().setStringValue("Amethyst Jiang").build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setStringValue("Jiang").build())
+ .build())
+ .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+ .build()));
+ mockSpanner.putPartialStatementResult(
+ StatementResult.query(
+ Statement.of(
+ "INSERT INTO albums (title, marketing_budget, release_date, cover_picture, singer_id) VALUES (@p1, @p2, @p3, @p4, @p5)\n"
+ + "THEN RETURN *"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("title")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("singer_id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("release_date")
+ .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("cover_picture")
+ .setType(Type.newBuilder().setCode(TypeCode.BYTES).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("marketing_budget")
+ .setType(
+ Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)
+ .build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(1L)))
+ .build())
+ .addValues(Value.newBuilder().setStringValue(randomTitle()))
+ .addValues(Value.newBuilder().setStringValue(String.valueOf(1L)))
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(
+ Value.newBuilder().setStringValue(randomDate().toString()).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .build())
+ .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+ .build()));
+ mockSpanner.putPartialStatementResult(
+ StatementResult.query(
+ Statement.of(
+ "INSERT INTO tracks (id, track_number, title, sample_rate) VALUES (@p1, @p2, @p3, @p4)\n"
+ + "THEN RETURN *"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("track_number")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("title")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("sample_rate")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.FLOAT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(1L)))
+ .build())
+ .addValues(Value.newBuilder().setStringValue("1").build())
+ .addValues(Value.newBuilder().setStringValue(randomTitle()))
+ .addValues(Value.newBuilder().setNumberValue(3.14d))
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .build())
+ .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+ .build()));
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of("select * from albums limit 1"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("title")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("singer_id")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("release_date")
+ .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("cover_picture")
+ .setType(Type.newBuilder().setCode(TypeCode.BYTES).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("marketing_budget")
+ .setType(
+ Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)
+ .build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(String.valueOf(Long.reverse(1L)))
+ .build())
+ .addValues(Value.newBuilder().setStringValue(randomTitle()))
+ .addValues(Value.newBuilder().setStringValue(String.valueOf(1L)))
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(
+ Value.newBuilder().setStringValue(randomDate().toString()).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .build())
+ .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+ .build()));
+ for (String prefix : new String[] {"J", "A", "B", "C"}) {
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.newBuilder("SELECT * FROM singers WHERE starts_with(last_name, @p1)")
+ .bind("p1")
+ .to(prefix)
+ .build(),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("active")
+ .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("last_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("full_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("first_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .build())
+ .build())
+ .addAllRows(
+ Streams.mapWithIndex(
+ INITIAL_SINGERS.stream()
+ .filter(
+ singer ->
+ singer.getLastName().startsWith(prefix.substring(0, 1))),
+ (singer, index) ->
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ String.valueOf(Long.reverse(index + 1)))
+ .build())
+ .addValues(Value.newBuilder().setBoolValue(true).build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(singer.getLastName())
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ singer.getFirstName()
+ + " "
+ + singer.getLastName())
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build())
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(singer.getFirstName())
+ .build())
+ .build())
+ .collect(Collectors.toList()))
+ .build()));
+ mockSpanner.putPartialStatementResult(
+ StatementResult.query(
+ Statement.of(
+ "INSERT OR UPDATE singers (id, first_name, last_name, active) VALUES (@p1, @p2, @p3, @p4)\n"
+ + "THEN RETURN *"),
+ ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("id")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("active")
+ .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("last_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("full_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("updated_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("created_at")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("first_name")
+ .setType(
+ Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(
+ Value.newBuilder()
+ .setStringValue(
+ String.valueOf(ThreadLocalRandom.current().nextLong()))
+ .build())
+ .addValues(Value.newBuilder().setBoolValue(true).build())
+ .addValues(Value.newBuilder().setStringValue("Russel").build())
+ .addValues(Value.newBuilder().setStringValue("Beatriz Russel").build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
+ .addValues(Value.newBuilder().setStringValue("Beatriz").build())
+ .build())
+ .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+ .build()));
+ }
+ }
+
+ @Test
+ public void testRunApplication() {
+ System.setProperty("spanner.emulator", "false");
+ System.setProperty("spanner.endpoint", "//localhost:" + getPort());
+ System.setProperty("spanner.additional_properties", "usePlainText=true");
+ Application.main(new String[] {});
+
+ assertEquals(
+ 39,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(
+ request ->
+ !request.getSql().equals("SELECT 1")
+ && !request
+ .getSql()
+ .equals(
+ "SELECT * FROM singers ORDER BY sha256(last_name::bytea) LIMIT 1"))
+ .count());
+ assertEquals(3, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+ assertEquals(6, mockSpanner.countRequestsOfType(CommitRequest.class));
+
+ // Verify that the service methods use transactions.
+ String insertSingerSql =
+ "INSERT INTO singers (first_name, last_name, active) VALUES (@p1, @p2, @p3)\nTHEN RETURN *";
+ assertEquals(
+ 1,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertSingerSql))
+ .count());
+ ExecuteSqlRequest insertSingerRequest =
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertSingerSql))
+ .findFirst()
+ .orElseThrow();
+ assertTrue(insertSingerRequest.hasTransaction());
+ assertTrue(insertSingerRequest.getTransaction().hasBegin());
+ assertTrue(insertSingerRequest.getTransaction().getBegin().hasReadWrite());
+ String insertAlbumSql =
+ "INSERT INTO albums (title, marketing_budget, release_date, cover_picture, singer_id) "
+ + "VALUES (@p1, @p2, @p3, @p4, @p5)\nTHEN RETURN *";
+ assertEquals(
+ 4,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertAlbumSql))
+ .count());
+ // The first 3 requests belong to the transaction that is executed together with the 'INSERT
+ // INTO singers' statement.
+ List insertAlbumRequests =
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertAlbumSql))
+ .toList()
+ .subList(0, 3);
+ ExecuteSqlRequest firstInsertAlbumRequest = insertAlbumRequests.get(0);
+ for (ExecuteSqlRequest request : insertAlbumRequests) {
+ assertTrue(request.hasTransaction());
+ assertTrue(request.getTransaction().hasId());
+ assertEquals(
+ firstInsertAlbumRequest.getTransaction().getId(), request.getTransaction().getId());
+ }
+ // Verify that the transaction is committed.
+ assertEquals(
+ 1,
+ mockSpanner.getRequestsOfType(CommitRequest.class).stream()
+ .filter(
+ request ->
+ request
+ .getTransactionId()
+ .equals(firstInsertAlbumRequest.getTransaction().getId()))
+ .count());
+
+ // The last 'INSERT INTO albums' request belong in a transaction with 8 'INSERT INTO tracks'
+ // requests.
+ ExecuteSqlRequest lastInsertAlbumRequest =
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertAlbumSql))
+ .toList()
+ .get(3);
+ assertNotEquals(
+ lastInsertAlbumRequest.getTransaction().getId(),
+ firstInsertAlbumRequest.getTransaction().getId());
+ assertTrue(lastInsertAlbumRequest.hasTransaction());
+ assertTrue(lastInsertAlbumRequest.getTransaction().hasBegin());
+ assertTrue(lastInsertAlbumRequest.getTransaction().getBegin().hasReadWrite());
+ String insertTrackSql =
+ "INSERT INTO tracks (id, track_number, title, sample_rate) "
+ + "VALUES (@p1, @p2, @p3, @p4)\nTHEN RETURN *";
+ assertEquals(
+ 7,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertTrackSql))
+ .count());
+ List insertTrackRequests =
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(insertTrackSql))
+ .toList();
+ for (ExecuteSqlRequest request : insertTrackRequests) {
+ assertTrue(request.hasTransaction());
+ assertTrue(request.getTransaction().hasId());
+ assertEquals(
+ insertTrackRequests.get(0).getTransaction().getId(), request.getTransaction().getId());
+ }
+ // Verify that the transaction is committed.
+ assertEquals(
+ 1,
+ mockSpanner.getRequestsOfType(CommitRequest.class).stream()
+ .filter(
+ request ->
+ request
+ .getTransactionId()
+ .equals(insertTrackRequests.get(0).getTransaction().getId()))
+ .count());
+
+ // Verify that the SingerService#listSingersWithLastNameStartingWith(..) method uses a read-only
+ // transaction.
+ assertEquals(
+ 1,
+ mockSpanner.getRequestsOfType(BeginTransactionRequest.class).stream()
+ .filter(request -> request.getOptions().hasReadOnly())
+ .count());
+ String selectSingersSql = "SELECT * FROM singers WHERE starts_with(last_name, @p1)";
+ assertEquals(
+ 4,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(selectSingersSql))
+ .count());
+ List selectSingersRequests =
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(selectSingersSql))
+ .toList()
+ .subList(1, 4);
+ ExecuteSqlRequest firstSelectSingersRequest = selectSingersRequests.get(0);
+ for (ExecuteSqlRequest request : selectSingersRequests) {
+ assertTrue(request.hasTransaction());
+ assertTrue(request.getTransaction().hasId());
+ }
+ // Verify that the read-only transaction is not committed.
+ assertEquals(
+ 0,
+ mockSpanner.getRequestsOfType(CommitRequest.class).stream()
+ .filter(
+ request ->
+ request
+ .getTransactionId()
+ .equals(firstSelectSingersRequest.getTransaction().getId()))
+ .count());
+ }
+
+ private static void addDdlResponseToSpannerAdmin() {
+ mockDatabaseAdmin.addResponse(
+ Operation.newBuilder()
+ .setDone(true)
+ .setResponse(Any.pack(Empty.getDefaultInstance()))
+ .setMetadata(Any.pack(UpdateDatabaseDdlMetadata.getDefaultInstance()))
+ .build());
+ }
+}
diff --git a/samples/spring-data-mybatis/postgresql/README.md b/samples/spring-data-mybatis/postgresql/README.md
new file mode 100644
index 000000000..61dc46f48
--- /dev/null
+++ b/samples/spring-data-mybatis/postgresql/README.md
@@ -0,0 +1,100 @@
+# Spring Data MyBatis Sample Application with Cloud Spanner PostgreSQL
+
+This sample application shows how to develop portable applications using Spring Data MyBatis in
+combination with Cloud Spanner PostgreSQL. This application can be configured to run on either a
+[Cloud Spanner PostgreSQL](https://cloud.google.com/spanner/docs/postgresql-interface) database or
+an open-source PostgreSQL database. The only change that is needed to switch between the two is
+changing the active Spring profile that is used by the application.
+
+The application uses the Cloud Spanner JDBC driver to connect to Cloud Spanner PostgreSQL, and it
+uses the PostgreSQL JDBC driver to connect to open-source PostgreSQL. Spring Data MyBatis works with
+both drivers and offers a single consistent API to the application developer, regardless of the
+actual database or JDBC driver being used.
+
+This sample shows:
+
+1. How to use Spring Data MyBatis with Cloud Spanner PostgreSQL.
+2. How to develop a portable application that runs on both Google Cloud Spanner PostgreSQL and
+ open-source PostgreSQL with the same code base.
+3. How to use bit-reversed sequences to automatically generate primary key values for entities.
+4. How to use the Spanner Emulator for development in combination with Spring Data.
+
+__NOTE__: This application does __not require PGAdapter__. Instead, it connects to Cloud Spanner
+PostgreSQL using the Cloud Spanner JDBC driver.
+
+## Cloud Spanner PostgreSQL
+
+Cloud Spanner PostgreSQL provides language support by expressing Spanner database functionality
+through a subset of open-source PostgreSQL language constructs, with extensions added to support
+Spanner functionality like interleaved tables and hinting.
+
+The PostgreSQL interface makes the capabilities of Spanner —__fully managed, unlimited scale, strong
+consistency, high performance, and up to 99.999% global availability__— accessible using the
+PostgreSQL dialect. Unlike other services that manage actual PostgreSQL database instances, Spanner
+uses PostgreSQL-compatible syntax to expose its existing scale-out capabilities. This provides
+familiarity for developers and portability for applications, but not 100% PostgreSQL compatibility.
+The SQL syntax that Spanner supports is semantically equivalent PostgreSQL, meaning schemas
+and queries written against the PostgreSQL interface can be easily ported to another PostgreSQL
+environment.
+
+This sample showcases this portability with an application that works on both Cloud Spanner PostgreSQL
+and open-source PostgreSQL with the same code base.
+
+## MyBatis Spring
+[MyBatis Spring](http://mybatis.org/spring/) integrates MyBatis with the popular Java Spring
+framework. This allows MyBatis to participate in Spring transactions and to automatically inject
+MyBatis mappers into other beans.
+
+## Sample Application
+
+This sample shows how to create a portable application using Spring Data MyBatis and the Cloud Spanner
+PostgreSQL dialect. The application works on both Cloud Spanner PostgreSQL and open-source
+PostgreSQL. You can switch between the two by changing the active Spring profile:
+* Profile `cs` runs the application on Cloud Spanner PostgreSQL.
+* Profile `pg` runs the application on open-source PostgreSQL.
+
+The default profile is `cs`. You can change the default profile by modifying the
+[application.properties](src/main/resources/application.properties) file.
+
+### Running the Application
+
+1. Choose the database system that you want to use by choosing a profile. The default profile is
+ `cs`, which runs the application on Cloud Spanner PostgreSQL.
+2. The sample by default starts an instance of the Spanner Emulator together with the application and
+ runs the application against the emulator.
+3. Modify the default profile in the [application.properties](src/main/resources/application.properties)
+ file to run the sample on an open-source PostgreSQL database.
+4. Modify either [application-cs.properties](src/main/resources/application-cs.properties) or
+ [application-pg.properties](src/main/resources/application-pg.properties) to point to an existing
+ database. If you use Cloud Spanner, the database that the configuration file references must be a
+ database that uses the PostgreSQL dialect.
+5. Run the application with `mvn spring-boot:run`.
+
+### Main Application Components
+
+The main application components are:
+* [DatabaseSeeder.java](src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java): This
+ class is responsible for creating the database schema and inserting some initial test data. The
+ schema is created from the [create_schema.sql](src/main/resources/create_schema.sql) file. The
+ `DatabaseSeeder` class loads this file into memory and executes it on the active database using
+ standard JDBC APIs. The class also removes Cloud Spanner-specific extensions to the PostgreSQL
+ dialect when the application runs on open-source PostgreSQL.
+* [JdbcConfiguration.java](src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java):
+ This utility class is used to determine whether the application is running on Cloud Spanner
+ PostgreSQL or open-source PostgreSQL. This can be used if you have specific features that should
+ only be executed on one of the two systems.
+* [EmulatorInitializer.java](src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java):
+ This ApplicationListener automatically starts the Spanner emulator as a Docker container if the
+ sample has been configured to run on the emulator.
+* [AbstractEntity.java](src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java):
+ This is the shared base class for all entities in this sample application. It defines a number of
+ standard attributes, such as the identifier (primary key). The primary key is automatically
+ generated using a (bit-reversed) sequence. [Bit-reversed sequential values](https://cloud.google.com/spanner/docs/schema-design#bit_reverse_primary_key)
+ are considered a good choice for primary keys on Cloud Spanner.
+* [Application.java](src/main/java/com/google/cloud/spanner/sample/Application.java): The starter
+ class of the application. It contains a command-line runner that executes a selection of queries
+ and updates on the database.
+* [SingerService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) and
+ [AlbumService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) are
+ standard Spring service beans that contain business logic that can be executed as transactions.
+ This includes both read/write and read-only transactions.
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/postgresql/pom.xml
similarity index 97%
rename from samples/spring-data-mybatis/pom.xml
rename to samples/spring-data-mybatis/postgresql/pom.xml
index 80ac5eb9a..ccc3499b7 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/postgresql/pom.xml
@@ -5,7 +5,7 @@
4.0.0
org.example
- cloud-spanner-spring-data-mybatis-example
+ cloud-spanner-spring-data-mybatis-postgresql-example
1.0-SNAPSHOT
Sample application showing how to use Spring Data MyBatis with Cloud Spanner PostgreSQL.
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/Application.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/Application.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Album.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Track.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java b/samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
similarity index 100%
rename from samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
rename to samples/spring-data-mybatis/postgresql/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
diff --git a/samples/spring-data-mybatis/src/main/resources/application-cs.properties b/samples/spring-data-mybatis/postgresql/src/main/resources/application-cs.properties
similarity index 100%
rename from samples/spring-data-mybatis/src/main/resources/application-cs.properties
rename to samples/spring-data-mybatis/postgresql/src/main/resources/application-cs.properties
diff --git a/samples/spring-data-mybatis/src/main/resources/application-pg.properties b/samples/spring-data-mybatis/postgresql/src/main/resources/application-pg.properties
similarity index 100%
rename from samples/spring-data-mybatis/src/main/resources/application-pg.properties
rename to samples/spring-data-mybatis/postgresql/src/main/resources/application-pg.properties
diff --git a/samples/spring-data-mybatis/src/main/resources/application.properties b/samples/spring-data-mybatis/postgresql/src/main/resources/application.properties
similarity index 100%
rename from samples/spring-data-mybatis/src/main/resources/application.properties
rename to samples/spring-data-mybatis/postgresql/src/main/resources/application.properties
diff --git a/samples/spring-data-mybatis/src/main/resources/create_schema.sql b/samples/spring-data-mybatis/postgresql/src/main/resources/create_schema.sql
similarity index 100%
rename from samples/spring-data-mybatis/src/main/resources/create_schema.sql
rename to samples/spring-data-mybatis/postgresql/src/main/resources/create_schema.sql
diff --git a/samples/spring-data-mybatis/postgresql/src/main/resources/drop_schema.sql b/samples/spring-data-mybatis/postgresql/src/main/resources/drop_schema.sql
new file mode 100644
index 000000000..23e7b65d3
--- /dev/null
+++ b/samples/spring-data-mybatis/postgresql/src/main/resources/drop_schema.sql
@@ -0,0 +1,5 @@
+drop table if exists concerts;
+drop table if exists venues;
+drop table if exists tracks;
+drop table if exists albums;
+drop table if exists singers;
diff --git a/samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java b/samples/spring-data-mybatis/postgresql/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java
similarity index 100%
rename from samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java
rename to samples/spring-data-mybatis/postgresql/src/test/java/com/google/cloud/spanner/sample/ApplicationEmulatorTest.java
diff --git a/samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java b/samples/spring-data-mybatis/postgresql/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
similarity index 100%
rename from samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
rename to samples/spring-data-mybatis/postgresql/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java