Skip to content
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("\"")))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Double Quotes("") are used to identify objects inside the database like column names, table names etc. So, "true" shouldn't work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it strange, but it actually allowed (see above)

value = value.substring(1, value.length() - 1);
}
if ("true".equalsIgnoreCase(value)
Copy link
Contributor

@gauravsnj gauravsnj Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also trim the string to consider the cases like ' true '

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(See also above). PostgreSQL does not consider that to be a valid boolean value. When the value is quoted with either single or double quotes, it may not contain any spaces.

|| "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)
Copy link
Contributor

@gauravsnj gauravsnj Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the values like "tru", "fal", "tr" are supported without single quotes. So, tru or fal shouldn't work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strangely enough, they are. I've tried a multitude of different statements with a real PG instance. See below for the results:

knut-test-db=# set default_transaction_read_only to "on"; SET knut-test-db=# set default_transaction_read_only to tr; SET knut-test-db=# set default_transaction_read_only to ' true '; ERROR: parameter "default_transaction_read_only" requires a Boolean value knut-test-db=# set default_transaction_read_only to "true"; SET knut-test-db=# set default_transaction_read_only to 'true'; SET knut-test-db=# set default_transaction_read_only to ye; SET knut-test-db=# set default_transaction_read_only to 'ye'; SET

So TLDR:

  • Both single and double quotes are allowed (which is strange, as double quotes would normally be used to quote identifiers)
  • Unique prefixes like tr and n are allowed both with and without quotes.
  • Spaces inside quotes are not allowed, so ' true ' is not a valid value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's strange because once I tested inserting boolean values in some table and the results were totally opposite :) Btw thanks for clarifying

|| "fal".equalsIgnoreCase(value)
Copy link
Contributor

@gauravsnj gauravsnj Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add support for 'ON', 'OFF', 'yes', 'no', 0 and 1 while converting the boolean values? For the Psycopg2, adding only 'ON' and 'OFF' will do but we can add 'yes', 'no', 0 and 1 to make it fully generic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, added for:

  • on/off
  • 1/0
  • yes/no

Including all unique prefixes (i.e. ye is also valid as true).

|| "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