From eebf2d0c33af9698eae9c05781426c49e9f60e6b Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Wed, 9 Apr 2025 15:00:37 +0530 Subject: [PATCH 01/13] feat: support unnamed parameters --- .../google/cloud/spanner/DatabaseClient.java | 13 + .../cloud/spanner/SpannerTypeConverter.java | 118 +++++++ .../com/google/cloud/spanner/Statement.java | 54 +++ .../java/com/google/cloud/spanner/Value.java | 177 ++++++++++ .../cloud/spanner/DatabaseClientImplTest.java | 17 + .../com/google/cloud/spanner/ValueTest.java | 319 ++++++++++++++++++ 6 files changed, 698 insertions(+) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index a33f39d47fd..00b8d6295f5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -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; @@ -606,4 +607,16 @@ ServerStream 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. + * + *

Examples using {@link StatementFactory} + * + *

databaseClient.newStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10) + */ + default StatementFactory newStatementFactory() { + return new StatementFactory(getDialect()); + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java new file mode 100644 index 00000000000..bd36ef69aa0 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java @@ -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 Value createUntypedArrayValue(Stream stream) { + List 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 String convertToISO8601(T dateTime) { + return ISO_8601_DATE_FORMATTER.format(dateTime); + } + + static Value createUntypedValue(T value) { + return Value.untyped( + com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build()); + } + + @SuppressWarnings("unchecked") + static Iterable convertToTypedIterable( + Function func, T val, Iterator iterator) { + List values = new ArrayList<>(); + values.add(func.apply(val)); + iterator.forEachRemaining(value -> values.add(func.apply((T) value))); + return values; + } + + static Iterable 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 Value createUntypedIterableValue( + T value, Iterator iterator, Function 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); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index a9cf3e7dec4..8625ac0da92 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -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,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 + *

This function accepts the SQL statement with unnamed parameters(?) and accepts the + * list of objects to replace unnamed parameters. Primitive types are supported + *

For Date column, following types are supported + *

+ *

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 + *

    + *
  • LocalDateTime + *
  • OffsetDateTime + *
  • ZonedDateTime + *
+ *

+ * @see DatabaseClient#newStatementFactory + */ + public Statement of(String sql, Object... values) { + Map parameters = getUnnamedParametersMap(values); + AbstractStatementParser statementParser = AbstractStatementParser.getInstance(this.dialect); + ParametersInfo parametersInfo = + statementParser.convertPositionalParametersToNamedParameters('?', sql); + return new Statement(parametersInfo.sqlWithNamedParameters, parameters, null); + } + + private Map getUnnamedParametersMap(Object[] values) { + Map parameters = new HashMap<>(); + int index = 1; + for (Object value : values) { + parameters.put("p" + (index++), Value.toValue(value)); + } + return parameters; + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 6cb68eeee4a..1388d3fd32e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -16,6 +16,14 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.SpannerTypeConverter.convertLocalDateToSpannerDate; +import static com.google.cloud.spanner.SpannerTypeConverter.convertToISO8601; +import static com.google.cloud.spanner.SpannerTypeConverter.convertToTypedIterable; +import static com.google.cloud.spanner.SpannerTypeConverter.convertToUTCTimezone; +import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedArrayValue; +import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedIterableValue; +import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedValue; + import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; @@ -39,16 +47,23 @@ import java.io.Serializable; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.BitSet; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -90,6 +105,8 @@ public abstract class Value implements Serializable { static final com.google.protobuf.Value NULL_PROTO = com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + private static final ZoneId UTC_ZONE = ZoneId.of("UTC"); + /** Constant to specify a PG Numeric NaN value. */ public static final String NAN = "NaN"; @@ -824,6 +841,166 @@ public static Value structArray(Type elementType, @Nullable Iterable v) private Value() {} + static Value toValue(Object value) { + if (value instanceof Value) { + return (Value) value; + } + if (value instanceof Boolean) { + return Value.bool((Boolean) value); + } + if (value instanceof Long || value instanceof Integer) { + return createUntypedValue(String.valueOf(value)); + } + if (value instanceof Float) { + return Value.float32((Float) value); + } + if (value instanceof Double) { + return Value.float64((Double) value); + } + if (value instanceof BigDecimal) { + return Value.numeric((BigDecimal) value); + } + if (value instanceof ByteArray) { + return Value.bytes((ByteArray) value); + } + if (value instanceof byte[]) { + return Value.bytes(ByteArray.copyFrom((byte[]) value)); + } + if (value instanceof Date) { + return Value.date((Date) value); + } + if (value instanceof java.util.Date) { + return Value.date(SpannerTypeConverter.convertUtilDateToSpannerDate((java.util.Date) value)); + } + if (value instanceof LocalDate) { + return Value.date(convertLocalDateToSpannerDate((LocalDate) value)); + } + if (value instanceof LocalDateTime) { + return createUntypedValue(convertToISO8601(convertToUTCTimezone((LocalDateTime) value))); + } + if (value instanceof OffsetDateTime) { + return createUntypedValue(convertToISO8601(convertToUTCTimezone((OffsetDateTime) value))); + } + if (value instanceof ZonedDateTime) { + return createUntypedValue(convertToISO8601(convertToUTCTimezone((ZonedDateTime) value))); + } + if (value instanceof ProtocolMessageEnum) { + return Value.protoEnum((ProtocolMessageEnum) value); + } + if (value instanceof AbstractMessage) { + return Value.protoMessage((AbstractMessage) value); + } + if (value instanceof Interval) { + return Value.interval((Interval) value); + } + if (value instanceof Struct) { + return Value.struct((Struct) value); + } + if (value instanceof Timestamp) { + return Value.timestamp((Timestamp) value); + } + if (value instanceof Iterable) { + Iterator iterator = ((Iterable) value).iterator(); + if (!iterator.hasNext()) { + return createUntypedArrayValue(Stream.empty()); + } + Object object = iterator.next(); + if (object instanceof Boolean) { + return Value.boolArray(convertToTypedIterable((Boolean) object, iterator)); + } + if (object instanceof Integer) { + return createUntypedIterableValue((Integer) object, iterator, String::valueOf); + } + if (object instanceof Long) { + return createUntypedIterableValue((Long) object, iterator, String::valueOf); + } + if (object instanceof Float) { + return Value.float32Array(convertToTypedIterable((Float) object, iterator)); + } + if (object instanceof Double) { + return Value.float64Array(convertToTypedIterable((Double) object, iterator)); + } + if (object instanceof BigDecimal) { + return Value.numericArray(convertToTypedIterable((BigDecimal) object, iterator)); + } + if (object instanceof ByteArray) { + return Value.bytesArray(convertToTypedIterable((ByteArray) object, iterator)); + } + if (object instanceof byte[]) { + return Value.bytesArray( + SpannerTypeConverter.convertToTypedIterable( + ByteArray::copyFrom, (byte[]) object, iterator)); + } + if (object instanceof Interval) { + return Value.intervalArray(convertToTypedIterable((Interval) object, iterator)); + } + if (object instanceof Timestamp) { + return Value.timestampArray(convertToTypedIterable((Timestamp) object, iterator)); + } + if (object instanceof Date) { + return Value.dateArray(convertToTypedIterable((Date) object, iterator)); + } + if (object instanceof java.util.Date) { + return Value.dateArray( + convertToTypedIterable( + SpannerTypeConverter::convertUtilDateToSpannerDate, + (java.util.Date) object, + iterator)); + } + if (object instanceof LocalDate) { + return Value.dateArray( + SpannerTypeConverter.convertToTypedIterable( + SpannerTypeConverter::convertLocalDateToSpannerDate, (LocalDate) object, iterator)); + } + if (object instanceof LocalDateTime) { + return createUntypedIterableValue( + (LocalDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + } + if (object instanceof OffsetDateTime) { + return createUntypedIterableValue( + (OffsetDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + } + if (object instanceof ZonedDateTime) { + return createUntypedIterableValue( + (ZonedDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + } + } + + // array and primitive array + if (value instanceof Boolean[]) { + return Value.boolArray(Arrays.asList((Boolean[]) value)); + } + if (value instanceof boolean[]) { + return Value.boolArray((boolean[]) value); + } + if (value instanceof Float[]) { + return Value.float32Array(Arrays.asList((Float[]) value)); + } + if (value instanceof float[]) { + return Value.float32Array((float[]) value); + } + if (value instanceof Double[]) { + return Value.float64Array(Arrays.asList((Double[]) value)); + } + if (value instanceof double[]) { + return Value.float64Array((double[]) value); + } + if (value instanceof Long[]) { + return createUntypedArrayValue(Arrays.stream((Long[]) value)); + } + if (value instanceof long[]) { + return createUntypedArrayValue(Arrays.stream((long[]) value).boxed()); + } + if (value instanceof Integer[]) { + return createUntypedArrayValue(Arrays.stream((Integer[]) value)); + } + if (value instanceof int[]) { + return createUntypedArrayValue(Arrays.stream((int[]) value).boxed()); + } + + return createUntypedValue(value); + } + /** Returns the type of this value. This will return a type even if {@code isNull()} is true. */ public abstract Type getType(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index 0fb4af2e8c7..b6fd51ac15c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -4896,6 +4896,23 @@ public void testMetadataUnknownTypes() { } } + @Test + public void testStatementWithIUnnamedParametersParameter() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = client.newStatementFactory().of("select id from test where b=?", true); + Statement generatedStatement = + Statement.newBuilder("select id from test where b=@p1").bind("p1").to(true).build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + @Test public void testStatementWithBytesArrayParameter() { Statement statement = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 3c96a482340..8045a99296c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -42,16 +42,28 @@ import com.google.common.testing.EqualsTester; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; +import com.google.protobuf.ProtocolMessageEnum; +import com.google.spanner.v1.PartialResultSet; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import org.junit.Test; @@ -2496,6 +2508,313 @@ public void verifyBrokenSerialization() { reserializeAndAssert(BrokenSerializationList.of(1, 2, 3)); } + @Test + public void testToValue() { + int i = 10; + Value value = Value.toValue(i); + assertNull(value.getType()); + assertEquals("10", value.getAsString()); + + Integer j = 10; + value = Value.toValue(j); + assertNull(value.getType()); + assertEquals("10", value.getAsString()); + + long k = 10L; + value = Value.toValue(k); + assertNull(value.getType()); + assertEquals("10", value.getAsString()); + + Long l = 10L; + value = Value.toValue(i); + assertNull(value.getType()); + assertEquals("10", value.getAsString()); + + boolean m = true; + value = Value.toValue(m); + assertEquals(Type.bool(), value.getType()); + assertTrue(value.getBool()); + + Boolean n = true; + value = Value.toValue(n); + assertEquals(Type.bool(), value.getType()); + assertTrue(value.getBool()); + + Float o = 0.3f; + value = Value.toValue(o); + assertEquals(Type.float32(), value.getType()); + assertEquals(0.3f, value.getFloat32(), 0); + + float p = 0.3f; + value = Value.toValue(p); + assertEquals(Type.float32(), value.getType()); + assertEquals(0.3f, value.getFloat32(), 0); + + Double q = 0.4d; + value = Value.toValue(q); + assertEquals(Type.float64(), value.getType()); + assertEquals(0.4d, value.getFloat64(), 0); + + double s = 0.5d; + value = Value.toValue(s); + assertEquals(Type.float64(), value.getType()); + assertEquals(0.5d, value.getFloat64(), 0); + + BigDecimal t = BigDecimal.valueOf(0.6d); + value = Value.toValue(t); + assertEquals(Type.numeric(), value.getType()); + assertEquals(t, value.getNumeric()); + + ByteArray bytes = ByteArray.copyFrom("hello"); + value = Value.toValue(bytes); + assertEquals(Type.bytes(), value.getType()); + assertEquals(bytes, value.getBytes()); + + byte[] byteArray = "hello".getBytes(); + value = Value.toValue(byteArray); + assertEquals(Type.bytes(), value.getType()); + assertEquals(bytes, value.getBytes()); + + Date date = Date.fromYearMonthDay(2018, 2, 26); + value = Value.toValue(date); + assertEquals(Type.date(), value.getType()); + assertEquals(date, value.getDate()); + + java.util.Date utilDate = + java.util.Date.from( + Instant.from(LocalDateTime.of(2018, 2, 26, 11, 30).toInstant(ZoneOffset.UTC))); + value = Value.toValue(utilDate); + assertEquals(Type.date(), value.getType()); + assertEquals(date, value.getDate()); + + LocalDate localDate = LocalDate.of(2018, 2, 26); + value = Value.toValue(localDate); + assertEquals(Type.date(), value.getType()); + assertEquals(date, value.getDate()); + + LocalDateTime localDateTime = LocalDateTime.of(2018, 2, 26, 11, 30, 10); + value = Value.toValue(localDateTime); + assertNull(value.getType()); + assertEquals("2018-02-26T11:30:10.000Z", value.getAsString()); + + OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(10)); + value = Value.toValue(offsetDateTime); + assertNull(value.getType()); + assertEquals("2018-02-26T01:30:10.000Z", value.getAsString()); + + ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Kolkata")); + value = Value.toValue(zonedDateTime); + assertNull(value.getType()); + assertEquals("2018-02-26T06:00:10.000Z", value.getAsString()); + + ProtocolMessageEnum protocolMessageEnum = IsolationLevel.SERIALIZABLE; + value = Value.toValue(protocolMessageEnum); + assertEquals( + Type.protoEnum("google.spanner.v1.TransactionOptions.IsolationLevel"), value.getType()); + assertEquals( + protocolMessageEnum, + value.getProtoEnum( + (val -> { + switch (val) { + case 1: + return IsolationLevel.SERIALIZABLE; + case 2: + return IsolationLevel.REPEATABLE_READ; + default: + return IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED; + } + }))); + + PartialResultSet partialResultSet = + PartialResultSet.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("hello").build()) + .build(); + value = Value.toValue(partialResultSet); + assertEquals(Type.proto("google.spanner.v1.PartialResultSet"), value.getType()); + assertEquals(partialResultSet, value.getProtoMessage(PartialResultSet.getDefaultInstance())); + + Interval interval = Interval.ofDays(10); + value = Value.toValue(interval); + assertEquals(Type.interval(), value.getType()); + assertEquals(interval, value.getInterval()); + + Struct struct = Struct.newBuilder().set("name").to(10L).build(); + value = Value.toValue(struct); + assertEquals(Type.struct(StructField.of("name", Type.int64())), value.getType()); + assertEquals(struct, value.getStruct()); + + Timestamp timestamp = Timestamp.now(); + value = Value.toValue(timestamp); + assertEquals(Type.timestamp(), value.getType()); + assertEquals(timestamp, value.getTimestamp()); + + List expectedBoolArray = Arrays.asList(true, false); + boolean[] bools1 = {true, false}; + value = Value.toValue(bools1); + assertEquals(Type.array(Type.bool()), value.getType()); + assertEquals(expectedBoolArray, value.getBoolArray()); + + Boolean[] bools2 = {true, false}; + value = Value.toValue(bools2); + assertEquals(Type.array(Type.bool()), value.getType()); + assertEquals(expectedBoolArray, value.getBoolArray()); + + List expectedFloatArray = Arrays.asList(0.1f, 0.2f, 0.3f); + Float[] floats1 = {0.1f, 0.2f, 0.3f}; + value = Value.toValue(floats1); + assertEquals(Type.array(Type.float32()), value.getType()); + assertEquals(expectedFloatArray, value.getFloat32Array()); + + float[] floats2 = {0.1f, 0.2f, 0.3f}; + value = Value.toValue(floats2); + assertEquals(Type.array(Type.float32()), value.getType()); + assertEquals(expectedFloatArray, value.getFloat32Array()); + + List expectedDoubleArray = Arrays.asList(0.1d, 0.2d, 0.3d, 0.4d); + Double[] doubles1 = {0.1d, 0.2d, 0.3d, 0.4d}; + value = Value.toValue(doubles1); + assertEquals(Type.array(Type.float64()), value.getType()); + assertEquals(expectedDoubleArray, value.getFloat64Array()); + + double[] doubles2 = {0.1d, 0.2d, 0.3d, 0.4d}; + value = Value.toValue(doubles2); + assertEquals(Type.array(Type.float64()), value.getType()); + assertEquals(expectedDoubleArray, value.getFloat64Array()); + + List expectedIntLongArray = Arrays.asList("1", "2", "3"); + int[] ints1 = {1, 2, 3}; + value = Value.toValue(ints1); + assertNull(value.getType()); + assertEquals(expectedIntLongArray, value.getAsStringList()); + + Integer[] ints2 = {1, 2, 3}; + value = Value.toValue(ints2); + assertNull(value.getType()); + assertEquals(expectedIntLongArray, value.getAsStringList()); + + Long[] longs1 = {1L, 2L, 3L}; + value = Value.toValue(longs1); + assertNull(value.getType()); + assertEquals(expectedIntLongArray, value.getAsStringList()); + + long[] longs2 = {1L, 2L, 3L}; + value = Value.toValue(longs2); + assertNull(value.getType()); + assertEquals(expectedIntLongArray, value.getAsStringList()); + + String string = "hello"; + value = Value.toValue(string); + assertNull(value.getType()); + assertEquals("hello", value.getAsString()); + } + + @Test + public void testToValueIterable() { + List booleans = Arrays.asList(true, false); + Value value = Value.toValue(booleans); + assertEquals(Type.array(Type.bool()), value.getType()); + assertEquals(booleans, value.getBoolArray()); + + List ints = Arrays.asList(1, 2, 3); + value = Value.toValue(ints); + assertNull(value.getType()); + assertEquals(Arrays.asList("1", "2", "3"), value.getAsStringList()); + + List longs = Arrays.asList(1L, 2L, 3L); + value = Value.toValue(longs); + assertNull(value.getType()); + assertEquals(Arrays.asList("1", "2", "3"), value.getAsStringList()); + + Set floats = new HashSet<>(Arrays.asList(0.1f, 0.2f, 0.3f)); + value = Value.toValue(floats); + assertEquals(Type.array(Type.float32()), value.getType()); + assertEquals(Arrays.asList(0.1f, 0.2f, 0.3f), value.getFloat32Array()); + + List doubles = Arrays.asList(0.1d, 0.2d, 0.3d, 0.4d); + value = Value.toValue(doubles); + assertEquals(Type.array(Type.float64()), value.getType()); + assertEquals(doubles, value.getFloat64Array()); + + List bigDecimals = + Arrays.asList(BigDecimal.valueOf(0.1d), BigDecimal.valueOf(0.2d)); + value = Value.toValue(bigDecimals); + assertEquals(Type.array(Type.numeric()), value.getType()); + assertEquals(bigDecimals, value.getNumericArray()); + + List byteArrays = + Arrays.asList(ByteArray.copyFrom("hello"), ByteArray.copyFrom("world")); + value = Value.toValue(byteArrays); + assertEquals(Type.array(Type.bytes()), value.getType()); + assertEquals(byteArrays, value.getBytesArray()); + + List bytes = Arrays.asList("hello".getBytes(), "world".getBytes()); + value = Value.toValue(bytes); + assertEquals(Type.array(Type.bytes()), value.getType()); + assertEquals(byteArrays, value.getBytesArray()); + + List intervals = Arrays.asList(Interval.ofDays(10), Interval.ofDays(20)); + value = Value.toValue(intervals); + assertEquals(Type.array(Type.interval()), value.getType()); + assertEquals(intervals, value.getIntervalArray()); + + List timestamps = Arrays.asList(Timestamp.now(), Timestamp.now()); + value = Value.toValue(timestamps); + assertEquals(Type.array(Type.timestamp()), value.getType()); + assertEquals(timestamps, value.getTimestampArray()); + + List dates = + Arrays.asList(Date.fromYearMonthDay(2024, 8, 23), Date.fromYearMonthDay(2024, 12, 27)); + value = Value.toValue(dates); + assertEquals(Type.array(Type.date()), value.getType()); + assertEquals(dates, value.getDateArray()); + + List javaDates = + Arrays.asList( + java.util.Date.from( + ZonedDateTime.of(2024, 8, 23, 11, 12, 13, 10, ZoneId.of("UTC")).toInstant()), + java.util.Date.from( + ZonedDateTime.of(2024, 12, 27, 11, 12, 13, 10, ZoneId.of("UTC")).toInstant())); + value = Value.toValue(javaDates); + assertEquals(Type.array(Type.date()), value.getType()); + assertEquals(dates, value.getDateArray()); + + List localDates = + Arrays.asList(LocalDate.of(2024, 8, 23), LocalDate.of(2024, 12, 27)); + value = Value.toValue(localDates); + assertEquals(Type.array(Type.date()), value.getType()); + assertEquals(dates, value.getDateArray()); + + List localDateTimes = + Arrays.asList( + LocalDateTime.of(2024, 8, 23, 1, 49, 52, 10), + LocalDateTime.of(2024, 12, 27, 1, 49, 52, 10)); + value = Value.toValue(localDateTimes); + assertNull(value.getType()); + assertEquals( + Arrays.asList("2024-08-23T01:49:52.000Z", "2024-12-27T01:49:52.000Z"), + value.getAsStringList()); + + List offsetDateTimes = + Arrays.asList( + LocalDateTime.of(2024, 8, 23, 1, 49, 52, 10).atOffset(ZoneOffset.ofHours(1)), + LocalDateTime.of(2024, 12, 27, 1, 49, 52, 10).atOffset(ZoneOffset.ofHours(1))); + value = Value.toValue(offsetDateTimes); + assertNull(value.getType()); + assertEquals( + Arrays.asList("2024-08-23T00:49:52.000Z", "2024-12-27T00:49:52.000Z"), + value.getAsStringList()); + + List zonedDateTimes = + Arrays.asList( + LocalDateTime.of(2024, 8, 23, 1, 49, 52, 10).atZone(ZoneId.of("UTC")), + LocalDateTime.of(2024, 12, 27, 1, 49, 52, 10).atZone(ZoneId.of("UTC"))); + value = Value.toValue(zonedDateTimes); + assertNull(value.getType()); + assertEquals( + Arrays.asList("2024-08-23T01:49:52.000Z", "2024-12-27T01:49:52.000Z"), + value.getAsStringList()); + } + private static class BrokenSerializationList extends ForwardingList implements Serializable { private static final long serialVersionUID = 1L; From 29a5c5aa31fdc566e1a1d3631cbc876a242db153 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 10:52:21 +0530 Subject: [PATCH 02/13] Create staementfactory on-demand --- .../google/cloud/spanner/DatabaseClient.java | 2 +- .../cloud/spanner/DatabaseClientImpl.java | 30 +++++++++++++++++++ .../MultiplexedSessionDatabaseClient.java | 9 ++++++ .../com/google/cloud/spanner/SessionPool.java | 5 ++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index 00b8d6295f5..6d608abd1d3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -617,6 +617,6 @@ ServerStream batchWriteAtLeastOnce( *

databaseClient.newStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10) */ default StatementFactory newStatementFactory() { - return new StatementFactory(getDialect()); + throw new UnsupportedOperationException("method should be overwritten"); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 775e38f05b9..2e18c5c5f09 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -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 { @@ -41,6 +46,8 @@ class DatabaseClientImpl implements DatabaseClient { @VisibleForTesting final boolean useMultiplexedSessionPartitionedOps; @VisibleForTesting final boolean useMultiplexedSessionForRW; + private StatementFactory statementFactory = null; + final boolean useMultiplexedSessionBlindWrite; @VisibleForTesting @@ -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() { @@ -346,6 +368,14 @@ public long executePartitionedUpdate(final Statement stmt, final UpdateOption... return executePartitionedUpdateWithPooledSession(stmt, options); } + private Future 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); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index 798b976c721..d5f31649637 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -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; @@ -652,6 +653,14 @@ public Dialect getDialect() { } } + public Future getDialectAsync() { + try { + return MAINTAINER_SERVICE.submit(dialectSupplier::get); + } catch (Exception exception) { + throw SpannerExceptionFactory.asSpannerException(exception); + } + } + @Override public Timestamp write(Iterable mutations) throws SpannerException { return createMultiplexedSessionTransaction(/* singleUse = */ false).write(mutations); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index c0a7fd9fa0e..37fa2c5d202 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -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; @@ -2548,6 +2549,10 @@ Dialect getDialect() { } } + Future getDialectAsync() { + return executor.submit(this::getDialect); + } + PooledSessionReplacementHandler getPooledSessionReplacementHandler() { return pooledSessionReplacementHandler; } From 3f42072bca8a178b0491ae9f92b0d707d43e4ba6 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 10:54:59 +0530 Subject: [PATCH 03/13] Remove unused variable --- .../src/main/java/com/google/cloud/spanner/Value.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 1388d3fd32e..6243477865d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -50,7 +50,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -105,8 +104,6 @@ public abstract class Value implements Serializable { static final com.google.protobuf.Value NULL_PROTO = com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); - private static final ZoneId UTC_ZONE = ZoneId.of("UTC"); - /** Constant to specify a PG Numeric NaN value. */ public static final String NAN = "NaN"; From 899400c6ae71c974ee2c139ce9571cb212ef9d5b Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 11:10:54 +0530 Subject: [PATCH 04/13] Update clirr for exposing newStatementFactory in the DatabaseClient interface --- google-cloud-spanner/clirr-ignored-differences.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index bcea799a409..68521a29aa7 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -945,5 +945,9 @@ boolean supportsExplain() - + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.Statement$StatementFactory newStatementFactory() + From d6777996d830dd6ff0b48e311768a4559713c69e Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 14:01:11 +0530 Subject: [PATCH 05/13] Address comments --- .../google/cloud/spanner/DatabaseClient.java | 9 ++--- .../cloud/spanner/DatabaseClientImpl.java | 31 ++++++++++------- .../MultiplexedSessionDatabaseClient.java | 2 +- .../cloud/spanner/SpannerTypeConverter.java | 18 +++------- .../com/google/cloud/spanner/Statement.java | 10 +++--- .../java/com/google/cloud/spanner/Value.java | 33 ++++++++++--------- .../cloud/spanner/DatabaseClientImplTest.java | 3 +- 7 files changed, 56 insertions(+), 50 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index 6d608abd1d3..b68447dcd5c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -609,14 +609,15 @@ ServerStream batchWriteAtLeastOnce( 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. + * Returns StatementFactory for the given dialect. + * + *

A {@link StatementFactory}, can be used to create statements with unnamed parameters. * *

Examples using {@link StatementFactory} * - *

databaseClient.newStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10) + *

databaseClient.getStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10) */ - default StatementFactory newStatementFactory() { + default StatementFactory getStatementFactory() { throw new UnsupportedOperationException("method should be overwritten"); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 2e18c5c5f09..d0cc698f49d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -146,19 +146,28 @@ public Dialect getDialect() { return pool.getDialect(); } + private final AbstractLazyInitializer statementFactorySupplier = + new AbstractLazyInitializer() { + @Override + protected StatementFactory initialize() { + try { + Dialect dialect = getDialectAsync().get(30, TimeUnit.SECONDS); + return new StatementFactory(dialect); + } catch (ExecutionException | TimeoutException e) { + throw SpannerExceptionFactory.asSpannerException(e); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }; + @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); - } + public StatementFactory getStatementFactory() { + try { + return statementFactorySupplier.get(); + } catch (Exception exception) { + throw SpannerExceptionFactory.asSpannerException(exception); } - return statementFactory; } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index d5f31649637..aa2dc857122 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -653,7 +653,7 @@ public Dialect getDialect() { } } - public Future getDialectAsync() { + Future getDialectAsync() { try { return MAINTAINER_SERVICE.submit(dialectSupplier::get); } catch (Exception exception) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java index bd36ef69aa0..0fd74048e02 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java @@ -18,7 +18,6 @@ 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; @@ -35,10 +34,7 @@ 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"); @@ -61,7 +57,7 @@ static String convertToISO8601(T dateTime) { return ISO_8601_DATE_FORMATTER.format(dateTime); } - static Value createUntypedValue(T value) { + static Value createUntypedStringValue(T value) { return Value.untyped( com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build()); } @@ -79,12 +75,8 @@ static Iterable 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)); + return Date.fromYearMonthDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth()); } static Value createUntypedIterableValue( @@ -104,15 +96,15 @@ static Value createUntypedIterableValue( .build()); } - static ZonedDateTime convertToUTCTimezone(LocalDateTime localDateTime) { + static ZonedDateTime atUTC(LocalDateTime localDateTime) { return localDateTime.atZone(UTC_ZONE); } - static ZonedDateTime convertToUTCTimezone(OffsetDateTime localDateTime) { + static ZonedDateTime atUTC(OffsetDateTime localDateTime) { return localDateTime.atZoneSameInstant(UTC_ZONE); } - static ZonedDateTime convertToUTCTimezone(ZonedDateTime localDateTime) { + static ZonedDateTime atUTC(ZonedDateTime localDateTime) { return localDateTime.withZoneSameInstant(UTC_ZONE); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index 8625ac0da92..772b7f36902 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -261,9 +261,9 @@ public Statement of(String 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 + * @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 *

This function accepts the SQL statement with unnamed parameters(?) and accepts the * list of objects to replace unnamed parameters. Primitive types are supported *

For Date column, following types are supported @@ -281,9 +281,9 @@ public Statement of(String sql) { *

  • ZonedDateTime * *

    - * @see DatabaseClient#newStatementFactory + * @see DatabaseClient#getStatementFactory */ - public Statement of(String sql, Object... values) { + public Statement withUnnamedParameters(String sql, Object... values) { Map parameters = getUnnamedParametersMap(values); AbstractStatementParser statementParser = AbstractStatementParser.getInstance(this.dialect); ParametersInfo parametersInfo = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 6243477865d..2fa27d171fa 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -16,13 +16,13 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.SpannerTypeConverter.atUTC; import static com.google.cloud.spanner.SpannerTypeConverter.convertLocalDateToSpannerDate; import static com.google.cloud.spanner.SpannerTypeConverter.convertToISO8601; import static com.google.cloud.spanner.SpannerTypeConverter.convertToTypedIterable; -import static com.google.cloud.spanner.SpannerTypeConverter.convertToUTCTimezone; import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedArrayValue; import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedIterableValue; -import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedValue; +import static com.google.cloud.spanner.SpannerTypeConverter.createUntypedStringValue; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -846,7 +846,7 @@ static Value toValue(Object value) { return Value.bool((Boolean) value); } if (value instanceof Long || value instanceof Integer) { - return createUntypedValue(String.valueOf(value)); + return createUntypedStringValue(String.valueOf(value)); } if (value instanceof Float) { return Value.float32((Float) value); @@ -867,19 +867,21 @@ static Value toValue(Object value) { return Value.date((Date) value); } if (value instanceof java.util.Date) { - return Value.date(SpannerTypeConverter.convertUtilDateToSpannerDate((java.util.Date) value)); + return Value.date(Date.fromJavaUtilDate((java.util.Date) value)); } if (value instanceof LocalDate) { return Value.date(convertLocalDateToSpannerDate((LocalDate) value)); } if (value instanceof LocalDateTime) { - return createUntypedValue(convertToISO8601(convertToUTCTimezone((LocalDateTime) value))); + return createUntypedStringValue( + convertToISO8601(SpannerTypeConverter.atUTC((LocalDateTime) value))); } if (value instanceof OffsetDateTime) { - return createUntypedValue(convertToISO8601(convertToUTCTimezone((OffsetDateTime) value))); + return createUntypedStringValue( + convertToISO8601(SpannerTypeConverter.atUTC((OffsetDateTime) value))); } if (value instanceof ZonedDateTime) { - return createUntypedValue(convertToISO8601(convertToUTCTimezone((ZonedDateTime) value))); + return createUntypedStringValue(convertToISO8601(atUTC((ZonedDateTime) value))); } if (value instanceof ProtocolMessageEnum) { return Value.protoEnum((ProtocolMessageEnum) value); @@ -939,10 +941,7 @@ static Value toValue(Object value) { } if (object instanceof java.util.Date) { return Value.dateArray( - convertToTypedIterable( - SpannerTypeConverter::convertUtilDateToSpannerDate, - (java.util.Date) object, - iterator)); + convertToTypedIterable(Date::fromJavaUtilDate, (java.util.Date) object, iterator)); } if (object instanceof LocalDate) { return Value.dateArray( @@ -951,15 +950,19 @@ static Value toValue(Object value) { } if (object instanceof LocalDateTime) { return createUntypedIterableValue( - (LocalDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + (LocalDateTime) object, + iterator, + val -> convertToISO8601(SpannerTypeConverter.atUTC(val))); } if (object instanceof OffsetDateTime) { return createUntypedIterableValue( - (OffsetDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + (OffsetDateTime) object, + iterator, + val -> convertToISO8601(SpannerTypeConverter.atUTC(val))); } if (object instanceof ZonedDateTime) { return createUntypedIterableValue( - (ZonedDateTime) object, iterator, val -> convertToISO8601(convertToUTCTimezone(val))); + (ZonedDateTime) object, iterator, val -> convertToISO8601(atUTC(val))); } } @@ -995,7 +998,7 @@ static Value toValue(Object value) { return createUntypedArrayValue(Arrays.stream((int[]) value).boxed()); } - return createUntypedValue(value); + return createUntypedStringValue(value); } /** Returns the type of this value. This will return a type even if {@code isNull()} is true. */ diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index b6fd51ac15c..b5055fa5e3f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -4901,7 +4901,8 @@ public void testStatementWithIUnnamedParametersParameter() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - Statement statement = client.newStatementFactory().of("select id from test where b=?", true); + Statement statement = + client.getStatementFactory().withUnnamedParameters("select id from test where b=?", true); Statement generatedStatement = Statement.newBuilder("select id from test where b=@p1").bind("p1").to(true).build(); mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); From cf7b3972fc188f29f454532a5f7bee28a9a49a67 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 14:04:44 +0530 Subject: [PATCH 06/13] Fix clirr issue with renaming --- google-cloud-spanner/clirr-ignored-differences.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 68521a29aa7..b6b19d1d577 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -948,6 +948,6 @@ 7012 com/google/cloud/spanner/DatabaseClient - com.google.cloud.spanner.Statement$StatementFactory newStatementFactory() + com.google.cloud.spanner.Statement$StatementFactory getStatementFactory() From fb618d5544b7f1bfc4826a76965bba2dd5525122 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 14:34:56 +0530 Subject: [PATCH 07/13] Addressed comments --- .../com/google/cloud/spanner/Statement.java | 40 +++++++++++-------- .../java/com/google/cloud/spanner/Value.java | 7 ---- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index 772b7f36902..3efddcfb584 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -261,26 +261,32 @@ public Statement of(String sql) { } /** + * This function accepts the SQL statement with unnamed parameters(?) and accepts the list of + * objects to replace unnamed parameters. Primitive types are supported + * + *

    For Date column, following types are supported + * + *

      + *
    • java.util.Date + *
    • LocalDate + *
    • com.google.cloud.Date + *
    + * + *

    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 + * + *

      + *
    • LocalDateTime + *
    • OffsetDateTime + *
    • ZonedDateTime + *
    + * + *

    + * * @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 - *

    This function accepts the SQL statement with unnamed parameters(?) and accepts the - * list of objects to replace unnamed parameters. Primitive types are supported - *

    For Date column, following types are supported - *

      - *
    • java.util.Date - *
    • LocalDate - *
    • com.google.cloud.Date - *
    - *

    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 - *

      - *
    • LocalDateTime - *
    • OffsetDateTime - *
    • ZonedDateTime - *
    - *

    * @see DatabaseClient#getStatementFactory */ public Statement withUnnamedParameters(String sql, Object... values) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 2fa27d171fa..510e53deed1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -866,9 +866,6 @@ static Value toValue(Object value) { if (value instanceof Date) { return Value.date((Date) value); } - if (value instanceof java.util.Date) { - return Value.date(Date.fromJavaUtilDate((java.util.Date) value)); - } if (value instanceof LocalDate) { return Value.date(convertLocalDateToSpannerDate((LocalDate) value)); } @@ -939,10 +936,6 @@ static Value toValue(Object value) { if (object instanceof Date) { return Value.dateArray(convertToTypedIterable((Date) object, iterator)); } - if (object instanceof java.util.Date) { - return Value.dateArray( - convertToTypedIterable(Date::fromJavaUtilDate, (java.util.Date) object, iterator)); - } if (object instanceof LocalDate) { return Value.dateArray( SpannerTypeConverter.convertToTypedIterable( From 07e5569776bae9327f7e352cb990a391fe2416d4 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 14:49:12 +0530 Subject: [PATCH 08/13] Addressed comments --- .../cloud/spanner/SpannerTypeConverter.java | 31 ++++++++++--------- .../com/google/cloud/spanner/ValueTest.java | 18 ----------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java index 0fd74048e02..25b125c2c75 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java @@ -28,6 +28,7 @@ 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; @@ -62,12 +63,10 @@ static Value createUntypedStringValue(T value) { com.google.protobuf.Value.newBuilder().setStringValue(String.valueOf(value)).build()); } - @SuppressWarnings("unchecked") static Iterable convertToTypedIterable( Function func, T val, Iterator iterator) { List values = new ArrayList<>(); - values.add(func.apply(val)); - iterator.forEachRemaining(value -> values.add(func.apply((T) value))); + SpannerTypeConverter.processIterable(val, iterator, func, values::add); return values; } @@ -75,25 +74,27 @@ static Iterable convertToTypedIterable(T val, Iterator iterator) { return convertToTypedIterable(v -> v, val, iterator); } + @SuppressWarnings("unchecked") + static void processIterable( + T val, Iterator iterator, Function func, Consumer 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 Value createUntypedIterableValue( T value, Iterator iterator, Function 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( - ListValue.newBuilder() - .addAllValues( - SpannerTypeConverter.convertToTypedIterable( - (val) -> - com.google.protobuf.Value.newBuilder() - .setStringValue(func.apply(val)) - .build(), - value, - iterator))) - .build()); + com.google.protobuf.Value.newBuilder().setListValue(listValueBuilder.build()).build()); } static ZonedDateTime atUTC(LocalDateTime localDateTime) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 8045a99296c..55e2659e2eb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -49,7 +49,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; -import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -2580,13 +2579,6 @@ public void testToValue() { assertEquals(Type.date(), value.getType()); assertEquals(date, value.getDate()); - java.util.Date utilDate = - java.util.Date.from( - Instant.from(LocalDateTime.of(2018, 2, 26, 11, 30).toInstant(ZoneOffset.UTC))); - value = Value.toValue(utilDate); - assertEquals(Type.date(), value.getType()); - assertEquals(date, value.getDate()); - LocalDate localDate = LocalDate.of(2018, 2, 26); value = Value.toValue(localDate); assertEquals(Type.date(), value.getType()); @@ -2768,16 +2760,6 @@ public void testToValueIterable() { assertEquals(Type.array(Type.date()), value.getType()); assertEquals(dates, value.getDateArray()); - List javaDates = - Arrays.asList( - java.util.Date.from( - ZonedDateTime.of(2024, 8, 23, 11, 12, 13, 10, ZoneId.of("UTC")).toInstant()), - java.util.Date.from( - ZonedDateTime.of(2024, 12, 27, 11, 12, 13, 10, ZoneId.of("UTC")).toInstant())); - value = Value.toValue(javaDates); - assertEquals(Type.array(Type.date()), value.getType()); - assertEquals(dates, value.getDateArray()); - List localDates = Arrays.asList(LocalDate.of(2024, 8, 23), LocalDate.of(2024, 12, 27)); value = Value.toValue(localDates); From c8390b14040b72e9458bf39458015c06623001dd Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 15:48:03 +0530 Subject: [PATCH 09/13] Addressed comments --- .../src/main/java/com/google/cloud/spanner/Value.java | 7 +++++-- .../test/java/com/google/cloud/spanner/ValueTest.java | 11 +++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 510e53deed1..4dee46fc191 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -50,6 +50,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -871,7 +872,8 @@ static Value toValue(Object value) { } if (value instanceof LocalDateTime) { return createUntypedStringValue( - convertToISO8601(SpannerTypeConverter.atUTC((LocalDateTime) value))); + convertToISO8601( + SpannerTypeConverter.atUTC(((LocalDateTime) value).atZone(ZoneId.systemDefault())))); } if (value instanceof OffsetDateTime) { return createUntypedStringValue( @@ -945,7 +947,8 @@ static Value toValue(Object value) { return createUntypedIterableValue( (LocalDateTime) object, iterator, - val -> convertToISO8601(SpannerTypeConverter.atUTC(val))); + val -> + convertToISO8601(SpannerTypeConverter.atUTC(val.atZone(ZoneId.systemDefault())))); } if (object instanceof OffsetDateTime) { return createUntypedIterableValue( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 55e2659e2eb..a03206c972c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -63,6 +63,7 @@ import java.util.List; import java.util.Random; import java.util.Set; +import java.util.TimeZone; import java.util.function.Supplier; import java.util.stream.Collectors; import org.junit.Test; @@ -2584,10 +2585,13 @@ public void testToValue() { assertEquals(Type.date(), value.getType()); assertEquals(date, value.getDate()); + TimeZone defaultTimezone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); LocalDateTime localDateTime = LocalDateTime.of(2018, 2, 26, 11, 30, 10); value = Value.toValue(localDateTime); assertNull(value.getType()); - assertEquals("2018-02-26T11:30:10.000Z", value.getAsString()); + assertEquals("2018-02-26T10:30:10.000Z", value.getAsString()); + TimeZone.setDefault(defaultTimezone); OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(10)); value = Value.toValue(offsetDateTime); @@ -2766,6 +2770,8 @@ public void testToValueIterable() { assertEquals(Type.array(Type.date()), value.getType()); assertEquals(dates, value.getDateArray()); + TimeZone defaultTimezone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Kolkata")); List localDateTimes = Arrays.asList( LocalDateTime.of(2024, 8, 23, 1, 49, 52, 10), @@ -2773,8 +2779,9 @@ public void testToValueIterable() { value = Value.toValue(localDateTimes); assertNull(value.getType()); assertEquals( - Arrays.asList("2024-08-23T01:49:52.000Z", "2024-12-27T01:49:52.000Z"), + Arrays.asList("2024-08-22T20:19:52.000Z", "2024-12-26T20:19:52.000Z"), value.getAsStringList()); + TimeZone.setDefault(defaultTimezone); List offsetDateTimes = Arrays.asList( From 668be971a9dd0ca03b441393989414cddbaa216c Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Mon, 14 Apr 2025 16:23:13 +0530 Subject: [PATCH 10/13] Added tests --- .../cloud/spanner/DatabaseClientImplTest.java | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index b5055fa5e3f..70209917f0b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -4897,7 +4897,7 @@ public void testMetadataUnknownTypes() { } @Test - public void testStatementWithIUnnamedParametersParameter() { + public void testStatementWithUnnamedParameters() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); @@ -4914,6 +4914,126 @@ public void testStatementWithIUnnamedParametersParameter() { } } + @Test + public void testStatementWithUnnamedParametersAndSingleLineComment() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = + client + .getStatementFactory() + .withUnnamedParameters( + "-- comment about ? in the statement\nselect id from test where b=?", true); + Statement generatedStatement = + Statement.newBuilder("-- comment about ? in the statement\nselect id from test where b=@p1") + .bind("p1") + .to(true) + .build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + + @Test + public void testStatementWithUnnamedParametersAndSingleLineCommentWithHash() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = + client + .getStatementFactory() + .withUnnamedParameters( + "# comment about ? in the statement\nselect id from test where b=?", true); + Statement generatedStatement = + Statement.newBuilder("# comment about ? in the statement\nselect id from test where b=@p1") + .bind("p1") + .to(true) + .build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + + @Test + public void testStatementWithUnnamedParametersAndMultiLineComment() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = + client + .getStatementFactory() + .withUnnamedParameters( + "# comment about ? in the statement\nselect id from test\n /* This is a ? comment \n about ? */ \n where b=? # this is a inline command about ?", + true); + Statement generatedStatement = + Statement.newBuilder( + "# comment about ? in the statement\nselect id from test\n /* This is a ? comment \n about ? */ \n where b=@p1 # this is a inline command about ?") + .bind("p1") + .to(true) + .build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + + @Test + public void testStatementWithUnnamedParametersAndStringLiteralWithQuestionMark() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = + client + .getStatementFactory() + .withUnnamedParameters("select id from test where name = \"abc?\" AND b=?", true); + Statement generatedStatement = + Statement.newBuilder("select id from test where name = \"abc?\" AND b=@p1") + .bind("p1") + .to(true) + .build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + + @Test + public void testStatementWithUnnamedParametersAndHint() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + Statement statement = + client + .getStatementFactory() + .withUnnamedParameters("@{FORCE_INDEX=ABCDEF} select id from test where b=?", true); + Statement generatedStatement = + Statement.newBuilder("@{FORCE_INDEX=ABCDEF} select id from test where b=@p1") + .bind("p1") + .to(true) + .build(); + mockSpanner.putStatementResult(StatementResult.query(generatedStatement, SELECT1_RESULTSET)); + + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + assertEquals(1L, resultSet.getLong(0)); + assertFalse(resultSet.next()); + } + } + @Test public void testStatementWithBytesArrayParameter() { Statement statement = From 05713f3664fb26ae8f8cc392451b417bcc12a921 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 15 Apr 2025 13:08:41 +0530 Subject: [PATCH 11/13] Addressed comments --- .../cloud/spanner/DatabaseClientImpl.java | 2 -- .../cloud/spanner/SpannerTypeConverter.java | 2 +- .../com/google/cloud/spanner/Statement.java | 30 ++++++++++++++++++- .../java/com/google/cloud/spanner/Value.java | 17 +++-------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index d0cc698f49d..8e0e07c457b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -46,8 +46,6 @@ class DatabaseClientImpl implements DatabaseClient { @VisibleForTesting final boolean useMultiplexedSessionPartitionedOps; @VisibleForTesting final boolean useMultiplexedSessionForRW; - private StatementFactory statementFactory = null; - final boolean useMultiplexedSessionBlindWrite; @VisibleForTesting diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java index 25b125c2c75..02c0cc213d6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerTypeConverter.java @@ -98,7 +98,7 @@ static Value createUntypedIterableValue( } static ZonedDateTime atUTC(LocalDateTime localDateTime) { - return localDateTime.atZone(UTC_ZONE); + return atUTC(localDateTime.atZone(ZoneId.systemDefault())); } static ZonedDateTime atUTC(OffsetDateTime localDateTime) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index 3efddcfb584..f7cae62c966 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -249,6 +249,34 @@ StringBuilder toString(StringBuilder b) { return b; } + /** + * Factory for creating {@link Statement}. + * + *

    This factory class supports creating {@link Statement} with positional(or unnamed) + * parameters. + * + *

    + * + *

    Usage Example

    + * + * Simple SQL query + * + *
    {@code
    +   * Statement statement = databaseClient.getStatementFactory()
    +   *     .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = ?", 10L)
    +   * }
    + * + * How to use SQL queries with IN command + * + *
    {@code
    +   * long[] ids = {10L, 12L, 1483L};
    +   * Statement statement = databaseClient.getStatementFactory()
    +   *     .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = UNNEST(?)", ids)
    +   * }
    + * + * @see DatabaseClient#getStatementFactory() + * @see StatementFactory#withUnnamedParameters(String, Object...) + */ public static final class StatementFactory { private final Dialect dialect; @@ -262,7 +290,7 @@ public Statement of(String sql) { /** * This function accepts the SQL statement with unnamed parameters(?) and accepts the list of - * objects to replace unnamed parameters. Primitive types are supported + * objects to replace unnamed parameters. Primitive types are supported. * *

    For Date column, following types are supported * diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 4dee46fc191..0590d0cf53c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -50,7 +50,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -871,13 +870,10 @@ static Value toValue(Object value) { return Value.date(convertLocalDateToSpannerDate((LocalDate) value)); } if (value instanceof LocalDateTime) { - return createUntypedStringValue( - convertToISO8601( - SpannerTypeConverter.atUTC(((LocalDateTime) value).atZone(ZoneId.systemDefault())))); + return createUntypedStringValue(convertToISO8601(atUTC((LocalDateTime) value))); } if (value instanceof OffsetDateTime) { - return createUntypedStringValue( - convertToISO8601(SpannerTypeConverter.atUTC((OffsetDateTime) value))); + return createUntypedStringValue(convertToISO8601(atUTC((OffsetDateTime) value))); } if (value instanceof ZonedDateTime) { return createUntypedStringValue(convertToISO8601(atUTC((ZonedDateTime) value))); @@ -945,16 +941,11 @@ static Value toValue(Object value) { } if (object instanceof LocalDateTime) { return createUntypedIterableValue( - (LocalDateTime) object, - iterator, - val -> - convertToISO8601(SpannerTypeConverter.atUTC(val.atZone(ZoneId.systemDefault())))); + (LocalDateTime) object, iterator, val -> convertToISO8601(atUTC(val))); } if (object instanceof OffsetDateTime) { return createUntypedIterableValue( - (OffsetDateTime) object, - iterator, - val -> convertToISO8601(SpannerTypeConverter.atUTC(val))); + (OffsetDateTime) object, iterator, val -> convertToISO8601(atUTC(val))); } if (object instanceof ZonedDateTime) { return createUntypedIterableValue( From 0a7594cbe4d9b813169698f64c33819b15eec82d Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 15 Apr 2025 15:14:26 +0530 Subject: [PATCH 12/13] Addressed comments --- .../google/cloud/spanner/DatabaseClient.java | 13 ++++-- .../com/google/cloud/spanner/Statement.java | 46 ++++++++++++------- .../java/com/google/cloud/spanner/Value.java | 3 ++ .../com/google/cloud/spanner/ValueTest.java | 6 ++- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index b68447dcd5c..e30c648c367 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -609,13 +609,20 @@ ServerStream batchWriteAtLeastOnce( long executePartitionedUpdate(Statement stmt, UpdateOption... options); /** - * Returns StatementFactory for the given dialect. + * Returns a {@link StatementFactory} for the given dialect. * - *

    A {@link StatementFactory}, can be used to create statements with unnamed parameters. + *

    A {@link StatementFactory} can be used to create statements with unnamed parameters. This is + * primarily intended for framework developers who want to integrate the Spanner client with + * frameworks that use unnamed parameters. Developers who just want to use the Spanner client in + * their application, should use named parameters. * *

    Examples using {@link StatementFactory} * - *

    databaseClient.getStatementFactory().of("SELECT NAME FROM TABLE WHERE ID = ?", 10) + *

    {@code
    +   * Statement statement = databaseClient
    +   *     .getStatementFactory()
    +   *     .withUnnamedParameters("SELECT NAME FROM TABLE WHERE ID = ?", 10);
    +   * }
    */ default StatementFactory getStatementFactory() { throw new UnsupportedOperationException("method should be overwritten"); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index f7cae62c966..ce8335c1953 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -250,10 +250,11 @@ StringBuilder toString(StringBuilder b) { } /** - * Factory for creating {@link Statement}. + * Factory for creating {@link Statement}s with unnamed parameters. * - *

    This factory class supports creating {@link Statement} with positional(or unnamed) - * parameters. + *

    This class is primarily intended for framework developers who want to integrate the Spanner + * client with a framework that uses unnamed parameters. Developers who want to use the Spanner + * client in their application, should use named parameters. * *

    * @@ -266,7 +267,18 @@ StringBuilder toString(StringBuilder b) { * .withUnnamedParameters("SELECT * FROM TABLE WHERE ID = ?", 10L) * } * - * How to use SQL queries with IN command + * SQL query with multiple parameters + * + *

    {@code
    +   * long id = 10L;
    +   * String name = "google";
    +   * List phoneNumbers = Arrays.asList("1234567890", "0987654321");
    +   * Statement statement = databaseClient.getStatementFactory()
    +   *    *     .withUnnamedParameters("INSERT INTO TABLE (ID, name, phonenumbers)
    +   *       // VALUES(?, ?, ?)", id, name, phoneNumbers)
    +   * }
    + * + * How to use arrays with the IN operator * *
    {@code
        * long[] ids = {10L, 12L, 1483L};
    @@ -289,25 +301,27 @@ public Statement of(String sql) {
         }
     
         /**
    -     * This function accepts the SQL statement with unnamed parameters(?) and accepts the list of
    -     * objects to replace unnamed parameters. Primitive types are supported.
    +     * This function accepts a SQL statement with unnamed parameters (?) and accepts a list of
    +     * objects that should be used as the values for those parameters. Primitive types are
    +     * supported.
          *
    -     * 

    For Date column, following types are supported + *

    For parameters of type DATE, the following types are supported * *

      - *
    • java.util.Date - *
    • LocalDate - *
    • com.google.cloud.Date + *
    • {@link java.time.LocalDate} + *
    • {@link com.google.cloud.Date} *
    * - *

    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 + *

    For parameters of type TIMESTAMP, the following types are supported. Note that Spanner + * stores all timestamps in UTC. Instances of ZonedDateTime and OffsetDateTime that use other + * timezones than UTC, will be converted to the corresponding UTC values before being sent to + * Spanner. Instances of LocalDateTime will be converted to a ZonedDateTime using the system + * default timezone, and then converted to UTC before being sent to Spanner. * *

      - *
    • LocalDateTime - *
    • OffsetDateTime - *
    • ZonedDateTime + *
    • {@link java.time.LocalDateTime} + *
    • {@link java.time.OffsetDateTime} + *
    • {@link java.time.ZonedDateTime} *
    * *

    diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 0590d0cf53c..5befba04e57 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -839,6 +839,9 @@ public static Value structArray(Type elementType, @Nullable Iterable v) private Value() {} static Value toValue(Object value) { + if (value == null) { + return Value.untyped(NULL_PROTO); + } if (value instanceof Value) { return (Value) value; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index a03206c972c..f88d7683967 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -2510,8 +2510,12 @@ public void verifyBrokenSerialization() { @Test public void testToValue() { + Value value = Value.toValue(null); + assertNull(value.getType()); + assertEquals("NULL", value.getAsString()); + int i = 10; - Value value = Value.toValue(i); + value = Value.toValue(i); assertNull(value.getType()); assertEquals("10", value.getAsString()); From 1cd0c6ef1da24a778c15f5aa0fb38d04d138271b Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 15 Apr 2025 22:13:39 +0530 Subject: [PATCH 13/13] Addressed comments --- .../src/main/java/com/google/cloud/spanner/Statement.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index ce8335c1953..a454fbb689b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -274,8 +274,7 @@ StringBuilder toString(StringBuilder b) { * String name = "google"; * List phoneNumbers = Arrays.asList("1234567890", "0987654321"); * Statement statement = databaseClient.getStatementFactory() - * * .withUnnamedParameters("INSERT INTO TABLE (ID, name, phonenumbers) - * // VALUES(?, ?, ?)", id, name, phoneNumbers) + * .withUnnamedParameters("INSERT INTO TABLE (ID, name, phonenumbers) VALUES(?, ?, ?)", id, name, phoneNumbers) * }

    * * How to use arrays with the IN operator