Skip to content

Commit 49d4d09

Browse files
feat: Add PreparedStatement and update ExecuteQuery API to use it (#2534)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Rollback plan is reviewed and LGTMed - [ ] All new data plane features have a completed end to end testing plan Fixes #<issue_number_goes_here> ☕️ If you write sample code, please follow the [samples format]( https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
1 parent 4da61d6 commit 49d4d09

File tree

61 files changed

+5688
-1268
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+5688
-1268
lines changed

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

+76-14
Original file line numberDiff line numberDiff line change
@@ -282,20 +282,6 @@
282282
<method>*getTimestamp(*)</method>
283283
<to>java.time.Instant</to>
284284
</difference>
285-
<difference>
286-
<!-- BetaApi was updated -->
287-
<differenceType>7006</differenceType>
288-
<className>com/google/cloud/bigtable/data/v2/models/sql/StructReader</className>
289-
<method>*getTimestamp(*)</method>
290-
<to>java.time.Instant</to>
291-
</difference>
292-
<difference>
293-
<!-- BetaApi was updated -->
294-
<differenceType>7005</differenceType>
295-
<className>com/google/cloud/bigtable/data/v2/models/sql/Statement$Builder</className>
296-
<method>*setTimestampParam(java.lang.String, org.threeten.bp.Instant)</method>
297-
<to>*setTimestampParam(java.lang.String, java.time.Instant)</to>
298-
</difference>
299285
<difference>
300286
<!-- ChangeStream api is internal, only used by apache/beam-->
301287
<differenceType>7013</differenceType>
@@ -320,4 +306,80 @@
320306
<className>com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter</className>
321307
<method>*</method>
322308
</difference>
309+
<difference>
310+
<!-- BetaApi was updated -->
311+
<differenceType>7005</differenceType>
312+
<className>com/google/cloud/bigtable/data/v2/BigtableDataClient</className>
313+
<method>*executeQuery*</method>
314+
<to>*</to>
315+
</difference>
316+
<difference>
317+
<!-- BetaApi was renamed -->
318+
<differenceType>8001</differenceType>
319+
<className>com/google/cloud/bigtable/data/v2/models/sql/Statement</className>
320+
<method>*</method>
321+
</difference>
322+
<difference>
323+
<!-- BetaApi was renamed -->
324+
<differenceType>8001</differenceType>
325+
<className>com/google/cloud/bigtable/data/v2/models/sql/Statement$Builder</className>
326+
<method>*</method>
327+
</difference>
328+
<difference>
329+
<!-- BetaApi was renamed -->
330+
<differenceType>8001</differenceType>
331+
<className>com/google/cloud/bigtable/data/v2/models/sql/Statement$Builder</className>
332+
<method>*</method>
333+
</difference>
334+
<difference>
335+
<!-- InternalApi was updated -->
336+
<differenceType>7004</differenceType>
337+
<className>com/google/cloud/bigtable/data/v2/internal/SqlRowMergerUtil</className>
338+
<method>*</method>
339+
</difference>
340+
<difference>
341+
<!-- InternalApi was updated -->
342+
<differenceType>7004</differenceType>
343+
<className>com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallContext</className>
344+
<method>*ExecuteQueryCallContext*</method>
345+
</difference>
346+
<difference>
347+
<!-- InternalApi was updated -->
348+
<differenceType>7009</differenceType>
349+
<className>com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallContext</className>
350+
<method>*ExecuteQueryCallContext*</method>
351+
</difference>
352+
<difference>
353+
<!-- InternalApi was updated -->
354+
<differenceType>7005</differenceType>
355+
<className>com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallContext</className>
356+
<method>*create*</method>
357+
<to>*</to>
358+
</difference>
359+
<difference>
360+
<!-- InternalApi was updated -->
361+
<differenceType>7004</differenceType>
362+
<className>com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallable</className>
363+
<method>*ExecuteQueryCallable*</method>
364+
<to>*</to>
365+
</difference>
366+
<difference>
367+
<!-- InternalApi was updated -->
368+
<differenceType>7005</differenceType>
369+
<className>com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallable</className>
370+
<method>*call*</method>
371+
<to>*</to>
372+
</difference>
373+
<difference>
374+
<!-- InternalApi was updated -->
375+
<differenceType>8001</differenceType>
376+
<className>com/google/cloud/bigtable/data/v2/stub/sql/MetadataResolvingCallable</className>
377+
</difference>
378+
<difference>
379+
<!-- InternalApi was updated -->
380+
<differenceType>7004</differenceType>
381+
<className>com/google/cloud/bigtable/data/v2/stub/sql/SqlRowMerger</className>
382+
<method>*</method>
383+
<to>*</to>
384+
</difference>
323385
</differences>

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClient.java

+50-13
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import com.google.api.gax.rpc.ServerStream;
3131
import com.google.api.gax.rpc.ServerStreamingCallable;
3232
import com.google.api.gax.rpc.UnaryCallable;
33+
import com.google.cloud.bigtable.data.v2.internal.PrepareQueryRequest;
34+
import com.google.cloud.bigtable.data.v2.internal.PrepareResponse;
35+
import com.google.cloud.bigtable.data.v2.internal.PreparedStatementImpl;
3336
import com.google.cloud.bigtable.data.v2.internal.ResultSetImpl;
3437
import com.google.cloud.bigtable.data.v2.models.BulkMutation;
3538
import com.google.cloud.bigtable.data.v2.models.ChangeStreamRecord;
@@ -48,14 +51,17 @@
4851
import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest;
4952
import com.google.cloud.bigtable.data.v2.models.TableId;
5053
import com.google.cloud.bigtable.data.v2.models.TargetId;
54+
import com.google.cloud.bigtable.data.v2.models.sql.BoundStatement;
55+
import com.google.cloud.bigtable.data.v2.models.sql.PreparedStatement;
5156
import com.google.cloud.bigtable.data.v2.models.sql.ResultSet;
52-
import com.google.cloud.bigtable.data.v2.models.sql.Statement;
57+
import com.google.cloud.bigtable.data.v2.models.sql.SqlType;
5358
import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub;
5459
import com.google.cloud.bigtable.data.v2.stub.sql.SqlServerStream;
5560
import com.google.common.util.concurrent.MoreExecutors;
5661
import com.google.protobuf.ByteString;
5762
import java.io.IOException;
5863
import java.util.List;
64+
import java.util.Map;
5965
import javax.annotation.Nonnull;
6066
import javax.annotation.Nullable;
6167

@@ -2705,30 +2711,61 @@ public void readChangeStreamAsync(
27052711
* Executes a SQL Query and returns a ResultSet to iterate over the results. The returned
27062712
* ResultSet instance is not threadsafe, it can only be used from single thread.
27072713
*
2714+
* <p> The {@link BoundStatement} must be built from a {@link PreparedStatement} created using
2715+
* the same instance and app profile.
2716+
*
27082717
* <p>Sample code:
27092718
*
27102719
* <pre>{@code
27112720
* try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
27122721
* String query = "SELECT CAST(cf['stringCol'] AS STRING) FROM [TABLE]";
2713-
*
2714-
* try (ResultSet resultSet = bigtableDataClient.executeQuery(Statement.of(query))) {
2715-
* while (resultSet.next()) {
2716-
* String s = resultSet.getString("stringCol");
2717-
* // do something with data
2718-
* }
2719-
* } catch (RuntimeException e) {
2720-
* e.printStackTrace();
2722+
* Map<String, SqlType<?>> paramTypes = new HashMap<>();
2723+
* PreparedStatement preparedStatement = bigtableDataClient.prepareStatement(query, paramTypes));
2724+
* // Ideally one PreparedStatement should be reused across requests
2725+
* BoundStatement boundStatement = preparedStatement.bind()
2726+
* // set any query params before calling build
2727+
* .build();
2728+
* try (ResultSet resultSet = bigtableDataClient.executeQuery(boundStatement)) {
2729+
* while (resultSet.next()) {
2730+
* String s = resultSet.getString("stringCol");
2731+
* // do something with data
2732+
* }
2733+
* } catch (RuntimeException e) {
2734+
* e.printStackTrace();
27212735
* }
27222736
* }</pre>
27232737
*
2724-
* @see Statement For query options.
2738+
* @see {@link PreparedStatement} & {@link BoundStatement} for query options.
27252739
*/
2726-
@BetaApi
2727-
public ResultSet executeQuery(Statement statement) {
2728-
SqlServerStream stream = stub.createExecuteQueryCallable().call(statement);
2740+
public ResultSet executeQuery(BoundStatement boundStatement) {
2741+
boundStatement.assertUsingSameStub(stub);
2742+
SqlServerStream stream = stub.createExecuteQueryCallable().call(boundStatement);
27292743
return ResultSetImpl.create(stream);
27302744
}
27312745

2746+
/**
2747+
* Prepares a query for execution. If possible this should be called once and reused across
2748+
* requests. This will amortize the cost of query preparation.
2749+
*
2750+
* <p>A parameterized query should contain placeholders in the form of {@literal @} followed by
2751+
* the parameter name. Parameter names may consist of any combination of letters, numbers, and
2752+
* underscores.
2753+
*
2754+
* <p>Parameters can appear anywhere that a literal value is expected. The same parameter name can
2755+
* be used more than once, for example: {@code WHERE cf["qualifier1"] = @value OR cf["qualifier2"]
2756+
* = @value }
2757+
*
2758+
* @param query sql query string to prepare
2759+
* @param paramTypes a Map of the parameter names and the corresponding {@link SqlType} for all
2760+
* query parameters in 'query'
2761+
* @return {@link PreparedStatement} which is used to create {@link BoundStatement}s to execute
2762+
*/
2763+
public PreparedStatement prepareStatement(String query, Map<String, SqlType<?>> paramTypes) {
2764+
PrepareQueryRequest request = PrepareQueryRequest.create(query, paramTypes);
2765+
PrepareResponse response = stub.prepareQueryCallable().call(request);
2766+
return PreparedStatementImpl.create(response, paramTypes, request, stub);
2767+
}
2768+
27322769
/** Close the clients and releases all associated resources. */
27332770
@Override
27342771
public void close() {

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReader.java

+3-8
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.cloud.bigtable.data.v2.models.sql.StructReader;
2525
import com.google.common.base.Preconditions;
2626
import com.google.protobuf.ByteString;
27-
import com.google.protobuf.Timestamp;
2827
import java.time.Instant;
2928
import java.util.ArrayList;
3029
import java.util.Collections;
@@ -169,15 +168,15 @@ public boolean getBoolean(String columnName) {
169168
public Instant getTimestamp(int columnIndex) {
170169
checkNonNullOfType(columnIndex, SqlType.timestamp(), columnIndex);
171170
Value value = values().get(columnIndex);
172-
return toInstant(value.getTimestampValue());
171+
return TimestampUtil.toInstant(value.getTimestampValue());
173172
}
174173

175174
@Override
176175
public Instant getTimestamp(String columnName) {
177176
int columnIndex = getColumnIndex(columnName);
178177
checkNonNullOfType(columnIndex, SqlType.timestamp(), columnName);
179178
Value value = values().get(columnIndex);
180-
return toInstant(value.getTimestampValue());
179+
return TimestampUtil.toInstant(value.getTimestampValue());
181180
}
182181

183182
@Override
@@ -275,7 +274,7 @@ Object decodeValue(Value value, SqlType<?> type) {
275274
case BOOL:
276275
return value.getBoolValue();
277276
case TIMESTAMP:
278-
return toInstant(value.getTimestampValue());
277+
return TimestampUtil.toInstant(value.getTimestampValue());
279278
case DATE:
280279
return fromProto(value.getDateValue());
281280
case STRUCT:
@@ -329,10 +328,6 @@ private void checkNonNullOfType(
329328
}
330329
}
331330

332-
private Instant toInstant(Timestamp timestamp) {
333-
return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
334-
}
335-
336331
private Date fromProto(com.google.type.Date proto) {
337332
return Date.fromYearMonthDay(proto.getYear(), proto.getMonth(), proto.getDay());
338333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.auto.value.AutoValue;
20+
import com.google.bigtable.v2.Type;
21+
import com.google.cloud.bigtable.data.v2.models.sql.SqlType;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
/**
26+
* Internal representation of PrepareQueryRequest that handles conversion from user-facing types to
27+
* proto.
28+
*
29+
* <p>This is considered an internal implementation detail and should not be used by applications.
30+
*/
31+
@InternalApi("For internal use only")
32+
@AutoValue
33+
public abstract class PrepareQueryRequest {
34+
35+
public abstract String query();
36+
37+
public abstract Map<String, SqlType<?>> paramTypes();
38+
39+
public static PrepareQueryRequest create(String query, Map<String, SqlType<?>> paramTypes) {
40+
return new AutoValue_PrepareQueryRequest(query, paramTypes);
41+
}
42+
43+
public com.google.bigtable.v2.PrepareQueryRequest toProto(RequestContext requestContext) {
44+
HashMap<String, Type> protoParamTypes = new HashMap<>(paramTypes().size());
45+
for (Map.Entry<String, SqlType<?>> entry : paramTypes().entrySet()) {
46+
Type proto = QueryParamUtil.convertToQueryParamProto(entry.getValue());
47+
protoParamTypes.put(entry.getKey(), proto);
48+
}
49+
50+
return com.google.bigtable.v2.PrepareQueryRequest.newBuilder()
51+
.setInstanceName(
52+
NameUtil.formatInstanceName(
53+
requestContext.getProjectId(), requestContext.getInstanceId()))
54+
.setAppProfileId(requestContext.getAppProfileId())
55+
.setQuery(query())
56+
.putAllParamTypes(protoParamTypes)
57+
.build();
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.auto.value.AutoValue;
20+
import com.google.bigtable.v2.PrepareQueryResponse;
21+
import com.google.cloud.bigtable.data.v2.models.sql.ResultSetMetadata;
22+
import com.google.protobuf.ByteString;
23+
import java.time.Instant;
24+
25+
/**
26+
* Wrapper for results of a PrepareQuery call.
27+
*
28+
* <p>This should only be managed by {@link
29+
* com.google.cloud.bigtable.data.v2.models.sql.PreparedStatement}, and never used directly by users
30+
*
31+
* <p>This is considered an internal implementation detail and should not be used by applications.
32+
*/
33+
@InternalApi("For internal use only")
34+
@AutoValue
35+
public abstract class PrepareResponse {
36+
public abstract ResultSetMetadata resultSetMetadata();
37+
38+
public abstract ByteString preparedQuery();
39+
40+
public abstract Instant validUntil();
41+
42+
public static PrepareResponse fromProto(PrepareQueryResponse proto) {
43+
ResultSetMetadata metadata = ProtoResultSetMetadata.fromProto(proto.getMetadata());
44+
Instant validUntil = TimestampUtil.toInstant(proto.getValidUntil());
45+
return new AutoValue_PrepareResponse(metadata, proto.getPreparedQuery(), validUntil);
46+
}
47+
}

0 commit comments

Comments
 (0)