Skip to content

Commit fddcc7f

Browse files
dmitry-skurtisvgshubha-rajan
authored
feat: add r2dbc support for MS SQL Server (#328)
* add mssql r2dbc support * add mssql r2dbc support * add mssql r2dbc support * switch to 0.8.5.RELEASE * switch to 1.1.1 * PR comments * remove unnecessary import * Correct name and description. * rename artifact and update readme * fix dependency convergence * fix tests * rename test file Co-authored-by: kurtisvg <[email protected]> Co-authored-by: Shubha Rajan <[email protected]>
1 parent 23860c2 commit fddcc7f

File tree

6 files changed

+305
-0
lines changed

6 files changed

+305
-0
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@ compile 'com.google.cloud.sql:cloud-sql-connector-r2dbc-postgres:1.1.0'
188188
```
189189
*Note: Also include the R2DBC Driver for Postgres, `io.r2dbc:r2dbc-postgresql:<LATEST-VERSION>`
190190

191+
#### PostgreSQL
192+
193+
##### Maven
194+
Include the following in the project's `pom.xml`:
195+
```maven-pom
196+
<dependency>
197+
<groupId>com.google.cloud.sql</groupId>
198+
<artifactId>cloud-sql-connector-r2dbc-sqlserver</artifactId>
199+
<version>1.1.0</version>
200+
</dependency>
201+
```
202+
##### Gradle
203+
Include the following the project's `build.gradle`
204+
```gradle
205+
compile 'com.google.cloud.sql:cloud-sql-connector-r2dbc-sqlserver:1.1.0'
206+
```
207+
*Note: Also include the R2DBC Driver for SQL Server, `io.r2dbc:r2dbc-mssql:<LATEST-VERSION>`
208+
191209
[//]: # ({x-version-update-end})
192210

193211
#### Creating the R2DBC URL
@@ -217,6 +235,19 @@ Add the following parameters:
217235
| DB_USER | Postgres username |
218236
| DB_PASS | Postgres user's password |
219237

238+
##### SQL Server
239+
R2DBC URL template: `r2dbc:gcp:mssql//<DB_USER>:<DB_PASS>@<CLOUD_SQL_CONNECTION_NAME>/<DATABASE_NAME>`
240+
241+
Add the following parameters:
242+
243+
| Property | Value |
244+
| ---------------- | ------------- |
245+
| DATABASE_NAME | The name of the database to connect to |
246+
| CLOUD_SQL_CONNECTION_NAME | The instance connection name (found on the instance details page) |
247+
| DB_USER | SQL Server username |
248+
| DB_PASS | SQL Server user's password |
249+
250+
220251
---
221252

222253
## Building the Drivers

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<module>postgres</module>
6969
<module>sqlserver</module>
7070
<module>r2dbc-core</module>
71+
<module>r2dbc-sqlserver</module>
7172
<module>r2dbc-mysql</module>
7273
<module>r2dbc-postgres</module>
7374
</modules>

r2dbc-sqlserver/pom.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.google.cloud.sql</groupId>
9+
<artifactId>jdbc-socket-factory-parent</artifactId>
10+
<version>1.1.1-SNAPSHOT</version> <!-- {x-version-update:cloud-sql-java-connector:current} -->
11+
</parent>
12+
<artifactId>cloud-sql-connector-r2dbc-sqlserver</artifactId>
13+
<packaging>jar</packaging>
14+
15+
<name>Cloud SQL R2DBC connector for SQL Server</name>
16+
<description>
17+
Connection Factory for the R2DBC Driver for SQL Server that allows a user with the
18+
appropriate permissions to connect to a Cloud SQL database without having to deal with IP
19+
allowlisting or SSL certificates manually.
20+
</description>
21+
22+
<dependencies>
23+
<dependency>
24+
<groupId>io.r2dbc</groupId>
25+
<artifactId>r2dbc-mssql</artifactId>
26+
<version>0.8.5.RELEASE</version>
27+
<scope>provided</scope>
28+
</dependency>
29+
<dependency>
30+
<groupId>com.google.cloud.sql</groupId>
31+
<artifactId>cloud-sql-connector-r2dbc-core</artifactId>
32+
<version>1.1.1-SNAPSHOT</version> <!-- {x-version-update:cloud-sql-java-connector:current} -->
33+
</dependency>
34+
<dependency>
35+
<groupId>junit</groupId>
36+
<artifactId>junit</artifactId>
37+
<version>4.13.1</version>
38+
<scope>test</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>com.google.truth</groupId>
42+
<artifactId>truth</artifactId>
43+
<version>1.1</version>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.r2dbc</groupId>
48+
<artifactId>r2dbc-pool</artifactId>
49+
<version>0.8.5.RELEASE</version>
50+
<scope>test</scope>
51+
</dependency>
52+
</dependencies>
53+
54+
<profiles>
55+
<profile>
56+
<id>jar-with-driver-and-dependencies</id>
57+
<dependencies>
58+
<dependency>
59+
<groupId>io.r2dbc</groupId>
60+
<artifactId>r2dbc-mssql</artifactId>
61+
<version>0.8.5.RELEASE</version>
62+
</dependency>
63+
</dependencies>
64+
</profile>
65+
</profiles>
66+
67+
</project>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2020 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.core;
18+
19+
import static io.r2dbc.spi.ConnectionFactoryOptions.Builder;
20+
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
21+
22+
import io.netty.handler.ssl.SslContextBuilder;
23+
import io.r2dbc.mssql.MssqlConnectionFactoryProvider;
24+
import io.r2dbc.spi.ConnectionFactory;
25+
import io.r2dbc.spi.ConnectionFactoryOptions;
26+
import io.r2dbc.spi.ConnectionFactoryProvider;
27+
import java.util.function.Function;
28+
29+
/**
30+
* {@link ConnectionFactoryProvider} for proxied access to GCP MsSQL instances.
31+
*/
32+
public class GcpConnectionFactoryProviderMssql extends GcpConnectionFactoryProvider {
33+
34+
static {
35+
CoreSocketFactory.addArtifactId("cloud-sql-connector-r2dbc-mssql");
36+
}
37+
38+
/**
39+
* MsSQL driver option value.
40+
*/
41+
private static final String MSSQL_DRIVER = "mssql";
42+
43+
@Override
44+
boolean supportedProtocol(String protocol) {
45+
return protocol.equals(MSSQL_DRIVER);
46+
}
47+
48+
@Override
49+
ConnectionFactory tcpConnectonFactory(
50+
Builder optionBuilder,
51+
Function<SslContextBuilder, SslContextBuilder> customizer,
52+
String csqlHostName) {
53+
optionBuilder
54+
.option(MssqlConnectionFactoryProvider.SSL_TUNNEL, customizer)
55+
.option(MssqlConnectionFactoryProvider.TCP_NODELAY, true)
56+
.option(MssqlConnectionFactoryProvider.TCP_KEEPALIVE, true);
57+
58+
return new CloudSqlConnectionFactory(
59+
(ConnectionFactoryOptions options) -> new MssqlConnectionFactoryProvider().create(options),
60+
optionBuilder,
61+
csqlHostName);
62+
}
63+
64+
@Override
65+
ConnectionFactory socketConnectionFactory(Builder optionBuilder, String socket) {
66+
throw new RuntimeException("UNIX socket connections are not supported");
67+
}
68+
69+
@Override
70+
Builder createBuilder(ConnectionFactoryOptions connectionFactoryOptions) {
71+
return connectionFactoryOptions.mutate().option(DRIVER, MSSQL_DRIVER);
72+
}
73+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.google.cloud.sql.core.GcpConnectionFactoryProviderMssql
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.core;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static com.google.common.truth.Truth.assertWithMessage;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import io.r2dbc.pool.ConnectionPool;
24+
import io.r2dbc.pool.ConnectionPoolConfiguration;
25+
import io.r2dbc.spi.ConnectionFactories;
26+
import io.r2dbc.spi.ConnectionFactory;
27+
import java.util.List;
28+
import java.util.UUID;
29+
import java.util.concurrent.TimeUnit;
30+
import org.junit.After;
31+
import org.junit.Before;
32+
import org.junit.Rule;
33+
import org.junit.Test;
34+
import org.junit.rules.Timeout;
35+
import org.junit.runner.RunWith;
36+
import org.junit.runners.JUnit4;
37+
import reactor.core.publisher.Mono;
38+
39+
@RunWith(JUnit4.class)
40+
public class R2dbcSqlserverIntegrationTests {
41+
42+
private static final ImmutableList<String> requiredEnvVars = ImmutableList
43+
.of("SQLSERVER_USER", "SQLSERVER_PASS", "SQLSERVER_DB", "SQLSERVER_CONNECTION_NAME");
44+
45+
private static final String CONNECTION_NAME = System.getenv("SQLSERVER_CONNECTION_NAME");
46+
private static final String DB_NAME = System.getenv("SQLSERVER_DB");
47+
private static final String DB_USER = System.getenv("SQLSERVER_USER");
48+
private static final String DB_PASSWORD = System.getenv("SQLSERVER_PASS");
49+
50+
@Rule
51+
public Timeout globalTimeout = new Timeout(20, TimeUnit.SECONDS);
52+
53+
private ConnectionPool connectionPool;
54+
private String tableName;
55+
56+
@Before
57+
public void setUpPool() {
58+
// Check that required env vars are set
59+
requiredEnvVars.forEach((varName) -> {
60+
assertWithMessage(
61+
String.format("Environment variable '%s' must be set to perform these tests.", varName))
62+
.that(System.getenv(varName)).isNotEmpty();
63+
});
64+
65+
// Set up URL parameters
66+
String r2dbcURL = String
67+
.format("r2dbc:gcp:mssql://%s:%s@%s/%s", DB_USER, DB_PASSWORD, CONNECTION_NAME,
68+
DB_NAME);
69+
70+
// Initialize connection pool
71+
ConnectionFactory connectionFactory = ConnectionFactories.get(r2dbcURL);
72+
ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration
73+
.builder(connectionFactory)
74+
.build();
75+
76+
this.connectionPool = new ConnectionPool(configuration);
77+
this.tableName = String.format("books_%s", UUID.randomUUID().toString().replace("-", ""));
78+
79+
// Create table
80+
Mono.from(this.connectionPool.create())
81+
.flatMapMany(
82+
c ->
83+
c.createStatement(
84+
String.format("CREATE TABLE %s (", this.tableName)
85+
+ " ID CHAR(20) NOT NULL,"
86+
+ " TITLE TEXT NOT NULL"
87+
+ ")")
88+
.execute())
89+
.blockLast();
90+
}
91+
92+
@After
93+
public void dropTableIfPresent() {
94+
String dropStmt = String.format("DROP TABLE %s", this.tableName);
95+
Mono.from(this.connectionPool.create())
96+
.delayUntil(c -> c.createStatement(dropStmt).execute())
97+
.block();
98+
}
99+
100+
@Test
101+
public void pooledConnectionTest() {
102+
String insertStmt = String.format("INSERT INTO %s (ID, TITLE) VALUES (@id, @title)", this.tableName);
103+
Mono.from(this.connectionPool.create())
104+
.flatMapMany(
105+
c ->
106+
c.createStatement(insertStmt)
107+
.bind("id", "book1")
108+
.bind("title", "Book One")
109+
.add()
110+
.bind("id", "book2")
111+
.bind("title", "Book Two")
112+
.execute())
113+
.flatMap(result -> result.map((row, rowMetadata) -> row.get(0)))
114+
.blockLast();
115+
116+
String selectStmt = String.format("SELECT TITLE FROM %s ORDER BY ID", this.tableName);
117+
List<String> books =
118+
Mono.from(this.connectionPool.create())
119+
.flatMapMany(
120+
connection ->
121+
connection.createStatement(selectStmt).execute())
122+
.flatMap(
123+
result ->
124+
result.map(
125+
(r, meta) -> r.get("TITLE", String.class)))
126+
.collectList()
127+
.block();
128+
129+
assertThat(books).containsExactly("Book One", "Book Two");
130+
131+
}
132+
}

0 commit comments

Comments
 (0)