8000 feat: support unnamed parameters by sakthivelmanii · Pull Request #3820 · googleapis/java-spanner · GitHub
[go: up one dir, main page]

Skip to content

feat: support unnamed parameters #3820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 16, 2025
6 changes: 5 additions & 1 deletion google-cloud-spanner/clirr-ignored-differences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -945,5 +945,9 @@
<method>boolean supportsExplain()</method>
</difference>


<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/DatabaseClient</className>
<method>com.google.cloud.spanner.Statement$StatementFactory newStatementFactory()</method>
</difference>
</differences>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.Options.UpdateOption;
import com.google.cloud.spanner.Statement.StatementFactory;
import com.google.spanner.v1.BatchWriteResponse;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;

Expand Down Expand Up @@ -606,4 +607,16 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
* idempotent, such as deleting old rows from a very large table.
*/
long executePartitionedUpdate(Statement stmt, UpdateOption... options);

/**
* Returns StatementFactory for the given dialect. With StatementFactory, unnamed parameterized
* queries can be passed along with the values to create a Statement.
*
* <p>Examples using {@link StatementFactory}
*
* <p>databaseClient.newStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10)
*/
default StatementFactory newStatementFactory() {
throw new UnsupportedOperationException("method should be overwritten");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
import com.google.cloud.spanner.Options.UpdateOption;
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
import com.google.cloud.spanner.SpannerImpl.ClosedException;
import com.google.cloud.spanner.Statement.StatementFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.spanner.v1.BatchWriteResponse;
import io.opentelemetry.api.common.Attributes;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;

class DatabaseClientImpl implements DatabaseClient {
Expand All @@ -41,6 +46,8 @@ class DatabaseClientImpl implements DatabaseClient {
@VisibleForTesting final boolean useMultiplexedSessionPartitionedOps;
@VisibleForTesting final boolean useMultiplexedSessionForRW;

private StatementFactory statementFactory = null;

final boolean useMultiplexedSessionBlindWrite;

@VisibleForTesting
Expand Down Expand Up @@ -139,6 +146,21 @@ public Dialect getDialect() {
return pool.getDialect();
}

@Override
public StatementFactory newStatementFactory() {
if (statementFactory == null) {
try {
Dialect dialect = getDialectAsync().get(5, TimeUnit.SECONDS);
statementFactory = new StatementFactory(dialect);
} catch (ExecutionException | TimeoutException e) {
throw SpannerExceptionFactory.asSpannerException(e);
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
}
}
return statementFactory;
}

@Override
@Nullable
public String getDatabaseRole() {
Expand Down Expand Up @@ -346,6 +368,14 @@ public long executePartitionedUpdate(final Statement stmt, final UpdateOption...
return executePartitionedUpdateWithPooledSession(stmt, options);
}

private Future<Dialect> getDialectAsync() {
MultiplexedSessionDatabaseClient client = getMultiplexedSessionDatabaseClient();
if (client != null) {
return client.getDialectAsync();
}
return pool.getDialectAsync();
}

private long executePartitionedUpdateWithPooledSession(
final Statement stmt, final UpdateOption... options) {
ISpan span = tracer.spanBuilder(PARTITION_DML_TRANSACTION, commonAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -652,6 +653,14 @@ public Dialect getDialect() {
}
}

public Future<Dialect> getDialectAsync() {
try {
return MAINTAINER_SERVICE.submit(dialectSupplier::get);
} catch (Exception exception) {
throw SpannerExceptionFactory.asSpannerException(exception);
}
}

@Override
public Timestamp write(Iterable<Mutation> mutations) throws SpannerException {
return createMultiplexedSessionTransaction(/* singleUse = */ false).write(mutations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -2548,6 +2549,10 @@ Dialect getDialect() {
}
}

Future<Dialect> getDialectAsync() {
return executor.submit(this::getDialect);
}

PooledSessionReplacementHandler getPooledSessionReplacementHandler() {
return pooledSessionReplacementHandler;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner;

import com.google.cloud.Date;
import com.google.protobuf.ListValue;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

final class SpannerTypeConverter {

private static final String DATE_PATTERN = "yyyy-MM-dd";
private static final SimpleDateFormat SIMPLE_DATE_FORMATTER = new SimpleDateFormat(DATE_PATTERN);
private static final ZoneId UTC_ZONE = ZoneId.of("UTC");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
private static final DateTimeFormatter ISO_8601_DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");

static <T> Value createUntypedArrayValue(Stream<T> stream) {
List<com.google.protobuf.Value> values =
stream
.map(
val ->
com.google.protobuf.Value.newBuilder()
.setStringValue(String.valueOf(val))
.build())
.collect(Collectors.toList());
return Value.untyped(
com.google.protobuf.Value.newBuilder()
.setListValue(ListValue.newBuilder().addAllValues(values).build())
.build());
}

static <T extends TemporalAccessor> String convertToISO8601(T dateTime) {
return ISO_8601_DATE_FORMATTER.format(dateTime);
}

static <T> Value createUntypedValue(T value) {
return Value.untyped(
com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build());
}

@SuppressWarnings("unchecked")
static <T, U> Iterable<U> convertToTypedIterable(
Function<T, U> func, T val, Iterator<?> iterator) {
List<U> values = new ArrayList<>();
values.add(func.apply(val));
iterator.forEachRemaining(value -> values.add(func.apply((T) value)));
return values;
}

static <T> Iterable<T> convertToTypedIterable(T val, Iterator<?> iterator) {
return convertToTypedIterable(v -> v, val, iterator);
}

static Date convertUtilDateToSpannerDate(java.util.Date date) {
return Date.parseDate(SIMPLE_DATE_FORMATTER.format(date));
}

static Date convertLocalDateToSpannerDate(LocalDate date) {
return Date.parseDate(DATE_FORMATTER.format(date));
}

static <T> Value createUntypedIterableValue(
T value, Iterator<?> iterator, Function<T, String> func) {
return Value.untyped(
com.google.protobuf.Value.newBuilder()
.setListValue(
ListValue.newBuilder()
.addAllValues(
SpannerTypeConverter.convertToTypedIterable(
(val) ->
com.google.protobuf.Value.newBuilder()
.setStringValue(func.apply(val))
.build(),
value,
iterator)))
.build());
}

static ZonedDateTime convertToUTCTimezone(LocalDateTime localDateTime) {
return localDateTime.atZone(UTC_ZONE);
}

static ZonedDateTime convertToUTCTimezone(OffsetDateTime localDateTime) {
return localDateTime.atZoneSameInstant(UTC_ZONE);
}

static ZonedDateTime convertToUTCTimezone(ZonedDateTime localDateTime) {
return localDateTime.withZoneSameInstant(UTC_ZONE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import static com.google.common.base.Preconditions.checkState;

import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
Expand Down Expand Up @@ -246,4 +248,56 @@ StringBuilder toString(StringBuilder b) {
}
return b;
}

public static final class StatementFactory {
private final Dialect dialect;

StatementFactory(Dialect dialect) {
this.dialect = dialect;
}

public Statement of(String sql) {
return Statement.of(sql);
}

/**
* @param sql SQL statement with unnamed parameter denoted as ?
* @param values list of values which needs to replace ? in the sql
* @return Statement object
* <p>This function accepts the SQL statement with unnamed parameters(?) and accepts the
* list of objects to replace unnamed parameters. Primitive types are supported
* <p>For Date column, following types are supported
* <ul>
* <li>java.util.Date
* <li>LocalDate
* <li>com.google.cloud.Date
* </ul>
* <p>For Timestamp column, following types are supported. All the dates should be in UTC
* format. Incase if the timezone is not in UTC, spanner client will convert that to UTC
* automatically
* <ul>
* <li>LocalDateTime
* <li>OffsetDateTime
* <li>ZonedDateTime
* </ul>
* <p>
* @see DatabaseClient#newStatementFactory
*/
public Statement of(String sql, Object... values) {
Map<String, Value> parameters = getUnnamedParametersMap(values);
AbstractStatementParser statementParser = AbstractStatementParser.getInstance(this.dialect);
ParametersInfo parametersInfo =
statementParser.convertPositionalParametersToNamedParameters('?', sql);
return new Statement(parametersInfo.sqlWithNamedParameters, parameters, null);
}

private Map<String, Value> getUnnamedParametersMap(Object[] values) {
Map<String, Value> parameters = new HashMap<>();
int index = 1;
for (Object value : values) {
parameters.put("p" + (index++), Value.toValue(value));
}
return parameters;
}
}
}
Loading
Loading
0