Skip to content

Commit

Permalink
feat: add r2dbc support for MS SQL Server (#328)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Nov 18, 2020
1 parent 23860c2 commit fddcc7f
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 0 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,24 @@ compile 'com.google.cloud.sql:cloud-sql-connector-r2dbc-postgres:1.1.0'
```
*Note: Also include the R2DBC Driver for Postgres, `io.r2dbc:r2dbc-postgresql:<LATEST-VERSION>`

#### PostgreSQL

##### Maven
Include the following in the project's `pom.xml`:
```maven-pom
<dependency>
<groupId>com.google.cloud.sql</groupId>
<artifactId>cloud-sql-connector-r2dbc-sqlserver</artifactId>
<version>1.1.0</version>
</dependency>
```
##### Gradle
Include the following the project's `build.gradle`
```gradle
compile 'com.google.cloud.sql:cloud-sql-connector-r2dbc-sqlserver:1.1.0'
```
*Note: Also include the R2DBC Driver for SQL Server, `io.r2dbc:r2dbc-mssql:<LATEST-VERSION>`

[//]: # ({x-version-update-end})

#### Creating the R2DBC URL
Expand Down Expand Up @@ -217,6 +235,19 @@ Add the following parameters:
| DB_USER | Postgres username |
| DB_PASS | Postgres user's password |

##### SQL Server
R2DBC URL template: `r2dbc:gcp:mssql//<DB_USER>:<DB_PASS>@<CLOUD_SQL_CONNECTION_NAME>/<DATABASE_NAME>`

Add the following parameters:

| Property | Value |
| ---------------- | ------------- |
| DATABASE_NAME | The name of the database to connect to |
| CLOUD_SQL_CONNECTION_NAME | The instance connection name (found on the instance details page) |
| DB_USER | SQL Server username |
| DB_PASS | SQL Server user's password |


---

## Building the Drivers
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
<module>postgres</module>
<module>sqlserver</module>
<module>r2dbc-core</module>
<module>r2dbc-sqlserver</module>
<module>r2dbc-mysql</module>
<module>r2dbc-postgres</module>
</modules>
Expand Down
67 changes: 67 additions & 0 deletions r2dbc-sqlserver/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.cloud.sql</groupId>
<artifactId>jdbc-socket-factory-parent</artifactId>
<version>1.1.1-SNAPSHOT</version> <!-- {x-version-update:cloud-sql-java-connector:current} -->
</parent>
<artifactId>cloud-sql-connector-r2dbc-sqlserver</artifactId>
<packaging>jar</packaging>

<name>Cloud SQL R2DBC connector for SQL Server</name>
<description>
Connection Factory for the R2DBC Driver for SQL Server that allows a user with the
appropriate permissions to connect to a Cloud SQL database without having to deal with IP
allowlisting or SSL certificates manually.
</description>

<dependencies>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-mssql</artifactId>
<version>0.8.5.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.cloud.sql</groupId>
<artifactId>cloud-sql-connector-r2dbc-core</artifactId>
<version>1.1.1-SNAPSHOT</version> <!-- {x-version-update:cloud-sql-java-connector:current} -->
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-pool</artifactId>
<version>0.8.5.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>jar-with-driver-and-dependencies</id>
<dependencies>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-mssql</artifactId>
<version>0.8.5.RELEASE</version>
</dependency>
</dependencies>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* 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.sql.core;

import static io.r2dbc.spi.ConnectionFactoryOptions.Builder;
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;

import io.netty.handler.ssl.SslContextBuilder;
import io.r2dbc.mssql.MssqlConnectionFactoryProvider;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryProvider;
import java.util.function.Function;

/**
* {@link ConnectionFactoryProvider} for proxied access to GCP MsSQL instances.
*/
public class GcpConnectionFactoryProviderMssql extends GcpConnectionFactoryProvider {

static {
CoreSocketFactory.addArtifactId("cloud-sql-connector-r2dbc-mssql");
}

/**
* MsSQL driver option value.
*/
private static final String MSSQL_DRIVER = "mssql";

@Override
boolean supportedProtocol(String protocol) {
return protocol.equals(MSSQL_DRIVER);
}

@Override
ConnectionFactory tcpConnectonFactory(
Builder optionBuilder,
Function<SslContextBuilder, SslContextBuilder> customizer,
String csqlHostName) {
optionBuilder
.option(MssqlConnectionFactoryProvider.SSL_TUNNEL, customizer)
.option(MssqlConnectionFactoryProvider.TCP_NODELAY, true)
.option(MssqlConnectionFactoryProvider.TCP_KEEPALIVE, true);

return new CloudSqlConnectionFactory(
(ConnectionFactoryOptions options) -> new MssqlConnectionFactoryProvider().create(options),
optionBuilder,
csqlHostName);
}

@Override
ConnectionFactory socketConnectionFactory(Builder optionBuilder, String socket) {
throw new RuntimeException("UNIX socket connections are not supported");
}

@Override
Builder createBuilder(ConnectionFactoryOptions connectionFactoryOptions) {
return connectionFactoryOptions.mutate().option(DRIVER, MSSQL_DRIVER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.google.cloud.sql.core.GcpConnectionFactoryProviderMssql
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2020 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.sql.core;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import com.google.common.collect.ImmutableList;
import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import reactor.core.publisher.Mono;

@RunWith(JUnit4.class)
public class R2dbcSqlserverIntegrationTests {

private static final ImmutableList<String> requiredEnvVars = ImmutableList
.of("SQLSERVER_USER", "SQLSERVER_PASS", "SQLSERVER_DB", "SQLSERVER_CONNECTION_NAME");

private static final String CONNECTION_NAME = System.getenv("SQLSERVER_CONNECTION_NAME");
private static final String DB_NAME = System.getenv("SQLSERVER_DB");
private static final String DB_USER = System.getenv("SQLSERVER_USER");
private static final String DB_PASSWORD = System.getenv("SQLSERVER_PASS");

@Rule
public Timeout globalTimeout = new Timeout(20, TimeUnit.SECONDS);

private ConnectionPool connectionPool;
private String tableName;

@Before
public void setUpPool() {
// Check that required env vars are set
requiredEnvVars.forEach((varName) -> {
assertWithMessage(
String.format("Environment variable '%s' must be set to perform these tests.", varName))
.that(System.getenv(varName)).isNotEmpty();
});

// Set up URL parameters
String r2dbcURL = String
.format("r2dbc:gcp:mssql://%s:%s@%s/%s", DB_USER, DB_PASSWORD, CONNECTION_NAME,
DB_NAME);

// Initialize connection pool
ConnectionFactory connectionFactory = ConnectionFactories.get(r2dbcURL);
ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration
.builder(connectionFactory)
.build();

this.connectionPool = new ConnectionPool(configuration);
this.tableName = String.format("books_%s", UUID.randomUUID().toString().replace("-", ""));

// Create table
Mono.from(this.connectionPool.create())
.flatMapMany(
c ->
c.createStatement(
String.format("CREATE TABLE %s (", this.tableName)
+ " ID CHAR(20) NOT NULL,"
+ " TITLE TEXT NOT NULL"
+ ")")
.execute())
.blockLast();
}

@After
public void dropTableIfPresent() {
String dropStmt = String.format("DROP TABLE %s", this.tableName);
Mono.from(this.connectionPool.create())
.delayUntil(c -> c.createStatement(dropStmt).execute())
.block();
}

@Test
public void pooledConnectionTest() {
String insertStmt = String.format("INSERT INTO %s (ID, TITLE) VALUES (@id, @title)", this.tableName);
Mono.from(this.connectionPool.create())
.flatMapMany(
c ->
c.createStatement(insertStmt)
.bind("id", "book1")
.bind("title", "Book One")
.add()
.bind("id", "book2")
.bind("title", "Book Two")
.execute())
.flatMap(result -> result.map((row, rowMetadata) -> row.get(0)))
.blockLast();

String selectStmt = String.format("SELECT TITLE FROM %s ORDER BY ID", this.tableName);
List<String> books =
Mono.from(this.connectionPool.create())
.flatMapMany(
connection ->
connection.createStatement(selectStmt).execute())
.flatMap(
result ->
result.map(
(r, meta) -> r.get("TITLE", String.class)))
.collectList()
.block();

assertThat(books).containsExactly("Book One", "Book Two");

}
}

0 comments on commit fddcc7f

Please sign in to comment.