From 1e5f5937aebc758c567d9539fbf71b80638a28d9 Mon Sep 17 00:00:00 2001
From: Sagar Agarwal <sagarwaal@google.com>
Date: Thu, 24 Oct 2024 20:57:21 +0530
Subject: [PATCH] Added integration test for typed query parameter and fixed
 Interval parsing regex

---
 .../com/google/cloud/spanner/Interval.java    |   4 +-
 .../connection/DirectExecuteResultSet.java    |   8 +-
 .../ReplaceableForwardingResultSet.java       |  10 +-
 .../com/google/cloud/spanner/ValueTest.java   |   2 +-
 .../cloud/spanner/it/ITIntervalTest.java      | 104 ++++++++++++------
 5 files changed, 89 insertions(+), 39 deletions(-)

diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java
index 68b81490dc2..c53524b3eb0 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Interval.java
@@ -51,7 +51,7 @@ public abstract class Interval implements Serializable {
 
   private static final Pattern INTERVAL_PATTERN =
       Pattern.compile(
-          "^P(?!$)(-?\\d+Y)?(-?\\d+M)?(-?\\d+D)?(T(?=-?\\d)(-?\\d+H)?(-?\\d+M)?(-?\\d+(\\.\\d{1,9})?S)?)?$");
+          "^P(?!$)(-?\\d+Y)?(-?\\d+M)?(-?\\d+D)?(T(?=-?.?\\d)(-?\\d+H)?(-?\\d+M)?(-?((\\d+(\\.\\d{1,9})?)|(\\.\\d{1,9}))S)?)?$");
 
   /** Returns the months component of the interval. */
   public abstract int months();
@@ -154,8 +154,6 @@ public static Interval fromMonthsDaysNanos(int months, int days, BigInteger nano
         (nanos.subtract(BigInteger.valueOf(micros).multiply(BigInteger.valueOf(NANOS_PER_MICRO))))
             .shortValue();
 
-    System.out.println("Micros: " + micros + " Nanos: " + nanoFractions);
-
     return builder()
         .setMonths(months)
         .setDays(days)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
index bd8271bb99b..5339c4b69f6 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java
@@ -19,7 +19,13 @@
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
-import com.google.cloud.spanner.*;
+import com.google.cloud.spanner.Interval;
+import com.google.cloud.spanner.ProtobufResultSet;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Value;
 import com.google.common.base.Preconditions;
 import com.google.protobuf.AbstractMessage;
 import com.google.protobuf.ProtocolMessageEnum;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
index 59229037e15..a6198b7d633 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java
@@ -19,7 +19,15 @@
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
-import com.google.cloud.spanner.*;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Interval;
+import com.google.cloud.spanner.ProtobufResultSet;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Value;
 import com.google.common.base.Preconditions;
 import com.google.protobuf.AbstractMessage;
 import com.google.protobuf.ProtocolMessageEnum;
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 6b2544c1915..cef76b29980 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
@@ -1855,7 +1855,7 @@ public void testValueToProto() {
                     .addAllValues(
                         Arrays.asList(
                             com.google.protobuf.Value.newBuilder()
-                                .setStringValue("P1Y2M3DT5H6M2.456787800S")
+                                .setStringValue("P1Y2M3DT5H6M2.4567878S")
                                 .build(),
                             com.google.protobuf.Value.newBuilder()
                                 .setNullValue(NullValue.NULL_VALUE)
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java
index 710e9ed483a..d74581b7afc 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITIntervalTest.java
@@ -17,9 +17,7 @@
 package com.google.cloud.spanner.it;
 
 import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
-import static com.google.common.base.Strings.isNullOrEmpty;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
@@ -30,6 +28,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 import org.junit.AfterClass;
@@ -63,32 +62,30 @@ public static List<DialectTestParameter> data() {
       new String[] {
         "CREATE TABLE IntervalTable (\n"
             + "  key STRING(MAX),\n"
-            + "  slo_days INT64,\n"
-            + "  update_time TIMESTAMP,\n"
-            + "  expiry_days INT64 AS (EXTRACT(DAY FROM make_interval(day => GREATEST(LEAST(slo_days, 365), 1)))),\n"
-            + "  interval_array_len bigint AS (ARRAY_LENGTH(ARRAY<INTERVAL>[INTERVAL '1-2 3 4:5:6' YEAR TO SECOND]))\n"
-            + ") PRIMARY KEY (key);"
+            + "  create_time TIMESTAMP,\n"
+            + "  expiry_time TIMESTAMP,\n"
+            + "  expiry_within_month bool AS (expiry_time - create_time < INTERVAL 30 DAY),\n"
+            + "  interval_array_len INT64 AS (ARRAY_LENGTH(ARRAY<INTERVAL>[INTERVAL '1-2 3 4:5:6' YEAR TO SECOND]))\n"
+            + ") PRIMARY KEY (key)"
       };
 
   private static final String[] POSTGRESQL_SCHEMA =
       new String[] {
         "CREATE TABLE IntervalTable (\n"
             + "  key text primary key,\n"
-            + "  slo_days bigint,\n"
-            + "  update_time timestamptz,\n"
-            + "  expiry_days bigint GENERATED ALWAYS AS (EXTRACT(DAY FROM make_interval(days =>GREATEST(LEAST(slo_days, 365), 1)))) STORED,\n"
+            + "  create_time timestamptz,\n"
+            + "  expiry_time timestamptz,\n"
+            + "  expiry_within_month bool GENERATED ALWAYS AS (INTERVAL '1' DAY < INTERVAL '30' DAY) STORED,\n"
             + "  interval_array_len bigint GENERATED ALWAYS AS (ARRAY_LENGTH(ARRAY[INTERVAL '1-2 3 4:5:6'], 1)) STORED\n"
-            + ");"
+            + ")"
       };
 
   private static DatabaseClient client;
 
   private static boolean isUsingCloudDevel() {
-    String jobType = System.getenv("JOB_TYPE");
-
-    // Assumes that the jobType contains the string "cloud-devel" to signal that
-    // the environment is cloud-devel.
-    return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel");
+    return Objects.equals(
+        System.getProperty("spanner.gce.config.server_url"),
+        "https://staging-wrenchworks.sandbox.googleapis.com");
   }
 
   @BeforeClass
@@ -131,7 +128,9 @@ private Timestamp write(Mutation m) {
   }
 
   private Mutation.WriteBuilder baseInsert() {
-    return Mutation.newInsertOrUpdateBuilder("T").set("Key").to(lastKey = uniqueString());
+    return Mutation.newInsertOrUpdateBuilder("IntervalTable")
+        .set("Key")
+        .to(lastKey = uniqueString());
   }
 
   private Struct readRow(String table, String key, String... columns) {
@@ -141,23 +140,30 @@ private Struct readRow(String table, String key, String... columns) {
   }
 
   private Struct readLastRow(String... columns) {
-    return readRow("T", lastKey, columns);
+    return readRow("IntervalTable", lastKey, columns);
   }
 
   @Test
   public void writeToTableWithIntervalExpressions() {
     write(
         baseInsert()
-            .set("slo_days")
-            .to(5)
-            .set("update_time")
-            .to(Timestamp.ofTimeMicroseconds(12345678L))
+            .set("create_time")
+            .to(Timestamp.parseTimestamp("2004-11-30T04:53:54Z"))
+            .set("expiry_time")
+            .to(Timestamp.parseTimestamp("2004-12-15T04:53:54Z"))
             .build());
-    Struct row = readLastRow("expiryDays", "interval_array_len");
-    assertFalse(row.isNull(0));
-    assertEquals(5, row.getLong(0));
-    assertFalse(row.isNull(1));
-    assertEquals(1, row.getLong(1));
+    try (ResultSet resultSet =
+        client
+            .singleUse()
+            .executeQuery(
+                Statement.of(
+                    "SELECT expiry_within_month, interval_array_len FROM IntervalTable WHERE key='"
+                        + lastKey
+                        + "'"))) {
+      assertTrue(resultSet.next());
+      assertTrue(resultSet.getBoolean(0));
+      assertEquals(1, resultSet.getLong(1));
+    }
   }
 
   @Test
@@ -167,7 +173,39 @@ public void queryInterval() {
             .singleUse()
             .executeQuery(Statement.of("SELECT INTERVAL '1' DAY + INTERVAL '1' MONTH AS Col1"))) {
       assertTrue(resultSet.next());
-      assertTrue(resultSet.getInterval(0).equals(Interval.fromMonthsDaysMicros(1, 1, 0)));
+      assertEquals(resultSet.getInterval(0), Interval.fromMonthsDaysMicros(1, 1, 0));
+    }
+  }
+
+  @Test
+  public void queryWithIntervalParam() {
+    write(
+        baseInsert()
+            .set("create_time")
+            .to(Timestamp.parseTimestamp("2004-08-30T04:53:54Z"))
+            .set("expiry_time")
+            .to(Timestamp.parseTimestamp("2004-12-15T04:53:54Z"))
+            .build());
+
+    String query;
+    if (dialect.dialect == Dialect.POSTGRESQL) {
+      query =
+          "SELECT COUNT(*) FROM IntervalTable WHERE create_time < TIMESTAMPTZ '2004-11-30T10:23:54+0530' - $1";
+    } else {
+      query =
+          "SELECT COUNT(*) FROM IntervalTable WHERE create_time < TIMESTAMP('2004-11-30T10:23:54+0530') - @p1";
+    }
+
+    try (ResultSet resultSet =
+        client
+            .singleUse()
+            .executeQuery(
+                Statement.newBuilder(query)
+                    .bind("p1")
+                    .to(Value.interval(Interval.ofDays(30)))
+                    .build())) {
+      assertTrue(resultSet.next());
+      assertEquals(resultSet.getLong(0), 1L);
     }
   }
 
@@ -203,12 +241,12 @@ public void queryIntervalArray() {
         "SELECT ARRAY[CAST('P1Y2M3DT4H5M6.789123S' AS INTERVAL), null, CAST('P-1Y-2M-3DT-4H-5M-6.789123S' AS INTERVAL)] AS Col1";
     try (ResultSet resultSet = client.singleUse().executeQuery(Statement.of(query))) {
       assertTrue(resultSet.next());
-      assertTrue(
+      assertEquals(
           Arrays.asList(
-                  Interval.parseFromString("P1Y2M3DT4H5M6.789123S"),
-                  null,
-                  Interval.parseFromString("P-1Y-2M-3DT-4H-5M-6.789123S"))
-              .equals(resultSet.getIntervalList(0)));
+              Interval.parseFromString("P1Y2M3DT4H5M6.789123S"),
+              null,
+              Interval.parseFromString("P-1Y-2M-3DT-4H-5M-6.789123S")),
+          resultSet.getIntervalList(0));
     }
   }
 }