From f0153614998209d387539f85c88bef9f865908b5 Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Tue, 26 Sep 2023 09:36:17 +0000
Subject: [PATCH 01/10] chore(main): release 2.13.3-SNAPSHOT (#1364)
:robot: I have created a release *beep* *boop*
---
### Updating meta-information for bleeding-edge SNAPSHOT release.
---
This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please).
---
pom.xml | 2 +-
samples/snapshot/pom.xml | 2 +-
versions.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/pom.xml b/pom.xml
index 8ea55af4b..ee49db7d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
google-cloud-spanner-jdbc
- 2.13.2
+ 2.13.3-SNAPSHOT
jar
Google Cloud Spanner JDBC
https://github.com/googleapis/java-spanner-jdbc
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 038e5e0d3..ec9a8dd95 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.2
+ 2.13.3-SNAPSHOT
diff --git a/versions.txt b/versions.txt
index fc35a6a01..094a46df1 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
# Format:
# module:released-version:current-version
-google-cloud-spanner-jdbc:2.13.2:2.13.2
+google-cloud-spanner-jdbc:2.13.2:2.13.3-SNAPSHOT
From ce52d07c308bcde0ed1b0c9f4d3556db2590f722 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Tue, 26 Sep 2023 14:45:03 +0200
Subject: [PATCH 02/10] docs: add sample for Spring Data MyBatis (#1352)
* docs: add sample for Spring Data MyBatis
Adds a sample application for using Spring Data and MyBatis with the Cloud Spanner
JDBC driver and a PostgreSQL-dialect database.
* chore: update sample database name
---
samples/spring-data-mybatis/README.md | 93 ++
samples/spring-data-mybatis/pom.xml | 112 +++
.../cloud/spanner/sample/Application.java | 133 +++
.../cloud/spanner/sample/DatabaseSeeder.java | 342 +++++++
.../spanner/sample/JdbcConfiguration.java | 70 ++
.../sample/entities/AbstractEntity.java | 73 ++
.../cloud/spanner/sample/entities/Album.java | 84 ++
.../spanner/sample/entities/Concert.java | 78 ++
.../cloud/spanner/sample/entities/Singer.java | 73 ++
.../cloud/spanner/sample/entities/Track.java | 66 ++
.../cloud/spanner/sample/entities/Venue.java | 43 +
.../spanner/sample/mappers/AlbumMapper.java | 48 +
.../spanner/sample/mappers/ConcertMapper.java | 29 +
.../spanner/sample/mappers/SingerMapper.java | 59 ++
.../spanner/sample/mappers/TrackMapper.java | 37 +
.../spanner/sample/mappers/VenueMapper.java | 29 +
.../spanner/sample/service/AlbumService.java | 49 +
.../spanner/sample/service/SingerService.java | 67 ++
.../main/resources/application-cs.properties | 9 +
.../main/resources/application-pg.properties | 7 +
.../src/main/resources/application.properties | 13 +
.../src/main/resources/create_schema.sql | 68 ++
.../src/main/resources/drop_schema.sql | 5 +
.../cloud/spanner/sample/ApplicationTest.java | 879 ++++++++++++++++++
.../test/resources/application-cs.properties | 9 +
.../test/resources/application-pg.properties | 7 +
.../src/test/resources/application.properties | 13 +
27 files changed, 2495 insertions(+)
create mode 100644 samples/spring-data-mybatis/README.md
create mode 100644 samples/spring-data-mybatis/pom.xml
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
create mode 100644 samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
create mode 100644 samples/spring-data-mybatis/src/main/resources/application-cs.properties
create mode 100644 samples/spring-data-mybatis/src/main/resources/application-pg.properties
create mode 100644 samples/spring-data-mybatis/src/main/resources/application.properties
create mode 100644 samples/spring-data-mybatis/src/main/resources/create_schema.sql
create mode 100644 samples/spring-data-mybatis/src/main/resources/drop_schema.sql
create mode 100644 samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
create mode 100644 samples/spring-data-mybatis/src/test/resources/application-cs.properties
create mode 100644 samples/spring-data-mybatis/src/test/resources/application-pg.properties
create mode 100644 samples/spring-data-mybatis/src/test/resources/application.properties
diff --git a/samples/spring-data-mybatis/README.md b/samples/spring-data-mybatis/README.md
new file mode 100644
index 000000000..02cf135d4
--- /dev/null
+++ b/samples/spring-data-mybatis/README.md
@@ -0,0 +1,93 @@
+# 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.
+
+__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. Modify the default profile in the
+ [application.properties](src/main/resources/application.properties) file.
+2. 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.
+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. 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.
+* [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/pom.xml
new file mode 100644
index 000000000..2d9de56fb
--- /dev/null
+++ b/samples/spring-data-mybatis/pom.xml
@@ -0,0 +1,112 @@
+
+
+ 4.0.0
+
+ org.example
+ cloud-spanner-spring-data-mybatis-example
+ 1.0-SNAPSHOT
+
+ Sample application showing how to use Spring Data MyBatis with Cloud Spanner PostgreSQL.
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.3
+
+
+
+ 17
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+ org.springframework.data
+ spring-data-bom
+ 2023.0.3
+ import
+ pom
+
+
+ com.google.cloud
+ libraries-bom
+ 26.22.0
+ import
+ pom
+
+
+
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 3.0.2
+
+
+ org.mybatis.dynamic-sql
+ mybatis-dynamic-sql
+ 1.5.0
+
+
+
+
+ com.google.cloud
+ google-cloud-spanner-jdbc
+ 2.12.1
+
+
+ org.postgresql
+ postgresql
+ 42.6.0
+
+
+
+ com.google.collections
+ google-collections
+ 1.0
+
+
+
+
+ com.google.cloud
+ google-cloud-spanner
+ test-jar
+ test
+
+
+ com.google.api
+ gax-grpc
+ testlib
+ test
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
+
+
+ com.spotify.fmt
+ fmt-maven-plugin
+ 2.20
+
+
+
+ format
+
+
+
+
+
+
+
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java
new file mode 100644
index 000000000..04286b5a9
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2023 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.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 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) {
+ SpringApplication.run(Application.class, args).close();
+ }
+
+ 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());
+ }
+ }
+}
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
new file mode 100644
index 000000000..eabd04c3b
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2023 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.base.Suppliers;
+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.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Supplier;
+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;
+
+ /** This value is determined once using a system query, and then cached. */
+ private final Supplier isCloudSpannerPG;
+
+ public DatabaseSeeder(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ this.isCloudSpannerPG =
+ Suppliers.memoize(() -> JdbcConfiguration.isCloudSpannerPG(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);
+ }
+ }
+
+ /**
+ * Returns true if this application is currently running on a Cloud Spanner PostgreSQL database,
+ * and false if it is running on an open-source PostgreSQL database.
+ */
+ private boolean isCloudSpanner() {
+ return isCloudSpannerPG.get();
+ }
+
+ /**
+ * Removes all statements that start with a 'skip_on_open_source_pg' comment if the application is
+ * running on open-source PostgreSQL. This ensures that we can use the same DDL script both on
+ * Cloud Spanner and on open-source PostgreSQL. It also removes any empty statements in the given
+ * array.
+ */
+ private String[] updateDdlStatements(String[] statements) {
+ if (!isCloudSpanner()) {
+ for (int i = 0; i < statements.length; i++) {
+ // Replace any line that starts with '/* skip_on_open_source_pg */' with an empty string.
+ statements[i] =
+ statements[i].replaceAll("(?m)^\\s*/\\*\\s*skip_on_open_source_pg\\s*\\*/.+$", "");
+ }
+ }
+ // 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 Cloud 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 Cloud 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",
+ "delete from venues",
+ "delete from tracks",
+ "delete from albums",
+ "delete from singers");
+ }
+
+ /** 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());
+ }
+
+ /** 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/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
new file mode 100644
index 000000000..2398add31
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 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.jdbc.JdbcSqlException;
+import com.google.rpc.Code;
+import java.util.Objects;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.jdbc.core.JdbcOperations;
+
+@Configuration
+public class JdbcConfiguration {
+
+ /** Returns true if the current database is a Cloud Spanner PostgreSQL database. */
+ public static boolean isCloudSpannerPG(JdbcOperations operations) {
+ try {
+ Long value =
+ operations.queryForObject(
+ "select 1 "
+ + "from information_schema.database_options "
+ + "where schema_name='public' "
+ + "and option_name='database_dialect' "
+ + "and option_value='POSTGRESQL'",
+ Long.class);
+ // Shouldn't really be anything else than 1 if the query succeeded, but this avoids complaints
+ // from the compiler.
+ if (Objects.equals(1L, value)) {
+ return true;
+ }
+ } catch (IncorrectResultSizeDataAccessException exception) {
+ // This indicates that it is a valid Cloud Spanner database, but not one that uses the
+ // PostgreSQL dialect.
+ throw new RuntimeException(
+ "The selected Cloud Spanner database does not use the PostgreSQL dialect");
+ } catch (DataAccessException exception) {
+ if (exception.getCause() instanceof JdbcSqlException) {
+ JdbcSqlException jdbcSqlException = (JdbcSqlException) exception.getCause();
+ if (jdbcSqlException.getCode() == Code.PERMISSION_DENIED
+ || jdbcSqlException.getCode() == Code.NOT_FOUND) {
+ throw new RuntimeException(
+ "Failed to get the dialect of the Cloud Spanner database. "
+ + "Please check that the selected database exists and that you have permission to access it. "
+ + "Cause: "
+ + exception.getCause().getMessage(),
+ exception.getCause());
+ }
+ }
+ // ignore and fall through
+ } catch (Throwable exception) {
+ // ignore and fall through
+ }
+ return false;
+ }
+}
diff --git a/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
new file mode 100644
index 000000000..2dbffd0e7
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 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) sequence. */
+ 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/src/main/java/com/google/cloud/spanner/sample/entities/Album.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
new file mode 100644
index 000000000..57df330bf
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
new file mode 100644
index 000000000..fa219b16d
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
new file mode 100644
index 000000000..90dc0e1e0
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/entities/Track.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
new file mode 100644
index 000000000..51fb756c9
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
new file mode 100644
index 000000000..ff7ee5049
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
new file mode 100644
index 000000000..85a05f28e
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/AlbumMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
new file mode 100644
index 000000000..d268b4327
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/ConcertMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
new file mode 100644
index 000000000..7f55466fe
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/SingerMapper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 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 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);
+
+ /** 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/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
new file mode 100644
index 000000000..5972e2711
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/TrackMapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
new file mode 100644
index 000000000..ab81c45cd
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/mappers/VenueMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
new file mode 100644
index 000000000..4e326a47c
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/AlbumService.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
new file mode 100644
index 000000000..6298bb63e
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/service/SingerService.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 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/src/main/resources/application-cs.properties b/samples/spring-data-mybatis/src/main/resources/application-cs.properties
new file mode 100644
index 000000000..cf70836d2
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/resources/application-cs.properties
@@ -0,0 +1,9 @@
+
+# This profile uses a Cloud Spanner PostgreSQL database.
+
+spanner.project=my-project
+spanner.instance=my-instance
+spanner.database=mybatis-sample
+
+spring.datasource.url=jdbc:cloudspanner:/projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database}
+spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
diff --git a/samples/spring-data-mybatis/src/main/resources/application-pg.properties b/samples/spring-data-mybatis/src/main/resources/application-pg.properties
new file mode 100644
index 000000000..0605cd3ab
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/resources/application-pg.properties
@@ -0,0 +1,7 @@
+
+# This profile uses an open-source PostgreSQL database.
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/mybatis-sample
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.username=postgres
+spring.datasource.password=mysecretpassword
diff --git a/samples/spring-data-mybatis/src/main/resources/application.properties b/samples/spring-data-mybatis/src/main/resources/application.properties
new file mode 100644
index 000000000..a6900a8ef
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/resources/application.properties
@@ -0,0 +1,13 @@
+
+# This application can use both a Cloud Spanner PostgreSQL database or an open-source PostgreSQL
+# database. Which database is used is determined by the active profile:
+# 1. 'cs' means use Cloud Spanner.
+# 2. 'pg' means use open-source PostgreSQL.
+
+# Activate the Cloud Spanner profile by default.
+# Change to 'pg' to activate the PostgreSQL profile.
+spring.profiles.default=cs
+
+# 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
diff --git a/samples/spring-data-mybatis/src/main/resources/create_schema.sql b/samples/spring-data-mybatis/src/main/resources/create_schema.sql
new file mode 100644
index 000000000..60552d3ad
--- /dev/null
+++ b/samples/spring-data-mybatis/src/main/resources/create_schema.sql
@@ -0,0 +1,68 @@
+/*
+ This script creates the database schema for this sample application.
+ All lines that start with /* skip_on_open_source_pg */ are skipped when the application is running on a
+ normal PostgreSQL database. The same lines are executed when the application is running on a Cloud
+ Spanner database. The script is executed by the DatabaseSeeder class.
+*/
+
+create sequence if not exists id_generator
+/* skip_on_open_source_pg */ bit_reversed_positive
+;
+
+create table if not exists singers (
+ id bigint not null primary key default nextval('id_generator'),
+ first_name varchar,
+ last_name varchar,
+ full_name varchar generated always 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 boolean default true,
+ created_at timestamptz default current_timestamp,
+ updated_at timestamptz default current_timestamp
+);
+
+create table if not exists albums (
+ id bigint not null primary key default nextval('id_generator'),
+ title varchar not null,
+ marketing_budget numeric,
+ release_date date,
+ cover_picture bytea,
+ singer_id bigint not null,
+ created_at timestamptz default current_timestamp,
+ updated_at timestamptz default current_timestamp,
+ constraint fk_albums_singers foreign key (singer_id) references singers (id)
+);
+
+create table if not exists tracks (
+ id bigint not null,
+ track_number bigint not null,
+ title varchar not null,
+ sample_rate float8 not null,
+ created_at timestamptz default current_timestamp,
+ updated_at timestamptz default current_timestamp,
+ primary key (id, track_number)
+)
+/* skip_on_open_source_pg */ interleave in parent albums on delete cascade
+;
+
+create table if not exists venues (
+ id bigint not null primary key default nextval('id_generator'),
+ name varchar not null,
+ description jsonb not null,
+ created_at timestamptz default current_timestamp,
+ updated_at timestamptz default current_timestamp
+);
+
+create table if not exists concerts (
+ id bigint not null primary key default nextval('id_generator'),
+ venue_id bigint not null,
+ singer_id bigint not null,
+ name varchar not null,
+ start_time timestamptz not null,
+ end_time timestamptz not null,
+ created_at timestamptz default current_timestamp,
+ updated_at timestamptz 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/src/main/resources/drop_schema.sql
new file mode 100644
index 000000000..23e7b65d3
--- /dev/null
+++ b/samples/spring-data-mybatis/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/ApplicationTest.java b/samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
new file mode 100644
index 000000000..677e086ec
--- /dev/null
+++ b/samples/spring-data-mybatis/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright 2023 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.Dialect;
+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.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;
+import org.springframework.boot.SpringApplication;
+
+@RunWith(JUnit4.class)
+public class ApplicationTest extends AbstractMockServerTest {
+
+ @BeforeClass
+ public static void setupQueryResults() {
+ // Set the database dialect.
+ mockSpanner.putStatementResult(StatementResult.detectDialectResult(Dialect.POSTGRESQL));
+ // Set up a result for the dialect check that is executed by the JdbcConfiguration class.
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of(
+ "select 1 "
+ + "from information_schema.database_options "
+ + "where schema_name='public' "
+ + "and option_name='database_dialect' "
+ + "and option_value='POSTGRESQL'"),
+ 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("1").build())
+ .build())
+ .build()));
+ // 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"), 0L));
+ mockSpanner.putStatementResult(StatementResult.update(Statement.of("delete from venues"), 0L));
+ mockSpanner.putStatementResult(StatementResult.update(Statement.of("delete from tracks"), 0L));
+ mockSpanner.putStatementResult(StatementResult.update(Statement.of("delete from albums"), 0L));
+ mockSpanner.putStatementResult(StatementResult.update(Statement.of("delete from singers"), 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 ($1, $2)")
+ .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 ($1, $2, $3, $4, $5)"),
+ 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 = $1"),
+ 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 = $1")
+ .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()));
+ }
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.newBuilder(
+ "INSERT INTO singers (first_name, last_name, active) VALUES ($1, $2, $3)\n"
+ + "RETURNING *")
+ .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 ($1, $2, $3, $4, $5)\n"
+ + "RETURNING *"),
+ 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 ($1, $2, $3, $4)\n"
+ + "RETURNING *"),
+ 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, $1)")
+ .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()));
+ }
+ }
+
+ @Test
+ public void testRunApplication() {
+ System.setProperty("port", String.valueOf(getPort()));
+ SpringApplication.run(Application.class).close();
+
+ assertEquals(
+ 39,
+ mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> !request.getSql().equals("SELECT 1"))
+ .count());
+ assertEquals(3, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+ assertEquals(5, mockSpanner.countRequestsOfType(CommitRequest.class));
+
+ // Verify that the service methods use transactions.
+ String insertSingerSql =
+ "INSERT INTO singers (first_name, last_name, active) VALUES ($1, $2, $3)\nRETURNING *";
+ 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 ($1, $2, $3, $4, $5)\nRETURNING *";
+ 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 ($1, $2, $3, $4)\nRETURNING *";
+ 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, $1)";
+ 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/src/test/resources/application-cs.properties b/samples/spring-data-mybatis/src/test/resources/application-cs.properties
new file mode 100644
index 000000000..05f7cfa92
--- /dev/null
+++ b/samples/spring-data-mybatis/src/test/resources/application-cs.properties
@@ -0,0 +1,9 @@
+
+# This profile uses a Cloud Spanner PostgreSQL database.
+
+spanner.project=my-project
+spanner.instance=my-instance
+spanner.database=spring-data-jdbc
+
+spring.datasource.url=jdbc:cloudspanner://localhost:${port}/projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database}?usePlainText=true
+spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
diff --git a/samples/spring-data-mybatis/src/test/resources/application-pg.properties b/samples/spring-data-mybatis/src/test/resources/application-pg.properties
new file mode 100644
index 000000000..894f63eba
--- /dev/null
+++ b/samples/spring-data-mybatis/src/test/resources/application-pg.properties
@@ -0,0 +1,7 @@
+
+# This profile uses an open-source PostgreSQL database.
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/spring-data-jdbc
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.username=postgres
+spring.datasource.password=mysecretpassword
diff --git a/samples/spring-data-mybatis/src/test/resources/application.properties b/samples/spring-data-mybatis/src/test/resources/application.properties
new file mode 100644
index 000000000..a6900a8ef
--- /dev/null
+++ b/samples/spring-data-mybatis/src/test/resources/application.properties
@@ -0,0 +1,13 @@
+
+# This application can use both a Cloud Spanner PostgreSQL database or an open-source PostgreSQL
+# database. Which database is used is determined by the active profile:
+# 1. 'cs' means use Cloud Spanner.
+# 2. 'pg' means use open-source PostgreSQL.
+
+# Activate the Cloud Spanner profile by default.
+# Change to 'pg' to activate the PostgreSQL profile.
+spring.profiles.default=cs
+
+# 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
From 749d2c3698c900560b6f85247b0a41a85cd55ac8 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 16:06:13 +0200
Subject: [PATCH 03/10] deps: update dependency
org.springframework.boot:spring-boot-starter-parent to v3.1.4 (#1366)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.springframework.boot:spring-boot-starter-parent](https://spring.io/projects/spring-boot) ([source](https://togithub.com/spring-projects/spring-boot)) | `3.1.3` -> `3.1.4` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
spring-projects/spring-boot (org.springframework.boot:spring-boot-starter-parent)
### [`v3.1.4`](https://togithub.com/spring-projects/spring-boot/releases/tag/v3.1.4)
[Compare Source](https://togithub.com/spring-projects/spring-boot/compare/v3.1.3...v3.1.4)
##### :star: New Features
- Add TWENTY_ONE to JavaVersion enum [#37364](https://togithub.com/spring-projects/spring-boot/issues/37364)
##### :lady_beetle: Bug Fixes
- When SLF4J and Logback are initialized on multiple threads in parallel, startup may fail due to SubstituteLoggerFactory being considered to be a competing LoggerFactory implementation [#37484](https://togithub.com/spring-projects/spring-boot/issues/37484)
- Saml2RelyingPartyAutoConfiguration ignores `sign-request` when `metadata-url` is used [#37482](https://togithub.com/spring-projects/spring-boot/issues/37482)
- Leaking file descriptor / socket within DomainSocket tooling [#37460](https://togithub.com/spring-projects/spring-boot/issues/37460)
- Invalid Accept header produces HTTP 500 in WelcomePageHandlerMapping [#37457](https://togithub.com/spring-projects/spring-boot/issues/37457)
- PrivateKeyParser doesn't support ed448, XDH and RSA-PSS keys [#37422](https://togithub.com/spring-projects/spring-boot/issues/37422)
- "languageVersion is final and cannot be changed" when using Gradle 8.3 and configuring the Java toolchain's language version [#37380](https://togithub.com/spring-projects/spring-boot/issues/37380)
- AOT processing fails when a `@ConfigurationProperties-annotated` record has multiple constructors [#37336](https://togithub.com/spring-projects/spring-boot/issues/37336)
- Spring Boot dependency management not working for ehcache when using Gradle and the dependency management plugin [#37270](https://togithub.com/spring-projects/spring-boot/issues/37270)
- SslStoreBundle implementations aren't immutable [#37222](https://togithub.com/spring-projects/spring-boot/issues/37222)
- Parsing OCI image names that are invalid due to the use of upper case letters is very slow [#37183](https://togithub.com/spring-projects/spring-boot/issues/37183)
- Producing and consuming different tracing propagation formats doesn't work [#37178](https://togithub.com/spring-projects/spring-boot/issues/37178)
- Using https with elliptic curves other than secp384r1 fails [#37169](https://togithub.com/spring-projects/spring-boot/issues/37169)
- In 3.0.x and later, Spring Security cannot be used to secure a WebSocket upgrade request when using Jetty [#37158](https://togithub.com/spring-projects/spring-boot/issues/37158)
- Local baggage is propagated when using Brave and W3C [#37156](https://togithub.com/spring-projects/spring-boot/issues/37156)
- ServiceConnectionContextCustomizer can trigger docker usage during AOT processing [#37097](https://togithub.com/spring-projects/spring-boot/issues/37097)
- java.lang.OutOfMemoryError: Metaspace when repeatedly deploying and undeploying a Spring Boot web application multiple times in Tomcat [#37096](https://togithub.com/spring-projects/spring-boot/issues/37096)
- Property 'logging.threshold.console' not working [#36741](https://togithub.com/spring-projects/spring-boot/issues/36741)
##### :notebook_with_decorative_cover: Documentation
- Document that PKCS8 PEM files should be used whenever possible [#37443](https://togithub.com/spring-projects/spring-boot/issues/37443)
- Add reference to Oracle Spring Boot Starters [#37411](https://togithub.com/spring-projects/spring-boot/issues/37411)
- Correct the description of spring.artemis.broker-url [#37309](https://togithub.com/spring-projects/spring-boot/issues/37309)
- Add default value metadata for management.metrics.export.signalfx.published-histogram-type [#37253](https://togithub.com/spring-projects/spring-boot/issues/37253)
- Polish javadoc [#37143](https://togithub.com/spring-projects/spring-boot/issues/37143)
##### :hammer: Dependency Upgrades
- Upgrade to Byte Buddy 1.14.8 [#37419](https://togithub.com/spring-projects/spring-boot/issues/37419)
- Upgrade to Couchbase Client 3.4.10 [#37297](https://togithub.com/spring-projects/spring-boot/issues/37297)
- Upgrade to Groovy 4.0.15 [#37386](https://togithub.com/spring-projects/spring-boot/issues/37386)
- Upgrade to Hibernate 6.2.9.Final [#37465](https://togithub.com/spring-projects/spring-boot/issues/37465)
- Upgrade to Infinispan 14.0.17.Final [#37299](https://togithub.com/spring-projects/spring-boot/issues/37299)
- Upgrade to Jakarta XML Bind 4.0.1 [#37387](https://togithub.com/spring-projects/spring-boot/issues/37387)
- Upgrade to Jetty 11.0.16 [#37300](https://togithub.com/spring-projects/spring-boot/issues/37300)
- Upgrade to Lombok 1.18.30 [#37488](https://togithub.com/spring-projects/spring-boot/issues/37488)
- Upgrade to Micrometer 1.11.4 [#37261](https://togithub.com/spring-projects/spring-boot/issues/37261)
- Upgrade to Micrometer Tracing 1.1.5 [#37262](https://togithub.com/spring-projects/spring-boot/issues/37262)
- Upgrade to Native Build Tools Plugin 0.9.27 [#37420](https://togithub.com/spring-projects/spring-boot/issues/37420)
- Upgrade to Neo4j Java Driver 5.12.0 [#37353](https://togithub.com/spring-projects/spring-boot/issues/37353)
- Upgrade to Pooled JMS 3.1.3 [#37421](https://togithub.com/spring-projects/spring-boot/issues/37421)
- Upgrade to R2DBC MySQL 1.0.3 [#37466](https://togithub.com/spring-projects/spring-boot/issues/37466)
- Upgrade to Reactor Bom 2022.0.11 [#37263](https://togithub.com/spring-projects/spring-boot/issues/37263)
- Upgrade to REST Assured 5.3.2 [#37303](https://togithub.com/spring-projects/spring-boot/issues/37303)
- Upgrade to SLF4J 2.0.9 [#37304](https://togithub.com/spring-projects/spring-boot/issues/37304)
- Upgrade to Spring AMQP 3.0.9 [#37264](https://togithub.com/spring-projects/spring-boot/issues/37264)
- Upgrade to Spring Data Bom 2023.0.4 [#37350](https://togithub.com/spring-projects/spring-boot/issues/37350)
- Upgrade to Spring Framework 6.0.12 [#37265](https://togithub.com/spring-projects/spring-boot/issues/37265)
- Upgrade to Spring GraphQL 1.2.3 [#37266](https://togithub.com/spring-projects/spring-boot/issues/37266)
- Upgrade to Spring Integration 6.1.3 [#37267](https://togithub.com/spring-projects/spring-boot/issues/37267)
- Upgrade to Spring Kafka 3.0.11 [#37305](https://togithub.com/spring-projects/spring-boot/issues/37305)
- Upgrade to Spring Retry 2.0.3 [#37280](https://togithub.com/spring-projects/spring-boot/issues/37280)
- Upgrade to Spring Security 6.1.4 [#37424](https://togithub.com/spring-projects/spring-boot/issues/37424)
- Upgrade to Spring WS 4.0.6 [#37425](https://togithub.com/spring-projects/spring-boot/issues/37425)
- Upgrade to Tomcat 10.1.13 [#37306](https://togithub.com/spring-projects/spring-boot/issues/37306)
##### :heart: Contributors
Thank you to all the contributors who worked on this release:
[@Eng-Fouad](https://togithub.com/Eng-Fouad), [@dependabot](https://togithub.com/dependabot)\[bot], [@izeye](https://togithub.com/izeye), [@markxnelson](https://togithub.com/markxnelson), [@mdeinum](https://togithub.com/mdeinum), and [@quaff](https://togithub.com/quaff)
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc).
---
samples/spring-data-mybatis/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 2d9de56fb..e522b1756 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -13,7 +13,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.3
+ 3.1.4
From 8db200cdae34c5b7e85fbf9ddf989bf4b39f9e90 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 16:08:14 +0200
Subject: [PATCH 04/10] chore(deps): update dependency
com.google.cloud:google-cloud-spanner-jdbc to v2.13.2 (#1365)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.cloud:google-cloud-spanner-jdbc](https://togithub.com/googleapis/java-spanner-jdbc) | `2.12.1` -> `2.13.2` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
| [com.google.cloud:google-cloud-spanner-jdbc](https://togithub.com/googleapis/java-spanner-jdbc) | `2.13.1` -> `2.13.2` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
googleapis/java-spanner-jdbc (com.google.cloud:google-cloud-spanner-jdbc)
### [`v2.13.2`](https://togithub.com/googleapis/java-spanner-jdbc/blob/HEAD/CHANGELOG.md#2132-2023-09-26)
[Compare Source](https://togithub.com/googleapis/java-spanner-jdbc/compare/v2.13.1...v2.13.2)
##### Dependencies
- Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.16.0 ([#1358](https://togithub.com/googleapis/java-spanner-jdbc/issues/1358)) ([c4c4925](https://togithub.com/googleapis/java-spanner-jdbc/commit/c4c492576d3e6c192a1855e8d6b3474bb2ad0c22))
- Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.16.1 ([#1363](https://togithub.com/googleapis/java-spanner-jdbc/issues/1363)) ([d574dbb](https://togithub.com/googleapis/java-spanner-jdbc/commit/d574dbb761fa7d0a7d1977844b48b8e4904f1bb0))
- Update dependency com.spotify.fmt:fmt-maven-plugin to v2.21.1 ([#1359](https://togithub.com/googleapis/java-spanner-jdbc/issues/1359)) ([70af99e](https://togithub.com/googleapis/java-spanner-jdbc/commit/70af99e96451fb0158abb45580eaae09ad0b6210))
### [`v2.13.1`](https://togithub.com/googleapis/java-spanner-jdbc/blob/HEAD/CHANGELOG.md#2131-2023-09-21)
[Compare Source](https://togithub.com/googleapis/java-spanner-jdbc/compare/v2.13.0...v2.13.1)
##### Dependencies
- Update dependency org.springframework.boot:spring-boot-starter-data-jdbc to v3.1.4 ([#1353](https://togithub.com/googleapis/java-spanner-jdbc/issues/1353)) ([88cd905](https://togithub.com/googleapis/java-spanner-jdbc/commit/88cd905bece9c8da7f26b637392e35ab2536edeb))
### [`v2.13.0`](https://togithub.com/googleapis/java-spanner-jdbc/blob/HEAD/CHANGELOG.md#2130-2023-09-15)
[Compare Source](https://togithub.com/googleapis/java-spanner-jdbc/compare/v2.12.1...v2.13.0)
##### Features
- Support partitioned queries ([#1300](https://togithub.com/googleapis/java-spanner-jdbc/issues/1300)) ([c50da41](https://togithub.com/googleapis/java-spanner-jdbc/commit/c50da41e688ff48f8967c0f114f5bac8eaac49f9))
##### Bug Fixes
- Comments should be sent to Spanner for PostgreSQL databases ([#1331](https://togithub.com/googleapis/java-spanner-jdbc/issues/1331)) ([7c9e781](https://togithub.com/googleapis/java-spanner-jdbc/commit/7c9e781bf45b112266e278e1df1586e56043698e))
##### Documentation
- Create Spring Data JDBC sample ([#1334](https://togithub.com/googleapis/java-spanner-jdbc/issues/1334)) ([cefea55](https://togithub.com/googleapis/java-spanner-jdbc/commit/cefea55086eb191f71a1a493e046cb136f9f9f87))
##### Dependencies
- Update actions/checkout action to v4 - abandoned ([#1333](https://togithub.com/googleapis/java-spanner-jdbc/issues/1333)) ([ce82b42](https://togithub.com/googleapis/java-spanner-jdbc/commit/ce82b42d3abb8de0f8b3ee2915c2008673775ea1))
- Update dependency org.springframework.data:spring-data-bom to v2023.0.4 ([#1347](https://togithub.com/googleapis/java-spanner-jdbc/issues/1347)) ([893f61a](https://togithub.com/googleapis/java-spanner-jdbc/commit/893f61ab04e32c690f1ff9fc813bd2ba6ebca328))
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.
---
- [ ] If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc).
---
samples/install-without-bom/pom.xml | 2 +-
samples/spring-data-jdbc/pom.xml | 2 +-
samples/spring-data-mybatis/pom.xml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index ed2d67e79..015e8b35d 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.1
+ 2.13.2
diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml
index 557270816..44d3da890 100644
--- a/samples/spring-data-jdbc/pom.xml
+++ b/samples/spring-data-jdbc/pom.xml
@@ -48,7 +48,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.1
+ 2.13.2
org.postgresql
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index e522b1756..d97a09d24 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -58,7 +58,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.12.1
+ 2.13.2
org.postgresql
From 916ad4a9e07b3afc15e53664f175db9e58f06376 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 16:12:13 +0200
Subject: [PATCH 05/10] deps: update dependency
org.springframework.data:spring-data-bom to v2023.0.4 (#1367)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.springframework.data:spring-data-bom](https://togithub.com/spring-projects/spring-data-bom) | `2023.0.3` -> `2023.0.4` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
spring-projects/spring-data-bom (org.springframework.data:spring-data-bom)
### [`v2023.0.4`](https://togithub.com/spring-projects/spring-data-bom/releases/tag/2023.0.4)
[Compare Source](https://togithub.com/spring-projects/spring-data-bom/compare/2023.0.3...2023.0.4)
#### :shipit: Participating Modules
- [Spring Data BOM 2023.0.4](https://togithub.com/spring-projects/spring-data-bom/releases/tag/2023.0.4)
- [Spring Data Build 3.1.4](https://togithub.com/spring-projects/spring-data-build/releases/tag/3.1.4)
- [Spring Data Cassandra 4.1.4](https://togithub.com/spring-projects/spring-data-cassandra/releases/tag/4.1.4)
- [Spring Data Commons 3.1.4](https://togithub.com/spring-projects/spring-data-commons/releases/tag/3.1.4)
- [Spring Data Couchbase 5.1.4](https://togithub.com/spring-projects/spring-data-couchbase/releases/tag/5.1.4)
- [Spring Data Elasticsearch 5.1.4](https://togithub.com/spring-projects/spring-data-elasticsearch/releases/tag/5.1.4)
- [Spring Data JPA 3.1.4](https://togithub.com/spring-projects/spring-data-jpa/releases/tag/3.1.4)
- [Spring Data KeyValue 3.1.4](https://togithub.com/spring-projects/spring-data-keyvalue/releases/tag/3.1.4)
- [Spring Data LDAP 3.1.4](https://togithub.com/spring-projects/spring-data-ldap/releases/tag/3.1.4)
- [Spring Data MongoDB 4.1.4](https://togithub.com/spring-projects/spring-data-mongodb/releases/tag/4.1.4)
- [Spring Data Neo4j 7.1.4](https://togithub.com/spring-projects/spring-data-neo4j/releases/tag/7.1.4)
- [Spring Data REST 4.1.4](https://togithub.com/spring-projects/spring-data-rest/releases/tag/4.1.4)
- [Spring Data Redis 3.1.4](https://togithub.com/spring-projects/spring-data-redis/releases/tag/3.1.4)
- [Spring Data Relational 3.1.4](https://togithub.com/spring-projects/spring-data-relational/releases/tag/3.1.4)
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc).
---
samples/spring-data-mybatis/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index d97a09d24..68026c1a2 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -28,7 +28,7 @@
org.springframework.data
spring-data-bom
- 2023.0.3
+ 2023.0.4
import
pom
From d59c8f553be17136e91dcd133bee310cff9fcf94 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 17:02:06 +0200
Subject: [PATCH 06/10] chore(deps): update dependency
com.google.cloud:libraries-bom to v26.23.0 (#1369)
---
samples/spring-data-mybatis/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 68026c1a2..a350dc1dd 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -35,7 +35,7 @@
com.google.cloud
libraries-bom
- 26.22.0
+ 26.23.0
import
pom
From 376e1c3ccdd71351a5d6151ce19b9f88df163776 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 17:02:29 +0200
Subject: [PATCH 07/10] deps: update dependency
com.google.cloud:google-cloud-spanner-bom to v6.48.0 (#1370)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index ee49db7d9..ba8863857 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,7 +62,7 @@
com.google.cloud
google-cloud-spanner-bom
- 6.47.0
+ 6.48.0
pom
import
From bf64add3e9ce8148d2fc3ad010b8abd446208e4f Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 26 Sep 2023 17:28:16 +0200
Subject: [PATCH 08/10] deps: update dependency
com.spotify.fmt:fmt-maven-plugin to v2.21.1 (#1372)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.spotify.fmt:fmt-maven-plugin](https://togithub.com/spotify/fmt-maven-plugin) | `2.20` -> `2.21.1` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
spotify/fmt-maven-plugin (com.spotify.fmt:fmt-maven-plugin)
### [`v2.21.1`](https://togithub.com/spotify/fmt-maven-plugin/releases/tag/2.21.1)
[Compare Source](https://togithub.com/spotify/fmt-maven-plugin/compare/2.21...2.21.1)
#### What's Changed
- Upgrade dependencies by [@caesar-ralf](https://togithub.com/caesar-ralf) in [https://togithub.com/spotify/fmt-maven-plugin/pull/180](https://togithub.com/spotify/fmt-maven-plugin/pull/180)
**Full Changelog**: https://togithub.com/spotify/fmt-maven-plugin/compare/2.21...2.21.1
### [`v2.21`](https://togithub.com/spotify/fmt-maven-plugin/releases/tag/2.21)
#### What's Changed
- Use 2.19 for formatting code in this repo by [@klaraward](https://togithub.com/klaraward) in [https://togithub.com/spotify/fmt-maven-plugin/pull/154](https://togithub.com/spotify/fmt-maven-plugin/pull/154)
- Add metadata for Spotify OSS by [@klaraward](https://togithub.com/klaraward) in [https://togithub.com/spotify/fmt-maven-plugin/pull/155](https://togithub.com/spotify/fmt-maven-plugin/pull/155)
- check release in Central instead of search by [@hboutemy](https://togithub.com/hboutemy) in [https://togithub.com/spotify/fmt-maven-plugin/pull/156](https://togithub.com/spotify/fmt-maven-plugin/pull/156)
- configure for Reproducible Builds by [@hboutemy](https://togithub.com/hboutemy) in [https://togithub.com/spotify/fmt-maven-plugin/pull/157](https://togithub.com/spotify/fmt-maven-plugin/pull/157)
- Allow skipping of sourceDirectory or testSourceDirectory by [@camac](https://togithub.com/camac) in [https://togithub.com/spotify/fmt-maven-plugin/pull/128](https://togithub.com/spotify/fmt-maven-plugin/pull/128)
- \[Snyk] Upgrade org.apache.maven.plugin-tools:maven-plugin-annotations from 3.4 to 3.7.1 by [@snyk-bot](https://togithub.com/snyk-bot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/162](https://togithub.com/spotify/fmt-maven-plugin/pull/162)
- \[Snyk] Upgrade io.norberg:auto-matter from 0.25.1 to 0.26.1 by [@snyk-bot](https://togithub.com/snyk-bot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/163](https://togithub.com/spotify/fmt-maven-plugin/pull/163)
- \[Snyk] Upgrade org.apache.maven:maven-plugin-api from 3.8.4 to 3.8.7 by [@snyk-bot](https://togithub.com/snyk-bot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/164](https://togithub.com/spotify/fmt-maven-plugin/pull/164)
- \[Snyk] Upgrade org.apache.maven.plugin-tools:maven-plugin-annotations from 3.7.1 to 3.8.1 by [@perploug](https://togithub.com/perploug) in [https://togithub.com/spotify/fmt-maven-plugin/pull/169](https://togithub.com/spotify/fmt-maven-plugin/pull/169)
- \[Snyk] Upgrade io.norberg:auto-matter from 0.26.1 to 0.26.2 by [@perploug](https://togithub.com/perploug) in [https://togithub.com/spotify/fmt-maven-plugin/pull/173](https://togithub.com/spotify/fmt-maven-plugin/pull/173)
- \[Snyk] Upgrade org.apache.maven.plugin-tools:maven-plugin-annotations from 3.8.1 to 3.8.2 by [@snyk-bot](https://togithub.com/snyk-bot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/174](https://togithub.com/spotify/fmt-maven-plugin/pull/174)
- Bump org.apache.maven:maven-core from 3.3.9 to 3.8.1 by [@dependabot](https://togithub.com/dependabot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/177](https://togithub.com/spotify/fmt-maven-plugin/pull/177)
- \[Snyk] Upgrade org.apache.maven:maven-plugin-api from 3.8.7 to 3.9.0 by [@snyk-bot](https://togithub.com/snyk-bot) in [https://togithub.com/spotify/fmt-maven-plugin/pull/167](https://togithub.com/spotify/fmt-maven-plugin/pull/167)
- Support java 21 by [@caesar-ralf](https://togithub.com/caesar-ralf) in [https://togithub.com/spotify/fmt-maven-plugin/pull/179](https://togithub.com/spotify/fmt-maven-plugin/pull/179)
#### New Contributors
- [@hboutemy](https://togithub.com/hboutemy) made their first contribution in [https://togithub.com/spotify/fmt-maven-plugin/pull/156](https://togithub.com/spotify/fmt-maven-plugin/pull/156)
- [@caesar-ralf](https://togithub.com/caesar-ralf) made their first contribution in [https://togithub.com/spotify/fmt-maven-plugin/pull/179](https://togithub.com/spotify/fmt-maven-plugin/pull/179)
**Full Changelog**: https://togithub.com/spotify/fmt-maven-plugin/compare/2.19.0...2.21
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc).
---
samples/spring-data-mybatis/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index a350dc1dd..615d6ca27 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -98,7 +98,7 @@
com.spotify.fmt
fmt-maven-plugin
- 2.20
+ 2.21.1
From b30e391792f2c2811038b35a065b35104bc614e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Wed, 27 Sep 2023 10:48:13 +0200
Subject: [PATCH 09/10] deps: remove specific JDBC version from samples (#1371)
- Removes the specific JDBC version from the samples. This is no longer needed, as the BOM now contains a JDBC driver version that is up-to-date enough to work with these samples (they require RETURN_GENERATED_KEYS).
- Adds a GitHub Actions workflow for testing the MyBatis sample.
---
.../workflows/spring-data-mybatis-sample.yaml | 30 +++++++++++++++++++
samples/spring-data-jdbc/pom.xml | 1 -
samples/spring-data-mybatis/pom.xml | 1 -
3 files changed, 30 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/spring-data-mybatis-sample.yaml
diff --git a/.github/workflows/spring-data-mybatis-sample.yaml b/.github/workflows/spring-data-mybatis-sample.yaml
new file mode 100644
index 000000000..39be0e6e6
--- /dev/null
+++ b/.github/workflows/spring-data-mybatis-sample.yaml
@@ -0,0 +1,30 @@
+# Copyright 2023 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.
+# Github action job to test core java library features on
+# downstream client libraries before they are released.
+on:
+ pull_request:
+name: spring-data-mybatis-sample
+jobs:
+ spring-data-jdbc:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+ - name: Run tests
+ run: mvn test
+ working-directory: samples/spring-data-mybatis
diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml
index 44d3da890..20f858d95 100644
--- a/samples/spring-data-jdbc/pom.xml
+++ b/samples/spring-data-jdbc/pom.xml
@@ -48,7 +48,6 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.2
org.postgresql
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 615d6ca27..07563c648 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -58,7 +58,6 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.2
org.postgresql
From 4372838caf5a1c9c8b173e32d373b4d8a25699a1 Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Wed, 27 Sep 2023 13:16:14 +0000
Subject: [PATCH 10/10] chore(main): release 2.13.3 (#1368)
:robot: I have created a release *beep* *boop*
---
## [2.13.3](https://togithub.com/googleapis/java-spanner-jdbc/compare/v2.13.2...v2.13.3) (2023-09-27)
### Dependencies
* Remove specific JDBC version from samples ([#1371](https://togithub.com/googleapis/java-spanner-jdbc/issues/1371)) ([b30e391](https://togithub.com/googleapis/java-spanner-jdbc/commit/b30e391792f2c2811038b35a065b35104bc614e7))
* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.48.0 ([#1370](https://togithub.com/googleapis/java-spanner-jdbc/issues/1370)) ([376e1c3](https://togithub.com/googleapis/java-spanner-jdbc/commit/376e1c3ccdd71351a5d6151ce19b9f88df163776))
* Update dependency com.spotify.fmt:fmt-maven-plugin to v2.21.1 ([#1372](https://togithub.com/googleapis/java-spanner-jdbc/issues/1372)) ([bf64add](https://togithub.com/googleapis/java-spanner-jdbc/commit/bf64add3e9ce8148d2fc3ad010b8abd446208e4f))
* Update dependency org.springframework.boot:spring-boot-starter-parent to v3.1.4 ([#1366](https://togithub.com/googleapis/java-spanner-jdbc/issues/1366)) ([749d2c3](https://togithub.com/googleapis/java-spanner-jdbc/commit/749d2c3698c900560b6f85247b0a41a85cd55ac8))
* Update dependency org.springframework.data:spring-data-bom to v2023.0.4 ([#1367](https://togithub.com/googleapis/java-spanner-jdbc/issues/1367)) ([916ad4a](https://togithub.com/googleapis/java-spanner-jdbc/commit/916ad4a9e07b3afc15e53664f175db9e58f06376))
### Documentation
* Add sample for Spring Data MyBatis ([#1352](https://togithub.com/googleapis/java-spanner-jdbc/issues/1352)) ([ce52d07](https://togithub.com/googleapis/java-spanner-jdbc/commit/ce52d07c308bcde0ed1b0c9f4d3556db2590f722))
---
This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please).
---
CHANGELOG.md | 16 ++++++++++++++++
pom.xml | 2 +-
samples/snapshot/pom.xml | 2 +-
versions.txt | 2 +-
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 659f4d353..a6db5acc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## [2.13.3](https://github.com/googleapis/java-spanner-jdbc/compare/v2.13.2...v2.13.3) (2023-09-27)
+
+
+### Dependencies
+
+* Remove specific JDBC version from samples ([#1371](https://github.com/googleapis/java-spanner-jdbc/issues/1371)) ([b30e391](https://github.com/googleapis/java-spanner-jdbc/commit/b30e391792f2c2811038b35a065b35104bc614e7))
+* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.48.0 ([#1370](https://github.com/googleapis/java-spanner-jdbc/issues/1370)) ([376e1c3](https://github.com/googleapis/java-spanner-jdbc/commit/376e1c3ccdd71351a5d6151ce19b9f88df163776))
+* Update dependency com.spotify.fmt:fmt-maven-plugin to v2.21.1 ([#1372](https://github.com/googleapis/java-spanner-jdbc/issues/1372)) ([bf64add](https://github.com/googleapis/java-spanner-jdbc/commit/bf64add3e9ce8148d2fc3ad010b8abd446208e4f))
+* Update dependency org.springframework.boot:spring-boot-starter-parent to v3.1.4 ([#1366](https://github.com/googleapis/java-spanner-jdbc/issues/1366)) ([749d2c3](https://github.com/googleapis/java-spanner-jdbc/commit/749d2c3698c900560b6f85247b0a41a85cd55ac8))
+* Update dependency org.springframework.data:spring-data-bom to v2023.0.4 ([#1367](https://github.com/googleapis/java-spanner-jdbc/issues/1367)) ([916ad4a](https://github.com/googleapis/java-spanner-jdbc/commit/916ad4a9e07b3afc15e53664f175db9e58f06376))
+
+
+### Documentation
+
+* Add sample for Spring Data MyBatis ([#1352](https://github.com/googleapis/java-spanner-jdbc/issues/1352)) ([ce52d07](https://github.com/googleapis/java-spanner-jdbc/commit/ce52d07c308bcde0ed1b0c9f4d3556db2590f722))
+
## [2.13.2](https://github.com/googleapis/java-spanner-jdbc/compare/v2.13.1...v2.13.2) (2023-09-26)
diff --git a/pom.xml b/pom.xml
index ba8863857..ab808ec92 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
google-cloud-spanner-jdbc
- 2.13.3-SNAPSHOT
+ 2.13.3
jar
Google Cloud Spanner JDBC
https://github.com/googleapis/java-spanner-jdbc
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index ec9a8dd95..83ae791af 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.13.3-SNAPSHOT
+ 2.13.3
diff --git a/versions.txt b/versions.txt
index 094a46df1..60ce25154 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
# Format:
# module:released-version:current-version
-google-cloud-spanner-jdbc:2.13.2:2.13.3-SNAPSHOT
+google-cloud-spanner-jdbc:2.13.3:2.13.3