Skip to content

Commit

Permalink
feat: support multiple PostgreSQL transaction options (#1949)
Browse files Browse the repository at this point in the history
* feat: support multiple PostgreSQL transaction options

PostgreSQL allows BEGIN and other transaction statements to set multiple transaction
options at once (e.g. both 'read only' and 'isolation level serializable'). This was
not supported by the Connection API, which only allowed one option at a time. The
Python psycopg2 driver generates statements that include multiple transaction options
in one statement.

* fix: remove duplicated statement

* chore: run code formatter

* fix: support 'to' in set statements

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* fix: support on/off yes/no 1/0

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
olavloite and gcf-owl-bot[bot] authored Jul 22, 2022
1 parent f01d263 commit 8b99f30
Show file tree
Hide file tree
Showing 14 changed files with 23,043 additions and 15,106 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-spanner:6.26.0'
implementation 'com.google.cloud:google-cloud-spanner:6.27.0'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.26.0"
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.27.0"
```

## Authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TimestampBound.Mode;
import com.google.cloud.spanner.connection.PgTransactionMode.AccessMode;
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.protobuf.Duration;
import com.google.protobuf.util.Durations;
import com.google.spanner.v1.RequestOptions.Priority;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -85,6 +88,53 @@ public Boolean convert(String value) {
}
}

/** Converter from string to {@link Boolean} */
static class PgBooleanConverter implements ClientSideStatementValueConverter<Boolean> {

public PgBooleanConverter(String allowedValues) {}

@Override
public Class<Boolean> getParameterClass() {
return Boolean.class;
}

@Override
public Boolean convert(String value) {
if (value == null) {
return null;
}
if (value.length() > 1
&& ((value.startsWith("'") && value.endsWith("'"))
|| (value.startsWith("\"") && value.endsWith("\"")))) {
value = value.substring(1, value.length() - 1);
}
if ("true".equalsIgnoreCase(value)
|| "tru".equalsIgnoreCase(value)
|| "tr".equalsIgnoreCase(value)
|| "t".equalsIgnoreCase(value)
|| "on".equalsIgnoreCase(value)
|| "1".equalsIgnoreCase(value)
|| "yes".equalsIgnoreCase(value)
|| "ye".equalsIgnoreCase(value)
|| "y".equalsIgnoreCase(value)) {
return Boolean.TRUE;
}
if ("false".equalsIgnoreCase(value)
|| "fals".equalsIgnoreCase(value)
|| "fal".equalsIgnoreCase(value)
|| "fa".equalsIgnoreCase(value)
|| "f".equalsIgnoreCase(value)
|| "off".equalsIgnoreCase(value)
|| "of".equalsIgnoreCase(value)
|| "0".equalsIgnoreCase(value)
|| "no".equalsIgnoreCase(value)
|| "n".equalsIgnoreCase(value)) {
return Boolean.FALSE;
}
return null;
}
}

/** Converter from string to {@link Duration}. */
static class DurationConverter implements ClientSideStatementValueConverter<Duration> {
private final Pattern allowedValues;
Expand Down Expand Up @@ -286,16 +336,39 @@ public TransactionMode convert(String value) {
}
}

static class PgTransactionIsolationConverter
implements ClientSideStatementValueConverter<IsolationLevel> {
private final CaseInsensitiveEnumMap<IsolationLevel> values =
new CaseInsensitiveEnumMap<>(IsolationLevel.class, IsolationLevel::getShortStatementString);

public PgTransactionIsolationConverter(String allowedValues) {}

@Override
public Class<IsolationLevel> getParameterClass() {
return IsolationLevel.class;
}

@Override
public IsolationLevel convert(String value) {
// Isolation level may contain multiple spaces.
String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
if (valueWithSingleSpaces.length() > 1
&& ((valueWithSingleSpaces.startsWith("'") && valueWithSingleSpaces.endsWith("'"))
|| (valueWithSingleSpaces.startsWith("\"")
&& valueWithSingleSpaces.endsWith("\"")))) {
valueWithSingleSpaces =
valueWithSingleSpaces.substring(1, valueWithSingleSpaces.length() - 1);
}
return values.get(valueWithSingleSpaces);
}
}

/**
* Converter for converting string values to {@link PgTransactionMode} values. Includes no-op
* handling of setting the isolation level of the transaction to default or serializable.
*/
static class PgTransactionModeConverter
implements ClientSideStatementValueConverter<PgTransactionMode> {
private final CaseInsensitiveEnumMap<PgTransactionMode> values =
new CaseInsensitiveEnumMap<>(
PgTransactionMode.class, PgTransactionMode::getStatementString);

PgTransactionModeConverter() {}

public PgTransactionModeConverter(String allowedValues) {}
Expand All @@ -307,9 +380,49 @@ public Class<PgTransactionMode> getParameterClass() {

@Override
public PgTransactionMode convert(String value) {
PgTransactionMode mode = new PgTransactionMode();
// Transaction mode may contain multiple spaces.
String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
return values.get(valueWithSingleSpaces);
String valueWithSingleSpaces =
value.replaceAll("\\s+", " ").toLowerCase(Locale.ENGLISH).trim();
int currentIndex = 0;
while (currentIndex < valueWithSingleSpaces.length()) {
// This will use the last access mode and isolation level that is encountered in the string.
// This is consistent with the behavior of PostgreSQL, which also allows multiple modes to
// be specified in one string, and will use the last one that is encountered.
if (valueWithSingleSpaces.substring(currentIndex).startsWith("read only")) {
currentIndex += "read only".length();
mode.setAccessMode(AccessMode.READ_ONLY_TRANSACTION);
} else if (valueWithSingleSpaces.substring(currentIndex).startsWith("read write")) {
currentIndex += "read write".length();
mode.setAccessMode(AccessMode.READ_WRITE_TRANSACTION);
} else if (valueWithSingleSpaces
.substring(currentIndex)
.startsWith("isolation level serializable")) {
currentIndex += "isolation level serializable".length();
mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_SERIALIZABLE);
} else if (valueWithSingleSpaces
.substring(currentIndex)
.startsWith("isolation level default")) {
currentIndex += "isolation level default".length();
mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_DEFAULT);
} else {
return null;
}
// Skip space and/or comma that may separate multiple transaction modes.
if (currentIndex < valueWithSingleSpaces.length()
&& valueWithSingleSpaces.charAt(currentIndex) == ' ') {
currentIndex++;
}
if (currentIndex < valueWithSingleSpaces.length()
&& valueWithSingleSpaces.charAt(currentIndex) == ',') {
currentIndex++;
}
if (currentIndex < valueWithSingleSpaces.length()
&& valueWithSingleSpaces.charAt(currentIndex) == ' ') {
currentIndex++;
}
}
return mode;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.connection;

import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.protobuf.Duration;
import com.google.spanner.v1.RequestOptions.Priority;

Expand Down Expand Up @@ -98,6 +99,8 @@ interface ConnectionStatementExecutor {
StatementResult statementSetPgSessionCharacteristicsTransactionMode(
PgTransactionMode transactionMode);

StatementResult statementSetPgDefaultTransactionIsolation(IsolationLevel isolationLevel);

StatementResult statementStartBatchDdl();

StatementResult statementStartBatchDml();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.RUN_BATCH;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DEFAULT_TRANSACTION_ISOLATION;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_STATISTICS_PACKAGE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_VERSION;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
Expand Down Expand Up @@ -70,6 +71,7 @@
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.StructField;
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.DurationValueGetter;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
Expand Down Expand Up @@ -372,39 +374,45 @@ public StatementResult statementSetTransactionMode(TransactionMode mode) {

@Override
public StatementResult statementSetPgTransactionMode(PgTransactionMode transactionMode) {
switch (transactionMode) {
case READ_ONLY_TRANSACTION:
getConnection().setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION);
break;
case READ_WRITE_TRANSACTION:
getConnection().setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION);
break;
case ISOLATION_LEVEL_DEFAULT:
case ISOLATION_LEVEL_SERIALIZABLE:
default:
// no-op
if (transactionMode.getAccessMode() != null) {
switch (transactionMode.getAccessMode()) {
case READ_ONLY_TRANSACTION:
getConnection().setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION);
break;
case READ_WRITE_TRANSACTION:
getConnection().setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION);
break;
default:
// no-op
}
}
return noResult(SET_TRANSACTION_MODE);
}

@Override
public StatementResult statementSetPgSessionCharacteristicsTransactionMode(
PgTransactionMode transactionMode) {
switch (transactionMode) {
case READ_ONLY_TRANSACTION:
getConnection().setReadOnly(true);
break;
case READ_WRITE_TRANSACTION:
getConnection().setReadOnly(false);
break;
case ISOLATION_LEVEL_DEFAULT:
case ISOLATION_LEVEL_SERIALIZABLE:
default:
// no-op
if (transactionMode.getAccessMode() != null) {
switch (transactionMode.getAccessMode()) {
case READ_ONLY_TRANSACTION:
getConnection().setReadOnly(true);
break;
case READ_WRITE_TRANSACTION:
getConnection().setReadOnly(false);
break;
default:
// no-op
}
}
return noResult(SET_TRANSACTION_MODE);
}

@Override
public StatementResult statementSetPgDefaultTransactionIsolation(IsolationLevel isolationLevel) {
// no-op
return noResult(SET_DEFAULT_TRANSACTION_ISOLATION);
}

@Override
public StatementResult statementStartBatchDdl() {
getConnection().startBatchDdl();
Expand Down
Loading

0 comments on commit 8b99f30

Please sign in to comment.