Skip to content

Commit e96e172

Browse files
authored
feat: add backend query options (#90)
* feat: add backend query options Adds support for setting QueryOptions that will be used by the backend to execute queries. * fix: set QueryOptions on Statement QueryOptions should be an option on a Statement instead of a parameter to the executeQuery method. By setting these options on a Statement, it is possible to use it with analyzeQuery as well. * feat: add toBuilder() method to Statement * fix: code review comments * fix: remove unused interface and class
1 parent 57f5fd0 commit e96e172

17 files changed

+983
-85
lines changed

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

Lines changed: 162 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
import com.google.cloud.spanner.Options.ReadOption;
3131
import com.google.cloud.spanner.SessionImpl.SessionTransaction;
3232
import com.google.cloud.spanner.spi.v1.SpannerRpc;
33+
import com.google.common.annotations.VisibleForTesting;
3334
import com.google.protobuf.ByteString;
3435
import com.google.spanner.v1.BeginTransactionRequest;
3536
import com.google.spanner.v1.ExecuteBatchDmlRequest;
3637
import com.google.spanner.v1.ExecuteSqlRequest;
3738
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
39+
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
3840
import com.google.spanner.v1.PartialResultSet;
3941
import com.google.spanner.v1.ReadRequest;
4042
import com.google.spanner.v1.Transaction;
@@ -53,20 +55,86 @@
5355
*/
5456
abstract class AbstractReadContext
5557
implements ReadContext, AbstractResultSet.Listener, SessionTransaction {
58+
59+
abstract static class Builder<B extends Builder<?, T>, T extends AbstractReadContext> {
60+
private SessionImpl session;
61+
private SpannerRpc rpc;
62+
private Span span = Tracing.getTracer().getCurrentSpan();
63+
private int defaultPrefetchChunks = SpannerOptions.Builder.DEFAULT_PREFETCH_CHUNKS;
64+
private QueryOptions defaultQueryOptions = SpannerOptions.Builder.DEFAULT_QUERY_OPTIONS;
65+
66+
Builder() {}
67+
68+
@SuppressWarnings("unchecked")
69+
B self() {
70+
return (B) this;
71+
}
72+
73+
B setSession(SessionImpl session) {
74+
this.session = session;
75+
return self();
76+
}
77+
78+
B setRpc(SpannerRpc rpc) {
79+
this.rpc = rpc;
80+
return self();
81+
}
82+
83+
B setSpan(Span span) {
84+
this.span = span;
85+
return self();
86+
}
87+
88+
B setDefaultPrefetchChunks(int defaultPrefetchChunks) {
89+
this.defaultPrefetchChunks = defaultPrefetchChunks;
90+
return self();
91+
}
92+
93+
B setDefaultQueryOptions(QueryOptions defaultQueryOptions) {
94+
this.defaultQueryOptions = defaultQueryOptions;
95+
return self();
96+
}
97+
98+
abstract T build();
99+
}
100+
56101
/**
57102
* A {@code ReadContext} for standalone reads. This can only be used for a single operation, since
58103
* each standalone read may see a different timestamp of Cloud Spanner data.
59104
*/
60105
static class SingleReadContext extends AbstractReadContext {
106+
static class Builder extends AbstractReadContext.Builder<Builder, SingleReadContext> {
107+
private TimestampBound bound;
108+
109+
private Builder() {}
110+
111+
Builder setTimestampBound(TimestampBound bound) {
112+
this.bound = bound;
113+
return self();
114+
}
115+
116+
@Override
117+
SingleReadContext build() {
118+
return new SingleReadContext(this);
119+
}
120+
121+
SingleUseReadOnlyTransaction buildSingleUseReadOnlyTransaction() {
122+
return new SingleUseReadOnlyTransaction(this);
123+
}
124+
}
125+
126+
static Builder newBuilder() {
127+
return new Builder();
128+
}
129+
61130
final TimestampBound bound;
62131

63132
@GuardedBy("lock")
64133
private boolean used;
65134

66-
SingleReadContext(
67-
SessionImpl session, TimestampBound bound, SpannerRpc rpc, int defaultPrefetchChunks) {
68-
super(session, rpc, defaultPrefetchChunks);
69-
this.bound = bound;
135+
private SingleReadContext(Builder builder) {
136+
super(builder);
137+
this.bound = builder.bound;
70138
}
71139

72140
@GuardedBy("lock")
@@ -99,9 +167,8 @@ static class SingleUseReadOnlyTransaction extends SingleReadContext
99167
@GuardedBy("lock")
100168
private Timestamp timestamp;
101169

102-
SingleUseReadOnlyTransaction(
103-
SessionImpl session, TimestampBound bound, SpannerRpc rpc, int defaultPrefetchChunks) {
104-
super(session, bound, rpc, defaultPrefetchChunks);
170+
private SingleUseReadOnlyTransaction(SingleReadContext.Builder builder) {
171+
super(builder);
105172
}
106173

107174
@Override
@@ -139,6 +206,38 @@ public void onTransactionMetadata(Transaction transaction) {
139206

140207
static class MultiUseReadOnlyTransaction extends AbstractReadContext
141208
implements ReadOnlyTransaction {
209+
static class Builder extends AbstractReadContext.Builder<Builder, MultiUseReadOnlyTransaction> {
210+
private TimestampBound bound;
211+
private Timestamp timestamp;
212+
private ByteString transactionId;
213+
214+
private Builder() {}
215+
216+
Builder setTimestampBound(TimestampBound bound) {
217+
this.bound = bound;
218+
return this;
219+
}
220+
221+
Builder setTimestamp(Timestamp timestamp) {
222+
this.timestamp = timestamp;
223+
return this;
224+
}
225+
226+
Builder setTransactionId(ByteString transactionId) {
227+
this.transactionId = transactionId;
228+
return this;
229+
}
230+
231+
@Override
232+
MultiUseReadOnlyTransaction build() {
233+
return new MultiUseReadOnlyTransaction(this);
234+
}
235+
}
236+
237+
static Builder newBuilder() {
238+
return new Builder();
239+
}
240+
142241
private TimestampBound bound;
143242
private final Object txnLock = new Object();
144243

@@ -148,27 +247,24 @@ static class MultiUseReadOnlyTransaction extends AbstractReadContext
148247
@GuardedBy("txnLock")
149248
private ByteString transactionId;
150249

151-
MultiUseReadOnlyTransaction(
152-
SessionImpl session, TimestampBound bound, SpannerRpc rpc, int defaultPrefetchChunks) {
153-
super(session, rpc, defaultPrefetchChunks);
250+
MultiUseReadOnlyTransaction(Builder builder) {
251+
super(builder);
154252
checkArgument(
155-
bound.getMode() != TimestampBound.Mode.MAX_STALENESS
156-
&& bound.getMode() != TimestampBound.Mode.MIN_READ_TIMESTAMP,
157-
"Bounded staleness mode %s is not supported for multi-use read-only transactions."
158-
+ " Create a single-use read or read-only transaction instead.",
159-
bound.getMode());
160-
this.bound = bound;
161-
}
162-
163-
MultiUseReadOnlyTransaction(
164-
SessionImpl session,
165-
ByteString transactionId,
166-
Timestamp timestamp,
167-
SpannerRpc rpc,
168-
int defaultPrefetchChunks) {
169-
super(session, rpc, defaultPrefetchChunks);
170-
this.transactionId = transactionId;
171-
this.timestamp = timestamp;
253+
!(builder.bound != null && builder.transactionId != null)
254+
&& !(builder.bound == null && builder.transactionId == null),
255+
"Either TimestampBound or TransactionId must be specified");
256+
if (builder.bound != null) {
257+
checkArgument(
258+
builder.bound.getMode() != TimestampBound.Mode.MAX_STALENESS
259+
&& builder.bound.getMode() != TimestampBound.Mode.MIN_READ_TIMESTAMP,
260+
"Bounded staleness mode %s is not supported for multi-use read-only transactions."
261+
+ " Create a single-use read or read-only transaction instead.",
262+
builder.bound.getMode());
263+
this.bound = builder.bound;
264+
} else {
265+
this.timestamp = builder.timestamp;
266+
this.transactionId = builder.transactionId;
267+
}
172268
}
173269

174270
@Override
@@ -256,6 +352,7 @@ void initTransaction() {
256352
final SpannerRpc rpc;
257353
final Span span;
258354
private final int defaultPrefetchChunks;
355+
private final QueryOptions defaultQueryOptions;
259356

260357
@GuardedBy("lock")
261358
private boolean isValid = true;
@@ -271,16 +368,12 @@ void initTransaction() {
271368
// much more frequently.
272369
private static final int MAX_BUFFERED_CHUNKS = 512;
273370

274-
AbstractReadContext(SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks) {
275-
this(session, rpc, defaultPrefetchChunks, Tracing.getTracer().getCurrentSpan());
276-
}
277-
278-
private AbstractReadContext(
279-
SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks, Span span) {
280-
this.session = session;
281-
this.rpc = rpc;
282-
this.defaultPrefetchChunks = defaultPrefetchChunks;
283-
this.span = span;
371+
AbstractReadContext(Builder<?, ?> builder) {
372+
this.session = builder.session;
373+
this.rpc = builder.rpc;
374+
this.defaultPrefetchChunks = builder.defaultPrefetchChunks;
375+
this.defaultQueryOptions = builder.defaultQueryOptions;
376+
this.span = builder.span;
284377
}
285378

286379
long getSeqNo() {
@@ -341,9 +434,36 @@ private ResultSet executeQueryInternal(
341434
Statement statement,
342435
com.google.spanner.v1.ExecuteSqlRequest.QueryMode queryMode,
343436
QueryOption... options) {
344-
Options readOptions = Options.fromQueryOptions(options);
437+
Options queryOptions = Options.fromQueryOptions(options);
345438
return executeQueryInternalWithOptions(
346-
statement, queryMode, readOptions, null /*partitionToken*/);
439+
statement, queryMode, queryOptions, null /*partitionToken*/);
440+
}
441+
442+
/**
443+
* Determines the {@link QueryOptions} to use for a query. This is determined using the following
444+
* precedence:
445+
*
446+
* <ol>
447+
* <li>Specific {@link QueryOptions} passed in for this query.
448+
* <li>Any value specified in a valid environment variable when the {@link SpannerOptions}
449+
* instance was created.
450+
* <li>The default {@link SpannerOptions#getDefaultQueryOptions()} specified for the database
451+
* where the query is executed.
452+
* </ol>
453+
*/
454+
@VisibleForTesting
455+
QueryOptions buildQueryOptions(QueryOptions requestOptions) {
456+
// Shortcut for the most common return value.
457+
if (defaultQueryOptions.equals(QueryOptions.getDefaultInstance()) && requestOptions == null) {
458+
return QueryOptions.getDefaultInstance();
459+
}
460+
// Create a builder based on the default query options.
461+
QueryOptions.Builder builder = defaultQueryOptions.toBuilder();
462+
// Then overwrite with specific options for this query.
463+
if (requestOptions != null) {
464+
builder.mergeFrom(requestOptions);
465+
}
466+
return builder.build();
347467
}
348468

349469
ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder(Statement statement, QueryMode queryMode) {
@@ -365,6 +485,7 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder(Statement statement, Query
365485
builder.setTransaction(selector);
366486
}
367487
builder.setSeqno(getSeqNo());
488+
builder.setQueryOptions(buildQueryOptions(statement.getQueryOptions()));
368489
return builder;
369490
}
370491

@@ -400,15 +521,15 @@ ExecuteBatchDmlRequest.Builder getExecuteBatchDmlRequestBuilder(Iterable<Stateme
400521
ResultSet executeQueryInternalWithOptions(
401522
Statement statement,
402523
com.google.spanner.v1.ExecuteSqlRequest.QueryMode queryMode,
403-
Options readOptions,
524+
Options options,
404525
ByteString partitionToken) {
405526
beforeReadOrQuery();
406527
final ExecuteSqlRequest.Builder request = getExecuteSqlRequestBuilder(statement, queryMode);
407528
if (partitionToken != null) {
408529
request.setPartitionToken(partitionToken);
409530
}
410531
final int prefetchChunks =
411-
readOptions.hasPrefetchChunks() ? readOptions.prefetchChunks() : defaultPrefetchChunks;
532+
options.hasPrefetchChunks() ? options.prefetchChunks() : defaultPrefetchChunks;
412533
ResumableStreamIterator stream =
413534
new ResumableStreamIterator(MAX_BUFFERED_CHUNKS, SpannerImpl.QUERY, span) {
414535
@Override

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,48 @@ public class BatchClientImpl implements BatchClient {
4545
public BatchReadOnlyTransaction batchReadOnlyTransaction(TimestampBound bound) {
4646
SessionImpl session = sessionClient.createSession();
4747
return new BatchReadOnlyTransactionImpl(
48-
sessionClient.getSpanner(), session, checkNotNull(bound));
48+
MultiUseReadOnlyTransaction.newBuilder()
49+
.setSession(session)
50+
.setRpc(sessionClient.getSpanner().getRpc())
51+
.setTimestampBound(bound)
52+
.setDefaultQueryOptions(
53+
sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId()))
54+
.setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()),
55+
checkNotNull(bound));
4956
}
5057

5158
@Override
5259
public BatchReadOnlyTransaction batchReadOnlyTransaction(BatchTransactionId batchTransactionId) {
5360
SessionImpl session =
5461
sessionClient.sessionWithId(checkNotNull(batchTransactionId).getSessionId());
5562
return new BatchReadOnlyTransactionImpl(
56-
sessionClient.getSpanner(), session, batchTransactionId);
63+
MultiUseReadOnlyTransaction.newBuilder()
64+
.setSession(session)
65+
.setRpc(sessionClient.getSpanner().getRpc())
66+
.setTransactionId(batchTransactionId.getTransactionId())
67+
.setTimestamp(batchTransactionId.getTimestamp())
68+
.setDefaultQueryOptions(
69+
sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId()))
70+
.setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()),
71+
batchTransactionId);
5772
}
5873

5974
private static class BatchReadOnlyTransactionImpl extends MultiUseReadOnlyTransaction
6075
implements BatchReadOnlyTransaction {
6176
private final String sessionName;
6277
private final Map<SpannerRpc.Option, ?> options;
6378

64-
BatchReadOnlyTransactionImpl(SpannerImpl spanner, SessionImpl session, TimestampBound bound) {
65-
super(
66-
checkNotNull(session),
67-
checkNotNull(bound),
68-
checkNotNull(spanner).getOptions().getSpannerRpcV1(),
69-
spanner.getOptions().getPrefetchChunks());
79+
BatchReadOnlyTransactionImpl(
80+
MultiUseReadOnlyTransaction.Builder builder, TimestampBound bound) {
81+
super(builder.setTimestampBound(bound));
7082
this.sessionName = session.getName();
7183
this.options = session.getOptions();
7284
initTransaction();
7385
}
7486

7587
BatchReadOnlyTransactionImpl(
76-
SpannerImpl spanner, SessionImpl session, BatchTransactionId batchTransactionId) {
77-
super(
78-
checkNotNull(session),
79-
checkNotNull(batchTransactionId).getTransactionId(),
80-
batchTransactionId.getTimestamp(),
81-
checkNotNull(spanner).getOptions().getSpannerRpcV1(),
82-
spanner.getOptions().getPrefetchChunks());
88+
MultiUseReadOnlyTransaction.Builder builder, BatchTransactionId batchTransactionId) {
89+
super(builder.setTransactionId(batchTransactionId.getTransactionId()));
8390
this.sessionName = session.getName();
8491
this.options = session.getOptions();
8592
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ SpannerImpl getSpanner() {
193193
return spanner;
194194
}
195195

196+
DatabaseId getDatabaseId() {
197+
return db;
198+
}
199+
196200
/** Create a single session. */
197201
SessionImpl createSession() {
198202
// The sessionChannelCounter could overflow, but that will just flip it to Integer.MIN_VALUE,

0 commit comments

Comments
 (0)