Skip to content

Commit d5cc437

Browse files
authored
feat(bigtable): add view_parameters support to BoundStatement (#13673)
This PR ports the Java client changes supporting `view_parameters` in `ExecuteQueryRequest` (originally developed and reviewed internally in CL 13080 / branch `add-view-parameters-support`). ### Changes - Added `view_parameters` support to `BoundStatement` and its builder. - Updated `BoundStatementTest` with unit tests for `view_parameters`. - Updated `BoundStatementDeserializer` in test-proxy to handle `view_parameters`. ### Verification - Confirmed `ExecuteQueryRequest` in generated GAPIC proto library already contains `view_parameters` support. - Verified compilation and build across `google-cloud-bigtable` and dependencies.
1 parent 56678e6 commit d5cc437

5 files changed

Lines changed: 232 additions & 5 deletions

File tree

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/BoundStatement.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,29 @@
4848
* paramName, Type value) for the appropriate type. For example:
4949
*
5050
* <pre>{@code
51-
* BoundStatementt boundStatement = preparedStatement.bind()
51+
* BoundStatement boundStatement = preparedStatement.bind()
5252
* .setBytesParam("qualifier", ByteString.copyFromUtf8("test"))
5353
* .setBytesParam("key", ByteString.copyFromUtf8("testKey"))
5454
* .build();
5555
* }</pre>
56+
*
57+
* <p>View parameters can also be specified on the statement using {@link
58+
* Builder#setStringViewParameter(String, String)}. This provides the runtime values returned by the
59+
* VIEW_PARAMETERS() function calls.
5660
*/
5761
public class BoundStatement {
5862

5963
private final PreparedStatementImpl preparedStatement;
6064
private final Map<String, Value> params;
65+
private final Map<String, Value> viewParameters;
6166

62-
private BoundStatement(PreparedStatementImpl preparedStatement, Map<String, Value> params) {
67+
private BoundStatement(
68+
PreparedStatementImpl preparedStatement,
69+
Map<String, Value> params,
70+
Map<String, Value> viewParameters) {
6371
this.preparedStatement = preparedStatement;
6472
this.params = params;
73+
this.viewParameters = viewParameters;
6574
}
6675

6776
/**
@@ -78,6 +87,7 @@ public static class Builder {
7887
private final PreparedStatementImpl preparedStatement;
7988
private final Map<String, SqlType<?>> paramTypes;
8089
private final Map<String, Value> params;
90+
private final Map<String, Value> viewParameters;
8191

8292
/**
8393
* Creates a builder from a {@link PreparedStatement}
@@ -90,6 +100,7 @@ public Builder(PreparedStatementImpl preparedStatement, Map<String, SqlType<?>>
90100
this.preparedStatement = preparedStatement;
91101
this.paramTypes = paramTypes;
92102
this.params = new HashMap<>();
103+
this.viewParameters = new HashMap<>();
93104
}
94105

95106
/** Builds a {@link BoundStatement} from the builder */
@@ -101,7 +112,19 @@ public BoundStatement build() {
101112
"Attempting to build BoundStatement without binding parameter: " + paramName);
102113
}
103114
}
104-
return new BoundStatement(preparedStatement, ImmutableMap.copyOf(params));
115+
return new BoundStatement(
116+
preparedStatement, ImmutableMap.copyOf(params), ImmutableMap.copyOf(viewParameters));
117+
}
118+
119+
/**
120+
* Sets a view parameter with the name {@code paramName} and the String typed value {@code
121+
* value}. This map provides the runtime values returned by the VIEW_PARAMETERS() function
122+
* calls.
123+
*/
124+
public Builder setStringViewParameter(String paramName, @Nullable String value) {
125+
Preconditions.checkNotNull(paramName, "paramName cannot be null");
126+
viewParameters.put(paramName, stringParamOf(value));
127+
return this;
105128
}
106129

107130
/**
@@ -372,7 +395,8 @@ public ExecuteQueryRequest toProto(
372395
requestContext.getProjectId(), requestContext.getInstanceId()))
373396
.setAppProfileId(requestContext.getAppProfileId())
374397
.setPreparedQuery(preparedQuery)
375-
.putAllParams(params);
398+
.putAllParams(params)
399+
.putAllViewParameters(viewParameters);
376400

377401
if (resumeToken != null) {
378402
requestBuilder.setResumeToken(resumeToken);

java-bigtable/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/sql/BoundStatementTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,4 +869,50 @@ public void builderValidatesAllParamsAreSet() {
869869
assertThat(e.getMessage())
870870
.contains("Attempting to build BoundStatement without binding parameter: bytesParam");
871871
}
872+
873+
@Test
874+
public void statementWithStringViewParameters() {
875+
Value stringVal = Value.newBuilder().setType(stringType()).setStringValue("alice").build();
876+
Value locationVal = Value.newBuilder().setType(stringType()).setStringValue("us-east1").build();
877+
878+
BoundStatement s =
879+
boundStatementBuilder()
880+
.setStringViewParameter("user_id", "alice")
881+
.setStringViewParameter("location", "us-east1")
882+
.build();
883+
884+
assertThat(s.toProto(EXPECTED_PREPARED_QUERY, REQUEST_CONTEXT, NO_RESUME_TOKEN))
885+
.isEqualTo(
886+
ExecuteQueryRequest.newBuilder()
887+
.setPreparedQuery(EXPECTED_PREPARED_QUERY)
888+
.setInstanceName(EXPECTED_INSTANCE_NAME)
889+
.setAppProfileId(EXPECTED_APP_PROFILE)
890+
.putViewParameters("user_id", stringVal)
891+
.putViewParameters("location", locationVal)
892+
.build());
893+
}
894+
895+
@Test
896+
public void statementWithNullStringViewParameter() {
897+
Value nullStringVal = Value.newBuilder().setType(stringType()).build();
898+
899+
BoundStatement s =
900+
boundStatementBuilder().setStringViewParameter("user_id", (String) null).build();
901+
902+
assertThat(s.toProto(EXPECTED_PREPARED_QUERY, REQUEST_CONTEXT, NO_RESUME_TOKEN))
903+
.isEqualTo(
904+
ExecuteQueryRequest.newBuilder()
905+
.setPreparedQuery(EXPECTED_PREPARED_QUERY)
906+
.setInstanceName(EXPECTED_INSTANCE_NAME)
907+
.setAppProfileId(EXPECTED_APP_PROFILE)
908+
.putViewParameters("user_id", nullStringVal)
909+
.build());
910+
}
911+
912+
@Test
913+
public void setStringViewParameterNullChecks() {
914+
BoundStatement.Builder builder = boundStatementBuilder();
915+
916+
assertThrows(NullPointerException.class, () -> builder.setStringViewParameter(null, "alice"));
917+
}
872918
}

java-bigtable/test-proxy/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@
5454
<groupId>com.google.protobuf</groupId>
5555
<artifactId>protobuf-java</artifactId>
5656
</dependency>
57+
<dependency>
58+
<groupId>junit</groupId>
59+
<artifactId>junit</artifactId>
60+
<scope>test</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>com.google.truth</groupId>
64+
<artifactId>truth</artifactId>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.mockito</groupId>
69+
<artifactId>mockito-core</artifactId>
70+
<scope>test</scope>
71+
</dependency>
5772
</dependencies>
5873

5974
<build>

java-bigtable/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/BoundStatementDeserializer.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
public class BoundStatementDeserializer {
3131

32-
static BoundStatement toBoundStatement(
32+
public static BoundStatement toBoundStatement(
3333
PreparedStatement preparedStatement, ExecuteQueryRequest request) {
3434
BoundStatement.Builder boundStatementBuilder = preparedStatement.bind();
3535
for (Map.Entry<String, Value> paramEntry : request.getRequest().getParamsMap().entrySet()) {
@@ -128,6 +128,23 @@ static BoundStatement toBoundStatement(
128128
throw new IllegalArgumentException("Unexpected query param type in param: " + value);
129129
}
130130
}
131+
for (Map.Entry<String, Value> entry : request.getRequest().getViewParametersMap().entrySet()) {
132+
String name = entry.getKey();
133+
Value value = entry.getValue();
134+
switch (value.getType().getKindCase()) {
135+
case STRING_TYPE:
136+
if (value.getKindCase().equals(KindCase.KIND_NOT_SET)) {
137+
boundStatementBuilder.setStringViewParameter(name, null);
138+
} else if (value.getKindCase().equals(KindCase.STRING_VALUE)) {
139+
boundStatementBuilder.setStringViewParameter(name, value.getStringValue());
140+
} else {
141+
throw new IllegalArgumentException("Malformed string value: " + value);
142+
}
143+
break;
144+
default:
145+
throw new IllegalArgumentException("Unexpected view parameter type in param: " + value);
146+
}
147+
}
131148
return boundStatementBuilder.build();
132149
}
133150

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2026 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.testproxy;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
import static org.junit.Assert.assertThrows;
20+
import static org.mockito.ArgumentMatchers.anyString;
21+
import static org.mockito.ArgumentMatchers.nullable;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
import com.google.bigtable.v2.ExecuteQueryRequest;
27+
import com.google.bigtable.v2.Type;
28+
import com.google.bigtable.v2.Value;
29+
import com.google.cloud.bigtable.data.v2.models.sql.BoundStatement;
30+
import com.google.cloud.bigtable.data.v2.models.sql.PreparedStatement;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.junit.runners.JUnit4;
34+
35+
@RunWith(JUnit4.class)
36+
public class BoundStatementDeserializerTest {
37+
38+
@Test
39+
public void testDeserializeStringViewParameter() {
40+
PreparedStatement preparedStatement = mock(PreparedStatement.class);
41+
BoundStatement.Builder builder = mock(BoundStatement.Builder.class);
42+
when(preparedStatement.bind()).thenReturn(builder);
43+
when(builder.setStringViewParameter(anyString(), nullable(String.class))).thenReturn(builder);
44+
45+
Value stringVal =
46+
Value.newBuilder()
47+
.setType(Type.newBuilder().setStringType(Type.String.getDefaultInstance()))
48+
.setStringValue("alice")
49+
.build();
50+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest request =
51+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest.newBuilder()
52+
.setRequest(ExecuteQueryRequest.newBuilder().putViewParameters("user_id", stringVal))
53+
.build();
54+
55+
BoundStatementDeserializer.toBoundStatement(preparedStatement, request);
56+
verify(builder).setStringViewParameter("user_id", "alice");
57+
}
58+
59+
@Test
60+
public void testDeserializeNullStringViewParameter() {
61+
PreparedStatement preparedStatement = mock(PreparedStatement.class);
62+
BoundStatement.Builder builder = mock(BoundStatement.Builder.class);
63+
when(preparedStatement.bind()).thenReturn(builder);
64+
when(builder.setStringViewParameter(anyString(), nullable(String.class))).thenReturn(builder);
65+
66+
Value nullStringVal =
67+
Value.newBuilder()
68+
.setType(Type.newBuilder().setStringType(Type.String.getDefaultInstance()))
69+
.build();
70+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest request =
71+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest.newBuilder()
72+
.setRequest(
73+
ExecuteQueryRequest.newBuilder().putViewParameters("user_id", nullStringVal))
74+
.build();
75+
76+
BoundStatementDeserializer.toBoundStatement(preparedStatement, request);
77+
verify(builder).setStringViewParameter("user_id", null);
78+
}
79+
80+
@Test
81+
public void testDeserializeInvalidTypeViewParameterThrows() {
82+
PreparedStatement preparedStatement = mock(PreparedStatement.class);
83+
BoundStatement.Builder builder = mock(BoundStatement.Builder.class);
84+
when(preparedStatement.bind()).thenReturn(builder);
85+
86+
Value intVal =
87+
Value.newBuilder()
88+
.setType(Type.newBuilder().setInt64Type(Type.Int64.getDefaultInstance()))
89+
.setIntValue(123)
90+
.build();
91+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest request =
92+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest.newBuilder()
93+
.setRequest(ExecuteQueryRequest.newBuilder().putViewParameters("user_id", intVal))
94+
.build();
95+
96+
IllegalArgumentException e =
97+
assertThrows(
98+
IllegalArgumentException.class,
99+
() -> BoundStatementDeserializer.toBoundStatement(preparedStatement, request));
100+
assertThat(e.getMessage()).contains("Unexpected view parameter type in param");
101+
}
102+
103+
@Test
104+
public void testDeserializeMalformedStringViewParameterThrows() {
105+
PreparedStatement preparedStatement = mock(PreparedStatement.class);
106+
BoundStatement.Builder builder = mock(BoundStatement.Builder.class);
107+
when(preparedStatement.bind()).thenReturn(builder);
108+
109+
Value malformedVal =
110+
Value.newBuilder()
111+
.setType(Type.newBuilder().setStringType(Type.String.getDefaultInstance()))
112+
.setIntValue(123)
113+
.build();
114+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest request =
115+
com.google.cloud.bigtable.testproxy.ExecuteQueryRequest.newBuilder()
116+
.setRequest(ExecuteQueryRequest.newBuilder().putViewParameters("user_id", malformedVal))
117+
.build();
118+
119+
IllegalArgumentException e =
120+
assertThrows(
121+
IllegalArgumentException.class,
122+
() -> BoundStatementDeserializer.toBoundStatement(preparedStatement, request));
123+
assertThat(e.getMessage()).contains("Malformed string value");
124+
}
125+
}

0 commit comments

Comments
 (0)