Skip to content

Commit 8b99f30

Browse files
feat: support multiple PostgreSQL transaction options (#1949)
* 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>
1 parent f01d263 commit 8b99f30

File tree

14 files changed

+23043
-15106
lines changed

14 files changed

+23043
-15106
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
5656
If you are using Gradle without BOM, add this to your dependencies:
5757

5858
```Groovy
59-
implementation 'com.google.cloud:google-cloud-spanner:6.26.0'
59+
implementation 'com.google.cloud:google-cloud-spanner:6.27.0'
6060
```
6161

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

6464
```Scala
65-
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.26.0"
65+
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.27.0"
6666
```
6767

6868
## Authentication

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import com.google.cloud.spanner.SpannerExceptionFactory;
2222
import com.google.cloud.spanner.TimestampBound;
2323
import com.google.cloud.spanner.TimestampBound.Mode;
24+
import com.google.cloud.spanner.connection.PgTransactionMode.AccessMode;
25+
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
2426
import com.google.common.base.Function;
2527
import com.google.common.base.Preconditions;
2628
import com.google.protobuf.Duration;
2729
import com.google.protobuf.util.Durations;
2830
import com.google.spanner.v1.RequestOptions.Priority;
2931
import java.util.EnumSet;
3032
import java.util.HashMap;
33+
import java.util.Locale;
3134
import java.util.Map;
3235
import java.util.concurrent.TimeUnit;
3336
import java.util.regex.Matcher;
@@ -85,6 +88,53 @@ public Boolean convert(String value) {
8588
}
8689
}
8790

91+
/** Converter from string to {@link Boolean} */
92+
static class PgBooleanConverter implements ClientSideStatementValueConverter<Boolean> {
93+
94+
public PgBooleanConverter(String allowedValues) {}
95+
96+
@Override
97+
public Class<Boolean> getParameterClass() {
98+
return Boolean.class;
99+
}
100+
101+
@Override
102+
public Boolean convert(String value) {
103+
if (value == null) {
104+
return null;
105+
}
106+
if (value.length() > 1
107+
&& ((value.startsWith("'") && value.endsWith("'"))
108+
|| (value.startsWith("\"") && value.endsWith("\"")))) {
109+
value = value.substring(1, value.length() - 1);
110+
}
111+
if ("true".equalsIgnoreCase(value)
112+
|| "tru".equalsIgnoreCase(value)
113+
|| "tr".equalsIgnoreCase(value)
114+
|| "t".equalsIgnoreCase(value)
115+
|| "on".equalsIgnoreCase(value)
116+
|| "1".equalsIgnoreCase(value)
117+
|| "yes".equalsIgnoreCase(value)
118+
|| "ye".equalsIgnoreCase(value)
119+
|| "y".equalsIgnoreCase(value)) {
120+
return Boolean.TRUE;
121+
}
122+
if ("false".equalsIgnoreCase(value)
123+
|| "fals".equalsIgnoreCase(value)
124+
|| "fal".equalsIgnoreCase(value)
125+
|| "fa".equalsIgnoreCase(value)
126+
|| "f".equalsIgnoreCase(value)
127+
|| "off".equalsIgnoreCase(value)
128+
|| "of".equalsIgnoreCase(value)
129+
|| "0".equalsIgnoreCase(value)
130+
|| "no".equalsIgnoreCase(value)
131+
|| "n".equalsIgnoreCase(value)) {
132+
return Boolean.FALSE;
133+
}
134+
return null;
135+
}
136+
}
137+
88138
/** Converter from string to {@link Duration}. */
89139
static class DurationConverter implements ClientSideStatementValueConverter<Duration> {
90140
private final Pattern allowedValues;
@@ -286,16 +336,39 @@ public TransactionMode convert(String value) {
286336
}
287337
}
288338

339+
static class PgTransactionIsolationConverter
340+
implements ClientSideStatementValueConverter<IsolationLevel> {
341+
private final CaseInsensitiveEnumMap<IsolationLevel> values =
342+
new CaseInsensitiveEnumMap<>(IsolationLevel.class, IsolationLevel::getShortStatementString);
343+
344+
public PgTransactionIsolationConverter(String allowedValues) {}
345+
346+
@Override
347+
public Class<IsolationLevel> getParameterClass() {
348+
return IsolationLevel.class;
349+
}
350+
351+
@Override
352+
public IsolationLevel convert(String value) {
353+
// Isolation level may contain multiple spaces.
354+
String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
355+
if (valueWithSingleSpaces.length() > 1
356+
&& ((valueWithSingleSpaces.startsWith("'") && valueWithSingleSpaces.endsWith("'"))
357+
|| (valueWithSingleSpaces.startsWith("\"")
358+
&& valueWithSingleSpaces.endsWith("\"")))) {
359+
valueWithSingleSpaces =
360+
valueWithSingleSpaces.substring(1, valueWithSingleSpaces.length() - 1);
361+
}
362+
return values.get(valueWithSingleSpaces);
363+
}
364+
}
365+
289366
/**
290367
* Converter for converting string values to {@link PgTransactionMode} values. Includes no-op
291368
* handling of setting the isolation level of the transaction to default or serializable.
292369
*/
293370
static class PgTransactionModeConverter
294371
implements ClientSideStatementValueConverter<PgTransactionMode> {
295-
private final CaseInsensitiveEnumMap<PgTransactionMode> values =
296-
new CaseInsensitiveEnumMap<>(
297-
PgTransactionMode.class, PgTransactionMode::getStatementString);
298-
299372
PgTransactionModeConverter() {}
300373

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

308381
@Override
309382
public PgTransactionMode convert(String value) {
383+
PgTransactionMode mode = new PgTransactionMode();
310384
// Transaction mode may contain multiple spaces.
311-
String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
312-
return values.get(valueWithSingleSpaces);
385+
String valueWithSingleSpaces =
386+
value.replaceAll("\\s+", " ").toLowerCase(Locale.ENGLISH).trim();
387+
int currentIndex = 0;
388+
while (currentIndex < valueWithSingleSpaces.length()) {
389+
// This will use the last access mode and isolation level that is encountered in the string.
390+
// This is consistent with the behavior of PostgreSQL, which also allows multiple modes to
391+
// be specified in one string, and will use the last one that is encountered.
392+
if (valueWithSingleSpaces.substring(currentIndex).startsWith("read only")) {
393+
currentIndex += "read only".length();
394+
mode.setAccessMode(AccessMode.READ_ONLY_TRANSACTION);
395+
} else if (valueWithSingleSpaces.substring(currentIndex).startsWith("read write")) {
396+
currentIndex += "read write".length();
397+
mode.setAccessMode(AccessMode.READ_WRITE_TRANSACTION);
398+
} else if (valueWithSingleSpaces
399+
.substring(currentIndex)
400+
.startsWith("isolation level serializable")) {
401+
currentIndex += "isolation level serializable".length();
402+
mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_SERIALIZABLE);
403+
} else if (valueWithSingleSpaces
404+
.substring(currentIndex)
405+
.startsWith("isolation level default")) {
406+
currentIndex += "isolation level default".length();
407+
mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_DEFAULT);
408+
} else {
409+
return null;
410+
}
411+
// Skip space and/or comma that may separate multiple transaction modes.
412+
if (currentIndex < valueWithSingleSpaces.length()
413+
&& valueWithSingleSpaces.charAt(currentIndex) == ' ') {
414+
currentIndex++;
415+
}
416+
if (currentIndex < valueWithSingleSpaces.length()
417+
&& valueWithSingleSpaces.charAt(currentIndex) == ',') {
418+
currentIndex++;
419+
}
420+
if (currentIndex < valueWithSingleSpaces.length()
421+
&& valueWithSingleSpaces.charAt(currentIndex) == ' ') {
422+
currentIndex++;
423+
}
424+
}
425+
return mode;
313426
}
314427
}
315428

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import com.google.cloud.spanner.TimestampBound;
20+
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
2021
import com.google.protobuf.Duration;
2122
import com.google.spanner.v1.RequestOptions.Priority;
2223

@@ -98,6 +99,8 @@ interface ConnectionStatementExecutor {
9899
StatementResult statementSetPgSessionCharacteristicsTransactionMode(
99100
PgTransactionMode transactionMode);
100101

102+
StatementResult statementSetPgDefaultTransactionIsolation(IsolationLevel isolationLevel);
103+
101104
StatementResult statementStartBatchDdl();
102105

103106
StatementResult statementStartBatchDml();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.RUN_BATCH;
2525
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT;
2626
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE;
27+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DEFAULT_TRANSACTION_ISOLATION;
2728
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_STATISTICS_PACKAGE;
2829
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_VERSION;
2930
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
@@ -70,6 +71,7 @@
7071
import com.google.cloud.spanner.TimestampBound;
7172
import com.google.cloud.spanner.Type;
7273
import com.google.cloud.spanner.Type.StructField;
74+
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
7375
import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.DurationValueGetter;
7476
import com.google.common.base.MoreObjects;
7577
import com.google.common.base.Preconditions;
@@ -372,39 +374,45 @@ public StatementResult statementSetTransactionMode(TransactionMode mode) {
372374

373375
@Override
374376
public StatementResult statementSetPgTransactionMode(PgTransactionMode transactionMode) {
375-
switch (transactionMode) {
376-
case READ_ONLY_TRANSACTION:
377-
getConnection().setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION);
378-
break;
379-
case READ_WRITE_TRANSACTION:
380-
getConnection().setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION);
381-
break;
382-
case ISOLATION_LEVEL_DEFAULT:
383-
case ISOLATION_LEVEL_SERIALIZABLE:
384-
default:
385-
// no-op
377+
if (transactionMode.getAccessMode() != null) {
378+
switch (transactionMode.getAccessMode()) {
379+
case READ_ONLY_TRANSACTION:
380+
getConnection().setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION);
381+
break;
382+
case READ_WRITE_TRANSACTION:
383+
getConnection().setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION);
384+
break;
385+
default:
386+
// no-op
387+
}
386388
}
387389
return noResult(SET_TRANSACTION_MODE);
388390
}
389391

390392
@Override
391393
public StatementResult statementSetPgSessionCharacteristicsTransactionMode(
392394
PgTransactionMode transactionMode) {
393-
switch (transactionMode) {
394-
case READ_ONLY_TRANSACTION:
395-
getConnection().setReadOnly(true);
396-
break;
397-
case READ_WRITE_TRANSACTION:
398-
getConnection().setReadOnly(false);
399-
break;
400-
case ISOLATION_LEVEL_DEFAULT:
401-
case ISOLATION_LEVEL_SERIALIZABLE:
402-
default:
403-
// no-op
395+
if (transactionMode.getAccessMode() != null) {
396+
switch (transactionMode.getAccessMode()) {
397+
case READ_ONLY_TRANSACTION:
398+
getConnection().setReadOnly(true);
399+
break;
400+
case READ_WRITE_TRANSACTION:
401+
getConnection().setReadOnly(false);
402+
break;
403+
default:
404+
// no-op
405+
}
404406
}
405407
return noResult(SET_TRANSACTION_MODE);
406408
}
407409

410+
@Override
411+
public StatementResult statementSetPgDefaultTransactionIsolation(IsolationLevel isolationLevel) {
412+
// no-op
413+
return noResult(SET_DEFAULT_TRANSACTION_ISOLATION);
414+
}
415+
408416
@Override
409417
public StatementResult statementStartBatchDdl() {
410418
getConnection().startBatchDdl();

0 commit comments

Comments
 (0)