Skip to content

Commit 9eccfc4

Browse files
authored
feat: automatically detect database dialect (#1677)
The dialect of the database that a `DatabaseClient` is connected to can be automatically detected: 1. `DatabaseClient#getDialect()` has been added. This method always returns the dialect of the underlying database. It will do so by executing a query that detects the dialect. This query can also be executed and cached automatically in the background during startup (see below). 2. `SessionPoolOptions#setAutoDetectDialect(true)` will cause the dialect detection query to be executed in the background automatically when a new client is created. This is disabled by default, except for when a Connection API connection (or anything that depends on that, such as JDBC) is opened. The reason for this default behavior is that a normal Spanner instance does normally not need to know what the dialect of the underlying database is, while the Connection API does. This reduces the number of times the detection query will be executed during production use.
1 parent 7095f94 commit 9eccfc4

22 files changed

+350
-149
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
<className>com/google/cloud/spanner/connection/Connection</className>
77
<method>com.google.cloud.spanner.Dialect getDialect()</method>
88
</difference>
9+
<difference>
10+
<differenceType>7012</differenceType>
11+
<className>com/google/cloud/spanner/DatabaseClient</className>
12+
<method>com.google.cloud.spanner.Dialect getDialect()</method>
13+
</difference>
914
<difference>
1015
<differenceType>7012</differenceType>
1116
<className>com/google/cloud/spanner/BatchReadOnlyTransaction</className>
@@ -15,4 +20,19 @@
1520
<differenceType>8001</differenceType>
1621
<className>com/google/cloud/spanner/connection/StatementParser</className>
1722
</difference>
23+
<difference>
24+
<differenceType>7002</differenceType>
25+
<className>com/google/cloud/spanner/SpannerOptions</className>
26+
<method>com.google.cloud.spanner.Dialect getDialect()</method>
27+
</difference>
28+
<difference>
29+
<differenceType>7002</differenceType>
30+
<className>com/google/cloud/spanner/SpannerOptions$Builder</className>
31+
<method>com.google.cloud.spanner.SpannerOptions$Builder setDialect(com.google.cloud.spanner.Dialect)</method>
32+
</difference>
33+
<difference>
34+
<differenceType>7002</differenceType>
35+
<className>com/google/cloud/spanner/connection/ConnectionOptions</className>
36+
<method>com.google.cloud.spanner.Dialect getDialect()</method>
37+
</difference>
1838
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
*/
2828
public interface DatabaseClient {
2929

30+
/**
31+
* Returns the SQL dialect that is used by the database.
32+
*
33+
* @return the SQL dialect that is used by the database.
34+
*/
35+
default Dialect getDialect() {
36+
throw new UnsupportedOperationException("method should be overwritten");
37+
}
38+
3039
/**
3140
* Writes the given mutations atomically to the database.
3241
*

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ PooledSessionFuture getSession() {
5353
return pool.getSession();
5454
}
5555

56+
@Override
57+
public Dialect getDialect() {
58+
return pool.getDialect();
59+
}
60+
5661
@Override
5762
public Timestamp write(final Iterable<Mutation> mutations) throws SpannerException {
5863
return writeWithOptions(mutations).getCommitTimestamp();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,34 @@ private void keepAlive() {
14841484
}
14851485
}
14861486

1487+
private void determineDialectAsync(final SettableFuture<Dialect> dialect) {
1488+
Preconditions.checkNotNull(dialect);
1489+
executor.submit(
1490+
() -> {
1491+
try {
1492+
dialect.set(determineDialect());
1493+
} catch (Throwable t) {
1494+
// Catch-all as we want to propagate all exceptions to anyone who might be interested
1495+
// in the database dialect, and there's nothing sensible that we can do with it here.
1496+
dialect.setException(t);
1497+
} finally {
1498+
releaseSession(this, Position.FIRST);
1499+
}
1500+
});
1501+
}
1502+
1503+
private Dialect determineDialect() {
1504+
try (ResultSet dialectResultSet =
1505+
delegate.singleUse().executeQuery(DETERMINE_DIALECT_STATEMENT)) {
1506+
if (dialectResultSet.next()) {
1507+
return Dialect.fromName(dialectResultSet.getString(0));
1508+
} else {
1509+
throw SpannerExceptionFactory.newSpannerException(
1510+
ErrorCode.NOT_FOUND, "No dialect found for database");
1511+
}
1512+
}
1513+
}
1514+
14871515
private void markBusy(Span span) {
14881516
this.delegate.setCurrentSpan(span);
14891517
this.state = SessionState.BUSY;
@@ -1724,7 +1752,27 @@ private enum Position {
17241752
RANDOM
17251753
}
17261754

1755+
/**
1756+
* This statement is (currently) used to determine the dialect of the database that is used by the
1757+
* session pool. This statement is subject to change when the INFORMATION_SCHEMA contains a table
1758+
* where the dialect of the database can be read directly, and any tests that want to detect the
1759+
* specific 'determine dialect statement' should rely on this constant instead of the actual
1760+
* value.
1761+
*/
1762+
@VisibleForTesting
1763+
static final Statement DETERMINE_DIALECT_STATEMENT =
1764+
Statement.newBuilder(
1765+
"SELECT 'POSTGRESQL' AS DIALECT\n"
1766+
+ "FROM INFORMATION_SCHEMA.SCHEMATA\n"
1767+
+ "WHERE SCHEMA_NAME='pg_catalog'\n"
1768+
+ "UNION ALL\n"
1769+
+ "SELECT 'GOOGLE_STANDARD_SQL' AS DIALECT\n"
1770+
+ "FROM INFORMATION_SCHEMA.SCHEMATA\n"
1771+
+ "WHERE SCHEMA_NAME='INFORMATION_SCHEMA' AND CATALOG_NAME=''")
1772+
.build();
1773+
17271774
private final SessionPoolOptions options;
1775+
private final SettableFuture<Dialect> dialect = SettableFuture.create();
17281776
private final SessionClient sessionClient;
17291777
private final ScheduledExecutorService executor;
17301778
private final ExecutorFactory<ScheduledExecutorService> executorFactory;
@@ -1734,6 +1782,9 @@ private enum Position {
17341782
private final Object lock = new Object();
17351783
private final Random random = new Random();
17361784

1785+
@GuardedBy("lock")
1786+
private boolean detectDialectStarted;
1787+
17371788
@GuardedBy("lock")
17381789
private int pendingClosure;
17391790

@@ -1861,6 +1912,39 @@ private SessionPool(
18611912
this.initMetricsCollection(metricRegistry, labelValues);
18621913
}
18631914

1915+
/**
1916+
* @return the {@link Dialect} of the underlying database. This method will block until the
1917+
* dialect is available. It will potentially execute one or two RPCs to get the dialect if
1918+
* necessary: One to create a session if there are no sessions in the pool (yet), and one to
1919+
* query the database for the dialect that is used. It is recommended that clients that always
1920+
* need to know the dialect set {@link
1921+
* SessionPoolOptions.Builder#setAutoDetectDialect(boolean)} to true. This will ensure that
1922+
* the dialect is fetched automatically in a background task when a session pool is created.
1923+
*/
1924+
Dialect getDialect() {
1925+
boolean mustDetectDialect = false;
1926+
synchronized (lock) {
1927+
if (!detectDialectStarted) {
1928+
mustDetectDialect = true;
1929+
detectDialectStarted = true;
1930+
}
1931+
}
1932+
if (mustDetectDialect) {
1933+
try (PooledSessionFuture session = getSession()) {
1934+
dialect.set(session.get().determineDialect());
1935+
}
1936+
}
1937+
try {
1938+
return dialect.get(60L, TimeUnit.SECONDS);
1939+
} catch (ExecutionException executionException) {
1940+
throw SpannerExceptionFactory.asSpannerException(executionException);
1941+
} catch (InterruptedException interruptedException) {
1942+
throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
1943+
} catch (TimeoutException timeoutException) {
1944+
throw SpannerExceptionFactory.propagateTimeout(timeoutException);
1945+
}
1946+
}
1947+
18641948
@VisibleForTesting
18651949
int getNumberOfSessionsInUse() {
18661950
synchronized (lock) {
@@ -2290,10 +2374,17 @@ public void onSessionReady(SessionImpl session) {
22902374
} else {
22912375
Preconditions.checkState(totalSessions() <= options.getMaxSessions() - 1);
22922376
allSessions.add(pooledSession);
2293-
// Release the session to a random position in the pool to prevent the case that a batch
2294-
// of sessions that are affiliated with the same channel are all placed sequentially in
2295-
// the pool.
2296-
releaseSession(pooledSession, Position.RANDOM);
2377+
if (options.isAutoDetectDialect() && !detectDialectStarted) {
2378+
// Get the dialect of the underlying database if that has not yet been done. Note that
2379+
// this method will release the session into the pool once it is done.
2380+
detectDialectStarted = true;
2381+
pooledSession.determineDialectAsync(SessionPool.this.dialect);
2382+
} else {
2383+
// Release the session to a random position in the pool to prevent the case that a batch
2384+
// of sessions that are affiliated with the same channel are all placed sequentially in
2385+
// the pool.
2386+
releaseSession(pooledSession, Position.RANDOM);
2387+
}
22972388
}
22982389
}
22992390
if (closeSession) {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class SessionPoolOptions {
5050
private final ActionOnSessionNotFound actionOnSessionNotFound;
5151
private final ActionOnSessionLeak actionOnSessionLeak;
5252
private final long initialWaitForSessionTimeoutMillis;
53+
private final boolean autoDetectDialect;
5354

5455
private SessionPoolOptions(Builder builder) {
5556
// minSessions > maxSessions is only possible if the user has only set a value for maxSessions.
@@ -67,6 +68,7 @@ private SessionPoolOptions(Builder builder) {
6768
this.loopFrequency = builder.loopFrequency;
6869
this.keepAliveIntervalMinutes = builder.keepAliveIntervalMinutes;
6970
this.removeInactiveSessionAfter = builder.removeInactiveSessionAfter;
71+
this.autoDetectDialect = builder.autoDetectDialect;
7072
}
7173

7274
@Override
@@ -87,7 +89,8 @@ public boolean equals(Object o) {
8789
this.initialWaitForSessionTimeoutMillis, other.initialWaitForSessionTimeoutMillis)
8890
&& Objects.equals(this.loopFrequency, other.loopFrequency)
8991
&& Objects.equals(this.keepAliveIntervalMinutes, other.keepAliveIntervalMinutes)
90-
&& Objects.equals(this.removeInactiveSessionAfter, other.removeInactiveSessionAfter);
92+
&& Objects.equals(this.removeInactiveSessionAfter, other.removeInactiveSessionAfter)
93+
&& Objects.equals(this.autoDetectDialect, other.autoDetectDialect);
9194
}
9295

9396
@Override
@@ -104,7 +107,8 @@ public int hashCode() {
104107
this.initialWaitForSessionTimeoutMillis,
105108
this.loopFrequency,
106109
this.keepAliveIntervalMinutes,
107-
this.removeInactiveSessionAfter);
110+
this.removeInactiveSessionAfter,
111+
this.autoDetectDialect);
108112
}
109113

110114
public Builder toBuilder() {
@@ -163,6 +167,10 @@ public boolean isBlockIfPoolExhausted() {
163167
return actionOnExhaustion == ActionOnExhaustion.BLOCK;
164168
}
165169

170+
public boolean isAutoDetectDialect() {
171+
return autoDetectDialect;
172+
}
173+
166174
@VisibleForTesting
167175
long getInitialWaitForSessionTimeoutMillis() {
168176
return initialWaitForSessionTimeoutMillis;
@@ -220,6 +228,7 @@ public static class Builder {
220228
private long loopFrequency = 10 * 1000L;
221229
private int keepAliveIntervalMinutes = 30;
222230
private Duration removeInactiveSessionAfter = Duration.ofMinutes(55L);
231+
private boolean autoDetectDialect = false;
223232

224233
public Builder() {}
225234

@@ -237,6 +246,7 @@ private Builder(SessionPoolOptions options) {
237246
this.loopFrequency = options.loopFrequency;
238247
this.keepAliveIntervalMinutes = options.keepAliveIntervalMinutes;
239248
this.removeInactiveSessionAfter = options.removeInactiveSessionAfter;
249+
this.autoDetectDialect = options.autoDetectDialect;
240250
}
241251

242252
/**
@@ -327,6 +337,24 @@ public Builder setBlockIfPoolExhausted() {
327337
return this;
328338
}
329339

340+
/**
341+
* Sets whether the client should automatically execute a background query to detect the dialect
342+
* that is used by the database or not. Set this option to true if you do not know what the
343+
* dialect of the database will be.
344+
*
345+
* <p>Note that you can always call {@link DatabaseClient#getDialect()} to get the dialect of a
346+
* database regardless of this setting, but by setting this to true, the value will be
347+
* pre-populated and cached in the client.
348+
*
349+
* @param autoDetectDialect Whether the client should automatically execute a background query
350+
* to detect the dialect of the underlying database
351+
* @return this builder for chaining
352+
*/
353+
public Builder setAutoDetectDialect(boolean autoDetectDialect) {
354+
this.autoDetectDialect = autoDetectDialect;
355+
return this;
356+
}
357+
330358
/**
331359
* The initial number of milliseconds to wait for a session to become available when one is
332360
* requested. The session pool will keep retrying to get a session, and the timeout will be

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
125125
private final CallCredentialsProvider callCredentialsProvider;
126126
private final CloseableExecutorProvider asyncExecutorProvider;
127127
private final String compressorName;
128-
private final Dialect dialect;
129128

130129
/**
131130
* Interface that can be used to provide {@link CallCredentials} instead of {@link Credentials} to
@@ -593,7 +592,6 @@ private SpannerOptions(Builder builder) {
593592
callCredentialsProvider = builder.callCredentialsProvider;
594593
asyncExecutorProvider = builder.asyncExecutorProvider;
595594
compressorName = builder.compressorName;
596-
dialect = builder.dialect;
597595
}
598596

599597
/**
@@ -693,7 +691,6 @@ public static class Builder
693691
private CloseableExecutorProvider asyncExecutorProvider;
694692
private String compressorName;
695693
private String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
696-
private Dialect dialect = Dialect.GOOGLE_STANDARD_SQL;
697694

698695
private Builder() {
699696
// Manually set retry and polling settings that work.
@@ -748,7 +745,6 @@ private Builder() {
748745
this.channelProvider = options.channelProvider;
749746
this.channelConfigurator = options.channelConfigurator;
750747
this.interceptorProvider = options.interceptorProvider;
751-
this.dialect = options.dialect;
752748
}
753749

754750
@Override
@@ -779,7 +775,6 @@ protected Set<String> getAllowedClientLibTokens() {
779775
* <li>{@link #setHost(String)}
780776
* <li>{@link #setNumChannels(int)}
781777
* <li>{@link #setInterceptorProvider(GrpcInterceptorProvider)}
782-
* <li>{@link #setDialect(Dialect)}
783778
* <li>{@link #setHeaderProvider(com.google.api.gax.rpc.HeaderProvider)}
784779
* </ol>
785780
*/
@@ -1139,16 +1134,6 @@ public Builder setEmulatorHost(String emulatorHost) {
11391134
return this;
11401135
}
11411136

1142-
/**
1143-
* Sets the {@link Dialect} to use with Cloud Spanner. The default is {@link
1144-
* Dialect#GOOGLE_STANDARD_SQL}.
1145-
*/
1146-
public Builder setDialect(Dialect dialect) {
1147-
Preconditions.checkNotNull(dialect);
1148-
this.dialect = dialect;
1149-
return this;
1150-
}
1151-
11521137
@SuppressWarnings("rawtypes")
11531138
@Override
11541139
public SpannerOptions build() {
@@ -1276,10 +1261,6 @@ public String getCompressorName() {
12761261
return compressorName;
12771262
}
12781263

1279-
public Dialect getDialect() {
1280-
return dialect;
1281-
}
1282-
12831264
/** Returns the default query options to use for the specific database. */
12841265
public QueryOptions getDefaultQueryOptions(DatabaseId databaseId) {
12851266
// Use the specific query options for the database if any have been specified. These have

0 commit comments

Comments
 (0)