Skip to content

Commit 12f1a4b

Browse files
authored
Merge branch 'main' into revert-3818-revert-3724-afe_metric
2 parents 9f5c6e5 + 1afd815 commit 12f1a4b

File tree

10 files changed

+906
-1
lines changed

10 files changed

+906
-1
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -952,5 +952,9 @@
952952
<method>boolean supportsExplain()</method>
953953
</difference>
954954

955-
955+
<difference>
956+
<differenceType>7012</differenceType>
957+
<className>com/google/cloud/spanner/DatabaseClient</className>
958+
<method>com.google.cloud.spanner.Statement$StatementFactory getStatementFactory()</method>
959+
</difference>
956960
</differences>

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

+21
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.cloud.spanner.Options.RpcPriority;
2222
import com.google.cloud.spanner.Options.TransactionOption;
2323
import com.google.cloud.spanner.Options.UpdateOption;
24+
import com.google.cloud.spanner.Statement.StatementFactory;
2425
import com.google.spanner.v1.BatchWriteResponse;
2526
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
2627

@@ -606,4 +607,24 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
606607
* idempotent, such as deleting old rows from a very large table.
607608
*/
608609
long executePartitionedUpdate(Statement stmt, UpdateOption... options);
610+
611+
/**
612+
* Returns a {@link StatementFactory} for the given dialect.
613+
*
614+
* <p>A {@link StatementFactory} can be used to create statements with unnamed parameters. This is
615+
* primarily intended for framework developers who want to integrate the Spanner client with
616+
* frameworks that use unnamed parameters. Developers who just want to use the Spanner client in
617+
* their application, should use named parameters.
618+
*
619+
* <p>Examples using {@link StatementFactory}
620+
*
621+
* <pre>{@code
622+
* Statement statement = databaseClient
623+
* .getStatementFactory()
624+
* .withUnnamedParameters("SELECT NAME FROM TABLE WHERE ID = ?", 10);
625+
* }</pre>
626+
*/
627+
default StatementFactory getStatementFactory() {
628+
throw new UnsupportedOperationException("method should be overwritten");
629+
}
609630
}

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

+37
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@
2222
import com.google.cloud.spanner.Options.UpdateOption;
2323
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
2424
import com.google.cloud.spanner.SpannerImpl.ClosedException;
25+
import com.google.cloud.spanner.Statement.StatementFactory;
2526
import com.google.common.annotations.VisibleForTesting;
2627
import com.google.common.base.Function;
2728
import com.google.common.util.concurrent.ListenableFuture;
2829
import com.google.spanner.v1.BatchWriteResponse;
2930
import io.opentelemetry.api.common.Attributes;
31+
import java.util.concurrent.ExecutionException;
32+
import java.util.concurrent.Future;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.TimeoutException;
3035
import javax.annotation.Nullable;
3136

3237
class DatabaseClientImpl implements DatabaseClient {
@@ -139,6 +144,30 @@ public Dialect getDialect() {
139144
return pool.getDialect();
140145
}
141146

147+
private final AbstractLazyInitializer<StatementFactory> statementFactorySupplier =
148+
new AbstractLazyInitializer<StatementFactory>() {
149+
@Override
150+
protected StatementFactory initialize() {
151+
try {
152+
Dialect dialect = getDialectAsync().get(30, TimeUnit.SECONDS);
153+
return new StatementFactory(dialect);
154+
} catch (ExecutionException | TimeoutException e) {
155+
throw SpannerExceptionFactory.asSpannerException(e);
156+
} catch (InterruptedException e) {
157+
throw SpannerExceptionFactory.propagateInterrupt(e);
158+
}
159+
}
160+
};
161+
162+
@Override
163+
public StatementFactory getStatementFactory() {
164+
try {
165+
return statementFactorySupplier.get();
166+
} catch (Exception exception) {
167+
throw SpannerExceptionFactory.asSpannerException(exception);
168+
}
169+
}
170+
142171
@Override
143172
@Nullable
144173
public String getDatabaseRole() {
@@ -346,6 +375,14 @@ public long executePartitionedUpdate(final Statement stmt, final UpdateOption...
346375
return executePartitionedUpdateWithPooledSession(stmt, options);
347376
}
348377

378+
private Future<Dialect> getDialectAsync() {
379+
MultiplexedSessionDatabaseClient client = getMultiplexedSessionDatabaseClient();
380+
if (client != null) {
381+
return client.getDialectAsync();
382+
}
383+
return pool.getDialectAsync();
384+
}
385+
349386
private long executePartitionedUpdateWithPooledSession(
350387
final Statement stmt, final UpdateOption... options) {
351388
ISpan span = tracer.spanBuilder(PARTITION_DML_TRANSACTION, commonAttributes);

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

+9
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.Map;
4545
import java.util.concurrent.ExecutionException;
4646
import java.util.concurrent.Executors;
47+
import java.util.concurrent.Future;
4748
import java.util.concurrent.ScheduledExecutorService;
4849
import java.util.concurrent.ScheduledFuture;
4950
import java.util.concurrent.TimeUnit;
@@ -652,6 +653,14 @@ public Dialect getDialect() {
652653
}
653654
}
654655

656+
Future<Dialect> getDialectAsync() {
657+
try {
658+
return MAINTAINER_SERVICE.submit(dialectSupplier::get);
659+
} catch (Exception exception) {
660+
throw SpannerExceptionFactory.asSpannerException(exception);
661+
}
662+
}
663+
655664
@Override
656665
public Timestamp write(Iterable<Mutation> mutations) throws SpannerException {
657666
return createMultiplexedSessionTransaction(/* singleUse = */ false).write(mutations);

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

+5
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
import java.util.concurrent.CountDownLatch;
105105
import java.util.concurrent.ExecutionException;
106106
import java.util.concurrent.Executor;
107+
import java.util.concurrent.Future;
107108
import java.util.concurrent.ScheduledExecutorService;
108109
import java.util.concurrent.ScheduledFuture;
109110
import java.util.concurrent.TimeUnit;
@@ -2548,6 +2549,10 @@ Dialect getDialect() {
25482549
}
25492550
}
25502551

2552+
Future<Dialect> getDialectAsync() {
2553+
return executor.submit(this::getDialect);
2554+
}
2555+
25512556
PooledSessionReplacementHandler getPooledSessionReplacementHandler() {
25522557
return pooledSessionReplacementHandler;
25532558
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
17+
package com.google.cloud.spanner;
18+
19+
import com.google.cloud.Date;
20+
import com.google.protobuf.ListValue;
21+
import java.time.LocalDate;
22+
import java.time.LocalDateTime;
23+
import java.time.OffsetDateTime;
24+
import java.time.ZoneId;
25+
import java.time.ZonedDateTime;
26+
import java.time.format.DateTimeFormatter;
27+
import java.time.temporal.TemporalAccessor;
28+
import java.util.ArrayList;
29+
import java.util.Iterator;
30+
import java.util.List;
31+
import java.util.function.Consumer;
32+
import java.util.function.Function;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
36+
final class SpannerTypeConverter {
37+
38+
private static final ZoneId UTC_ZONE = ZoneId.of("UTC");
39+
private static final DateTimeFormatter ISO_8601_DATE_FORMATTER =
40+
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
41+
42+
static <T> Value createUntypedArrayValue(Stream<T> stream) {
43+
List<com.google.protobuf.Value> values =
44+
stream
45+
.map(
46+
val ->
47+
com.google.protobuf.Value.newBuilder()
48+
.setStringValue(String.valueOf(val))
49+
.build())
50+
.collect(Collectors.toList());
51+
return Value.untyped(
52+
com.google.protobuf.Value.newBuilder()
53+
.setListValue(ListValue.newBuilder().addAllValues(values).build())
54+
.build());
55+
}
56+
57+
static <T extends TemporalAccessor> String convertToISO8601(T dateTime) {
58+
return ISO_8601_DATE_FORMATTER.format(dateTime);
59+
}
60+
61+
static <T> Value createUntypedStringValue(T value) {
62+
return Value.untyped(
63+
com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build());
64+
}
65+
66+
static <T, U> Iterable<U> convertToTypedIterable(
67+
Function<T, U> func, T val, Iterator<?> iterator) {
68+
List<U> values = new ArrayList<>();
69+
SpannerTypeConverter.processIterable(val, iterator, func, values::add);
70+
return values;
71+
}
72+
73+
static <T> Iterable<T> convertToTypedIterable(T val, Iterator<?> iterator) {
74+
return convertToTypedIterable(v -> v, val, iterator);
75+
}
76+
77+
@SuppressWarnings("unchecked")
78+
static <T, U> void processIterable(
79+
T val, Iterator<?> iterator, Function<T, U> func, Consumer<U> consumer) {
80+
consumer.accept(func.apply(val));
81+
iterator.forEachRemaining(values -> consumer.accept(func.apply((T) values)));
82+
}
83+
84+
static Date convertLocalDateToSpannerDate(LocalDate date) {
85+
return Date.fromYearMonthDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
86+
}
87+
88+
static <T> Value createUntypedIterableValue(
89+
T value, Iterator<?> iterator, Function<T, String> func) {
90+
ListValue.Builder listValueBuilder = ListValue.newBuilder();
91+
SpannerTypeConverter.processIterable(
92+
value,
93+
iterator,
94+
(val) -> com.google.protobuf.Value.newBuilder().setStringValue(func.apply(val)).build(),
95+
listValueBuilder::addValues);
96+
return Value.untyped(
97+
com.google.protobuf.Value.newBuilder().setListValue(listValueBuilder.build()).build());
98+
}
99+
100+
static ZonedDateTime atUTC(LocalDateTime localDateTime) {
101+
return atUTC(localDateTime.atZone(ZoneId.systemDefault()));
102+
}
103+
104+
static ZonedDateTime atUTC(OffsetDateTime localDateTime) {
105+
return localDateTime.atZoneSameInstant(UTC_ZONE);
106+
}
107+
108+
static ZonedDateTime atUTC(ZonedDateTime localDateTime) {
109+
return localDateTime.withZoneSameInstant(UTC_ZONE);
110+
}
111+
}

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

+101
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static com.google.common.base.Preconditions.checkState;
2121

2222
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
23+
import com.google.cloud.spanner.connection.AbstractStatementParser;
24+
import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
2325
import com.google.common.base.Preconditions;
2426
import com.google.common.collect.ImmutableMap;
2527
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
@@ -251,4 +253,103 @@ StringBuilder toString(StringBuilder b) {
251253
}
252254
return b;
253255
}
256+
257+
/**
258+
* Factory for creating {@link Statement}s with unnamed parameters.
259+
*
260+
* <p>This class is primarily intended for framework developers who want to integrate the Spanner
261+
* client with a framework that uses unnamed parameters. Developers who want to use the Spanner
262+
* client in their application, should use named parameters.
263+
*
264+
* <p>
265+
*
266+
* <h2>Usage Example</h2>
267+
*
268+
* Simple SQL query
269+
*
270+
* <pre>{@code
271+
* Statement statement = databaseClient.getStatementFactory()
272+
* .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = ?", 10L)
273+
* }</pre>
274+
*
275+
* SQL query with multiple parameters
276+
*
277+
* <pre>{@code
278+
* long id = 10L;
279+
* String name = "google";
280+
* List<String> phoneNumbers = Arrays.asList("1234567890", "0987654321");
281+
* Statement statement = databaseClient.getStatementFactory()
282+
* .withUnnamedParameters("INSERT INTO TABLE (ID, name, phonenumbers) VALUES(?, ?, ?)", id, name, phoneNumbers)
283+
* }</pre>
284+
*
285+
* How to use arrays with the IN operator
286+
*
287+
* <pre>{@code
288+
* long[] ids = {10L, 12L, 1483L};
289+
* Statement statement = databaseClient.getStatementFactory()
290+
* .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = UNNEST(?)", ids)
291+
* }</pre>
292+
*
293+
* @see DatabaseClient#getStatementFactory()
294+
* @see StatementFactory#withUnnamedParameters(String, Object...)
295+
*/
296+
public static final class StatementFactory {
297+
private final Dialect dialect;
298+
299+
StatementFactory(Dialect dialect) {
300+
this.dialect = dialect;
301+
}
302+
303+
public Statement of(String sql) {
304+
return Statement.of(sql);
305+
}
306+
307+
/**
308+
* This function accepts a SQL statement with unnamed parameters (?) and accepts a list of
309+
* objects that should be used as the values for those parameters. Primitive types are
310+
* supported.
311+
*
312+
* <p>For parameters of type DATE, the following types are supported
313+
*
314+
* <ul>
315+
* <li>{@link java.time.LocalDate}
316+
* <li>{@link com.google.cloud.Date}
317+
* </ul>
318+
*
319+
* <p>For parameters of type TIMESTAMP, the following types are supported. Note that Spanner
320+
* stores all timestamps in UTC. Instances of ZonedDateTime and OffsetDateTime that use other
321+
* timezones than UTC, will be converted to the corresponding UTC values before being sent to
322+
* Spanner. Instances of LocalDateTime will be converted to a ZonedDateTime using the system
323+
* default timezone, and then converted to UTC before being sent to Spanner.
324+
*
325+
* <ul>
326+
* <li>{@link java.time.LocalDateTime}
327+
* <li>{@link java.time.OffsetDateTime}
328+
* <li>{@link java.time.ZonedDateTime}
329+
* </ul>
330+
*
331+
* <p>
332+
*
333+
* @param sql SQL statement with unnamed parameters denoted as ?
334+
* @param values positional list of values for the unnamed parameters in the SQL string
335+
* @return Statement a statement that can be executed on Spanner
336+
* @see DatabaseClient#getStatementFactory
337+
*/
338+
public Statement withUnnamedParameters(String sql, Object... values) {
339+
Map<String, Value> parameters = getUnnamedParametersMap(values);
340+
AbstractStatementParser statementParser = AbstractStatementParser.getInstance(this.dialect);
341+
ParametersInfo parametersInfo =
342+
statementParser.convertPositionalParametersToNamedParameters('?', sql);
343+
return new Statement(parametersInfo.sqlWithNamedParameters, parameters, null);
344+
}
345+
346+
private Map<String, Value> getUnnamedParametersMap(Object[] values) {
347+
Map<String, Value> parameters = new HashMap<>();
348+
int index = 1;
349+
for (Object value : values) {
350+
parameters.put("p" + (index++), Value.toValue(value));
351+
}
352+
return parameters;
353+
}
354+
}
254355
}

0 commit comments

Comments
 (0)