-
Notifications
You must be signed in to change notification settings - Fork 131
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
Changes from 11 commits
eebf2d0
29a5c5a
3f42072
899400c
d677799
cf7b397
fb618d5
07e5569
c8390b1
668be97
05713f3
0a7594c
1cd0c6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* 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.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.Consumer; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
final class SpannerTypeConverter { | ||
|
||
private static final ZoneId UTC_ZONE = ZoneId.of("UTC"); | ||
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 createUntypedStringValue(T value) { | ||
return Value.untyped( | ||
com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build()); | ||
} | ||
|
||
static <T, U> Iterable<U> convertToTypedIterable( | ||
Function<T, U> func, T val, Iterator<?> iterator) { | ||
List<U> values = new ArrayList<>(); | ||
SpannerTypeConverter.processIterable(val, iterator, func, values::add); | ||
return values; | ||
} | ||
|
||
static <T> Iterable<T> convertToTypedIterable(T val, Iterator<?> iterator) { | ||
olavloite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return convertToTypedIterable(v -> v, val, iterator); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
static <T, U> void processIterable( | ||
T val, Iterator<?> iterator, Function<T, U> func, Consumer<U> consumer) { | ||
consumer.accept(func.apply(val)); | ||
iterator.forEachRemaining(values -> consumer.accept(func.apply((T) values))); | ||
} | ||
|
||
static Date convertLocalDateToSpannerDate(LocalDate date) { | ||
return Date.fromYearMonthDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth()); | ||
} | ||
|
||
static <T> Value createUntypedIterableValue( | ||
T value, Iterator<?> iterator, Function<T, String> func) { | ||
ListValue.Builder listValueBuilder = ListValue.newBuilder(); | ||
SpannerTypeConverter.processIterable( | ||
value, | ||
iterator, | ||
(val) -> com.google.protobuf.Value.newBuilder().setStringValue(func.apply(val)).build(), | ||
listValueBuilder::addValues); | ||
return Value.untyped( | ||
com.google.protobuf.Value.newBuilder().setListValue(listValueBuilder.build()).build()); | ||
} | ||
|
||
static ZonedDateTime atUTC(LocalDateTime localDateTime) { | ||
return atUTC(localDateTime.atZone(ZoneId.systemDefault())); | ||
} | ||
|
||
static ZonedDateTime atUTC(OffsetDateTime localDateTime) { | ||
return localDateTime.atZoneSameInstant(UTC_ZONE); | ||
} | ||
|
||
static ZonedDateTime atUTC(ZonedDateTime localDateTime) { | ||
return localDateTime.withZoneSameInstant(UTC_ZONE); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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; | ||||||||||||||||||
|
@@ -246,4 +248,90 @@ StringBuilder toString(StringBuilder b) { | |||||||||||||||||
} | ||||||||||||||||||
return b; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* Factory for creating {@link Statement}. | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* | ||||||||||||||||||
* <p>This factory class supports creating {@link Statement} with positional(or unnamed) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||||||||||||
* parameters. | ||||||||||||||||||
* | ||||||||||||||||||
* <p> | ||||||||||||||||||
* | ||||||||||||||||||
* <h2>Usage Example</h2> | ||||||||||||||||||
* | ||||||||||||||||||
* Simple SQL query | ||||||||||||||||||
* | ||||||||||||||||||
* <pre>{@code | ||||||||||||||||||
* Statement statement = databaseClient.getStatementFactory() | ||||||||||||||||||
* .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = ?", 10L) | ||||||||||||||||||
* }</pre> | ||||||||||||||||||
* | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add an example here (so after the simple query, but before the array example) that shows how to use it with multiple parameters. This could for example be an INSERT statement that inserts a row with 2 or 3 column values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||||||||||||||||||
* How to use SQL queries with IN command | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* | ||||||||||||||||||
* <pre>{@code | ||||||||||||||||||
* long[] ids = {10L, 12L, 1483L}; | ||||||||||||||||||
* Statement statement = databaseClient.getStatementFactory() | ||||||||||||||||||
* .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = UNNEST(?)", ids) | ||||||||||||||||||
* }</pre> | ||||||||||||||||||
* | ||||||||||||||||||
* @see DatabaseClient#getStatementFactory() | ||||||||||||||||||
* @see StatementFactory#withUnnamedParameters(String, Object...) | ||||||||||||||||||
*/ | ||||||||||||||||||
public static final class StatementFactory { | ||||||||||||||||||
olavloite marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
private final Dialect dialect; | ||||||||||||||||||
|
||||||||||||||||||
StatementFactory(Dialect dialect) { | ||||||||||||||||||
this.dialect = dialect; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
public Statement of(String sql) { | ||||||||||||||||||
return Statement.of(sql); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* This function accepts the SQL statement with unnamed parameters(?) and accepts the list of | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* objects to replace unnamed parameters. Primitive types are supported. | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* | ||||||||||||||||||
* <p>For Date column, following types are supported | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* | ||||||||||||||||||
* <ul> | ||||||||||||||||||
* <li>java.util.Date | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* <li>LocalDate | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* <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 | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
* | ||||||||||||||||||
* <ul> | ||||||||||||||||||
* <li>LocalDateTime | ||||||||||||||||||
* <li>OffsetDateTime | ||||||||||||||||||
* <li>ZonedDateTime | ||||||||||||||||||
* </ul> | ||||||||||||||||||
* | ||||||||||||||||||
* <p> | ||||||||||||||||||
* | ||||||||||||||||||
* @param sql SQL statement with unnamed parameters denoted as ? | ||||||||||||||||||
* @param values positional list of values for the unnamed parameters in the SQL string | ||||||||||||||||||
* @return Statement a statement that can be executed on Spanner | ||||||||||||||||||
* @see DatabaseClient#getStatementFactory | ||||||||||||||||||
*/ | ||||||||||||||||||
public Statement withUnnamedParameters(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; | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.