From 2b41dff3836c1ffec604ac333f4c7b9dd519e377 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 19:47:10 +0000
Subject: [PATCH 01/13] dataconnect: DataConnectExecutableVersions.json updated
 with version 1.7.0

---
 .../plugin/DataConnectExecutableVersions.json | 20 ++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
index fc1c279feca..886c8eeacb1 100644
--- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
+++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
@@ -1,5 +1,5 @@
 {
-  "defaultVersion": "1.6.1",
+  "defaultVersion": "1.7.0",
   "versions": [
     {
       "version": "1.3.4",
@@ -270,6 +270,24 @@
       "os": "linux",
       "size": 25223320,
       "sha512DigestHex": "5e8002a048b55a358d6d4a8c59c30ef8c3615461fd400bf4c8e86e84cc1b6482da604cb2635ec1639276c3b9c9f22a9c570f6729934e4fc77b09c8ccb6f3b986"
+    },
+    {
+      "version": "1.7.0",
+      "os": "windows",
+      "size": 25783808,
+      "sha512DigestHex": "9fc0bf918ea2c20bc8dcf26efd101e7a567a13bf7b0967c16f35989e3557d0055edce6522b23fb70361f26f3ad1abd93458af5b33d3fa3019333ca72680353a2"
+    },
+    {
+      "version": "1.7.0",
+      "os": "macos",
+      "size": 25350912,
+      "sha512DigestHex": "f887290a6083c3c88ee92f532c6fceb993a64714d5703ea7c389381222eab05998e8a25e0bee0770686433d5e7915fe6bfd58b3a0098d13ad0800c4e515fee0d"
+    },
+    {
+      "version": "1.7.0",
+      "os": "linux",
+      "size": 25272472,
+      "sha512DigestHex": "795c3f63a3c78b94204ae8c525227f3295a02cd90e553f52bde543029a91f68da0d17653cc6b4c863ed778104fd2baa97a729f80ab4bd54dd5dd4f5e15354b7a"
     }
   ]
 }
\ No newline at end of file

From 799f33d731bdde04393c84100d197fd8b42dc133 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 19:53:36 +0000
Subject: [PATCH 02/13] ListVariablesAndDataIntegrationTest.kt fixed

---
 .../demo/ListVariablesAndDataIntegrationTest.kt      | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt
index 0125ae2cb15..98adca0a0e9 100644
--- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt
+++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/ListVariablesAndDataIntegrationTest.kt
@@ -17,12 +17,12 @@
 package com.google.firebase.dataconnect.connectors.demo
 
 import com.google.firebase.Timestamp
+import com.google.firebase.dataconnect.LocalDate
 import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase
 import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb
 import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
-import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
-import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate
+import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate
 import com.google.firebase.dataconnect.testutil.withMicrosecondPrecision
 import io.kotest.common.ExperimentalKotest
 import io.kotest.matchers.shouldBe
@@ -37,7 +37,6 @@ import io.kotest.property.arbitrary.long
 import io.kotest.property.arbitrary.map
 import io.kotest.property.arbitrary.uuid
 import io.kotest.property.checkAll
-import java.util.Date
 import java.util.UUID
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.test.runTest
@@ -475,7 +474,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() {
     val booleans: List<Boolean>,
     val uuids: List<UUID>,
     val int64s: List<Long>,
-    val dates: List<Date>,
+    val dates: List<LocalDate>,
     val timestamps: List<Timestamp>,
   ) {
 
@@ -524,7 +523,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() {
             booleans = EdgeCases.booleans,
             uuids = EdgeCases.uuids,
             int64s = EdgeCases.int64s,
-            dates = EdgeCases.dates.all().map { it.toJavaUtilDate() },
+            dates = EdgeCases.dates.all().map { it.date },
             timestamps = EdgeCases.javaTime.instants.all.map { it.timestamp },
           )
     }
@@ -546,8 +545,7 @@ class ListVariablesAndDataIntegrationTest : DemoConnectorIntegrationTestBase() {
       booleans: Arb<List<Boolean>> = Arb.list(Arb.boolean(), 1..100),
       uuids: Arb<List<UUID>> = Arb.list(Arb.uuid(), 1..100),
       int64s: Arb<List<Long>> = Arb.list(Arb.long(), 1..100),
-      dates: Arb<List<Date>> =
-        Arb.list(Arb.dataConnect.dateTestData().map { it.toJavaUtilDate() }, 1..100),
+      dates: Arb<List<LocalDate>> = Arb.list(Arb.dataConnect.localDate(), 1..100),
       timestamps: Arb<List<Timestamp>> =
         Arb.list(Arb.dataConnect.javaTime.instantTestCase().map { it.timestamp }, 1..100),
     ): Arb<Lists> = arbitrary {

From 941c39804dce790a078c14725222a389c5fb0734 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 19:54:23 +0000
Subject: [PATCH 03/13] KeyVariablesIntegrationTest.kt fixed

---
 .../dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt
index 0ec62809cf0..231be08834f 100644
--- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt
+++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/KeyVariablesIntegrationTest.kt
@@ -19,7 +19,6 @@ package com.google.firebase.dataconnect.connectors.demo
 import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
-import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate
 import com.google.firebase.dataconnect.testutil.randomTimestamp
 import com.google.firebase.dataconnect.testutil.withMicrosecondPrecision
 import io.kotest.matchers.shouldBe
@@ -86,7 +85,7 @@ class KeyVariablesIntegrationTest : DemoConnectorIntegrationTestBase() {
 
   @Test
   fun primaryKeyIsDate() = runTest {
-    val id = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate()
+    val id = Arb.dataConnect.dateTestData().next(rs).date
     val value = Arb.dataConnect.string().next(rs)
 
     val key = connector.insertPrimaryKeyIsDate.execute(foo = id, value = value).data.key

From 76528b240bbf70832716fcd1b5d12ec8b7d7b554 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 20:15:49 +0000
Subject: [PATCH 04/13] DateScalarIntegrationTest.kt fixed

---
 .../demo/DateScalarIntegrationTest.kt         | 84 +++++++++----------
 1 file changed, 40 insertions(+), 44 deletions(-)

diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
index dca905d755d..67eac700af0 100644
--- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
+++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
@@ -20,15 +20,14 @@ package com.google.firebase.dataconnect.connectors.demo
 
 import com.google.firebase.dataconnect.DataConnectException
 import com.google.firebase.dataconnect.ExperimentalFirebaseDataConnect
+import com.google.firebase.dataconnect.LocalDate
 import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase
 import com.google.firebase.dataconnect.generated.GeneratedMutation
 import com.google.firebase.dataconnect.generated.GeneratedQuery
-import com.google.firebase.dataconnect.testutil.dateFromYearMonthDayUTC
 import com.google.firebase.dataconnect.testutil.executeWithEmptyVariables
 import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
-import com.google.firebase.dataconnect.testutil.property.arbitrary.toJavaUtilDate
 import io.kotest.assertions.assertSoftly
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.assertions.withClue
@@ -47,7 +46,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
   @Test
   fun nonNullDate_insert_NormalCases() = runTest {
     checkAll(20, Arb.dataConnect.dateTestData()) {
-      val key = connector.insertNonNullDate.execute(it.toJavaUtilDate()).data.key
+      val key = connector.insertNonNullDate.execute(it.date).data.key
       assertNonNullDateByKeyEquals(key, it.string)
     }
   }
@@ -56,7 +55,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
   fun nonNullDate_insert_EdgeCases() = runTest {
     assertSoftly {
       EdgeCases.dates.all().forEach {
-        val key = connector.insertNonNullDate.execute(it.toJavaUtilDate()).data.key
+        val key = connector.insertNonNullDate.execute(it.date).data.key
         assertNonNullDateByKeyEquals(key, it.string)
       }
     }
@@ -70,14 +69,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD.
     val date = "2024-03-26T19:48:00.144-07:00"
     val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key
-    assertNonNullDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26))
+    assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26))
   }
 
   @Test
   fun nonNullDate_insert_ShouldIgnoreTime() = runTest {
     val date = "2024-03-26T19:48:00.144Z"
     val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key
-    assertNonNullDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26))
+    assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26))
   }
 
   @Test
@@ -92,9 +91,9 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     queryResult.data shouldBe
       GetNonNullDatesWithDefaultsByKeyQuery.Data(
         GetNonNullDatesWithDefaultsByKeyQuery.Data.NonNullDatesWithDefaults(
-          valueWithVariableDefault = dateFromYearMonthDayUTC(6904, 11, 30),
-          valueWithSchemaDefault = dateFromYearMonthDayUTC(2112, 1, 31),
-          epoch = EdgeCases.dates.epoch.toJavaUtilDate(),
+          valueWithVariableDefault = LocalDate(6904, 11, 30),
+          valueWithSchemaDefault = LocalDate(2112, 1, 31),
+          epoch = EdgeCases.dates.epoch.date,
           requestTime1 = expectedRequestTime,
           requestTime2 = expectedRequestTime,
         )
@@ -134,8 +133,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
   @Test
   fun nonNullDate_update_NormalCases() = runTest {
     checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 ->
-      val key = connector.insertNonNullDate.execute(date1.toJavaUtilDate()).data.key
-      connector.updateNonNullDate.execute(key) { value = date2.toJavaUtilDate() }
+      val key = connector.insertNonNullDate.execute(date1.date).data.key
+      connector.updateNonNullDate.execute(key) { value = date2.date }
       assertNonNullDateByKeyEquals(key, date2.string)
     }
   }
@@ -151,8 +150,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     assertSoftly {
       for ((date1, date2) in dates1.zip(dates2)) {
         withClue("date1=${date1.string} date2=${date2.string}") {
-          val key = connector.insertNonNullDate.execute(date1.toJavaUtilDate()).data.key
-          connector.updateNonNullDate.execute(key) { value = date2.toJavaUtilDate() }
+          val key = connector.insertNonNullDate.execute(date1.date).data.key
+          connector.updateNonNullDate.execute(key) { value = date2.date }
           assertNonNullDateByKeyEquals(key, date2.string)
         }
       }
@@ -162,15 +161,15 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
   @Test
   fun nonNullDate_update_DateVariableOmitted() = runTest {
     val date = Arb.dataConnect.dateTestData().next(rs)
-    val key = connector.insertNonNullDate.execute(date.toJavaUtilDate()).data.key
+    val key = connector.insertNonNullDate.execute(date.date).data.key
     connector.updateNonNullDate.execute(key) {}
-    assertNonNullDateByKeyEquals(key, date.toJavaUtilDate())
+    assertNonNullDateByKeyEquals(key, date.date)
   }
 
   @Test
   fun nullableDate_insert_NormalCases() = runTest {
     checkAll(20, Arb.dataConnect.dateTestData()) {
-      val key = connector.insertNullableDate.execute { value = it.toJavaUtilDate() }.data.key
+      val key = connector.insertNullableDate.execute { value = it.date }.data.key
       assertNullableDateByKeyEquals(key, it.string)
     }
   }
@@ -180,7 +179,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     val edgeCases = EdgeCases.dates.all() + listOf(null)
     assertSoftly {
       edgeCases.forEach {
-        val key = connector.insertNullableDate.execute { value = it?.toJavaUtilDate() }.data.key
+        val key = connector.insertNullableDate.execute { value = it?.date }.data.key
         if (it === null) {
           assertNullableDateByKeyHasNullInnerValue(key)
         } else {
@@ -204,14 +203,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD.
     val date = "2024-03-26T19:48:00.144-07:00"
     val key = connector.insertNullableDate.executeWithStringVariables(date).data.key
-    assertNullableDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26))
+    assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26))
   }
 
   @Test
   fun nullableDate_insert_ShouldIgnoreTime() = runTest {
     val date = "2024-03-26T19:48:00.144Z"
     val key = connector.insertNullableDate.executeWithStringVariables(date).data.key
-    assertNullableDateByKeyEquals(key, dateFromYearMonthDayUTC(2024, 3, 26))
+    assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26))
   }
 
   @Test
@@ -242,9 +241,9 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     queryResult.data shouldBe
       GetNullableDatesWithDefaultsByKeyQuery.Data(
         GetNullableDatesWithDefaultsByKeyQuery.Data.NullableDatesWithDefaults(
-          valueWithVariableDefault = dateFromYearMonthDayUTC(8113, 2, 9),
-          valueWithSchemaDefault = dateFromYearMonthDayUTC(1921, 12, 2),
-          epoch = EdgeCases.dates.epoch.toJavaUtilDate(),
+          valueWithVariableDefault = LocalDate(8113, 2, 9),
+          valueWithSchemaDefault = LocalDate(1921, 12, 2),
+          epoch = EdgeCases.dates.epoch.date,
           requestTime1 = expectedRequestTime,
           requestTime2 = expectedRequestTime,
         )
@@ -254,8 +253,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
   @Test
   fun nullableDate_update_NormalCases() = runTest {
     checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 ->
-      val key = connector.insertNullableDate.execute { value = date1.toJavaUtilDate() }.data.key
-      connector.updateNullableDate.execute(key) { value = date2.toJavaUtilDate() }
+      val key = connector.insertNullableDate.execute { value = date1.date }.data.key
+      connector.updateNullableDate.execute(key) { value = date2.date }
       assertNullableDateByKeyEquals(key, date2.string)
     }
   }
@@ -271,8 +270,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     assertSoftly {
       for ((date1, date2) in dates1.zip(dates2)) {
         withClue("date1=${date1.string} date2=${date2.string}") {
-          val key = connector.insertNullableDate.execute { value = date1.toJavaUtilDate() }.data.key
-          connector.updateNullableDate.execute(key) { value = date2.toJavaUtilDate() }
+          val key = connector.insertNullableDate.execute { value = date1.date }.data.key
+          connector.updateNullableDate.execute(key) { value = date2.date }
           assertNullableDateByKeyEquals(key, date2.string)
         }
       }
@@ -281,26 +280,26 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
 
   @Test
   fun nullableDate_update_UpdateNonNullValueToNull() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate()
-    val key = connector.insertNullableDate.execute { value = date }.data.key
+    val date = Arb.dataConnect.dateTestData().next(rs)
+    val key = connector.insertNullableDate.execute { value = date.date }.data.key
     connector.updateNullableDate.execute(key) { value = null }
     assertNullableDateByKeyHasNullInnerValue(key)
   }
 
   @Test
   fun nullableDate_update_UpdateNullValueToNonNull() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate()
+    val date = Arb.dataConnect.dateTestData().next(rs)
     val key = connector.insertNullableDate.execute { value = null }.data.key
-    connector.updateNullableDate.execute(key) { value = date }
-    assertNullableDateByKeyEquals(key, date)
+    connector.updateNullableDate.execute(key) { value = date.date }
+    assertNullableDateByKeyEquals(key, date.date)
   }
 
   @Test
   fun nullableDate_update_DateVariableOmitted() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs).toJavaUtilDate()
-    val key = connector.insertNullableDate.execute { value = date }.data.key
+    val date = Arb.dataConnect.dateTestData().next(rs)
+    val key = connector.insertNullableDate.execute { value = date.date }.data.key
     connector.updateNullableDate.execute(key) {}
-    assertNullableDateByKeyEquals(key, date)
+    assertNullableDateByKeyEquals(key, date.date)
   }
 
   private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: String) {
@@ -311,7 +310,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     queryResult.data shouldBe GetDateByKeyQueryStringData(expected)
   }
 
-  private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: java.util.Date) {
+  private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: LocalDate) {
     val queryResult = connector.getNonNullDateByKey.execute(key)
     queryResult.data shouldBe
       GetNonNullDateByKeyQuery.Data(GetNonNullDateByKeyQuery.Data.Value(expected))
@@ -334,10 +333,7 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
     queryResult.data shouldBe GetDateByKeyQueryStringData(expected)
   }
 
-  private suspend fun assertNullableDateByKeyEquals(
-    key: NullableDateKey,
-    expected: java.util.Date
-  ) {
+  private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: LocalDate) {
     val queryResult = connector.getNullableDateByKey.execute(key)
     queryResult.data shouldBe
       GetNullableDateByKeyQuery.Data(GetNullableDateByKeyQuery.Data.Value(expected))
@@ -345,8 +341,8 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
 
   /**
    * A `Data` type that can be used in place of [GetNonNullDateByKeyQuery.Data] that types the value
-   * as a [String] instead of a [java.util.Date], allowing verification of the data sent over the
-   * wire without possible confounding from date deserialization.
+   * as a [String] instead of a [LocalDate], allowing verification of the data sent over the wire
+   * without possible confounding from date deserialization.
    */
   @Serializable
   private data class GetDateByKeyQueryStringData(val value: DateStringValue?) {
@@ -357,14 +353,14 @@ class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
 
   /**
    * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that
-   * types the value as a [String] instead of a [java.util.Date], allowing verification of the data
-   * sent over the wire without possible confounding from date serialization.
+   * types the value as a [String] instead of a [LocalDate], allowing verification of the data sent
+   * over the wire without possible confounding from date serialization.
    */
   @Serializable private data class InsertDateStringVariables(val value: String?)
 
   /**
    * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that
-   * types the value as a [Int] instead of a [java.util.Date], allowing verification that the server
+   * types the value as a [Int] instead of a [LocalDate], allowing verification that the server
    * fails with an expected error (rather than crashing, for example).
    */
   @Serializable private data class InsertDateIntVariables(val value: Int)

From 8addb04550719ce007f50ee35a1ed035de32875f Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 20:16:24 +0000
Subject: [PATCH 05/13] Remove unused test helper functions related to
 java.util.Date

---
 .../dataconnect/testutil/DateTimeTestUtils.kt | 21 -------------------
 .../testutil/property/arbitrary/dates.kt      |  4 ----
 2 files changed, 25 deletions(-)

diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt
index f3d3018d2dd..a0cfdcbcf3c 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DateTimeTestUtils.kt
@@ -18,31 +18,10 @@ package com.google.firebase.dataconnect.testutil
 
 import com.google.firebase.Timestamp
 import java.util.Calendar
-import java.util.Date
 import java.util.GregorianCalendar
 import java.util.TimeZone
 import kotlin.random.Random
 
-/**
- * Creates and returns a new [Date] object that represents the given year, month, and day in UTC.
- *
- * @param year The year; must be between 0 and 9999, inclusive.
- * @param month The month; must be between 1 and 12, inclusive.
- * @param day The day of the month; must be between 1 and 31, inclusive.
- */
-fun dateFromYearMonthDayUTC(year: Int, month: Int, day: Int): Date {
-  require(year in 0..9999) { "year must be between 0 and 9999, inclusive" }
-  require(month in 1..12) { "month must be between 1 and 12, inclusive" }
-  require(day in 1..31) { "day must be between 1 and 31, inclusive" }
-
-  return GregorianCalendar(TimeZone.getTimeZone("UTC"))
-    .apply {
-      set(year, month - 1, day, 0, 0, 0)
-      set(Calendar.MILLISECOND, 0)
-    }
-    .time
-}
-
 /** Generates and returns a random [Timestamp] object. */
 fun randomTimestamp(): Timestamp {
   val nanoseconds = Random.nextInt(1_000_000_000)
diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
index 6959ccbd50b..6ed685f0476 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
@@ -20,7 +20,6 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary
 
 import com.google.firebase.dataconnect.LocalDate
 import com.google.firebase.dataconnect.testutil.NullableReference
-import com.google.firebase.dataconnect.testutil.dateFromYearMonthDayUTC
 import com.google.firebase.dataconnect.testutil.dayRangeInYear
 import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MAX_YEAR
 import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MIN_YEAR
@@ -102,9 +101,6 @@ data class DateTestData(
   val string: String,
 )
 
-fun DateTestData.toJavaUtilDate(): java.util.Date =
-  dateFromYearMonthDayUTC(year = date.year, month = date.month, day = date.day)
-
 @Suppress("MemberVisibilityCanBePrivate")
 object DateEdgeCases {
   // See https://en.wikipedia.org/wiki/ISO_8601#Years for rationale of lower bound of 1583.

From ef7b70b1eca8d00a8f226078a1c7dfaf7eecd243 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 21:21:59 +0000
Subject: [PATCH 06/13] DateSerializer removed, as it is superceded by
 LocalDateSerializer

---
 .../dataconnect/serializers/DateSerializer.kt | 76 -------------------
 1 file changed, 76 deletions(-)
 delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt

diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt
deleted file mode 100644
index 6ff9cb79c13..00000000000
--- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/DateSerializer.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2024 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
- *
- *      http://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.firebase.dataconnect.serializers
-
-import java.util.Calendar
-import java.util.Date
-import java.util.GregorianCalendar
-import java.util.TimeZone
-import java.util.regex.Matcher
-import java.util.regex.Pattern
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-
-/**
- * An implementation of [KSerializer] for serializing and deserializing [Date] objects in the wire
- * format expected by the Firebase Data Connect backend.
- */
-public object DateSerializer : KSerializer<Date> {
-
-  override val descriptor: SerialDescriptor =
-    PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
-
-  override fun serialize(encoder: Encoder, value: Date) {
-    val calendar = GregorianCalendar(TimeZone.getTimeZone("UTC"))
-    calendar.time = value
-
-    val year = calendar.get(Calendar.YEAR)
-    val month = calendar.get(Calendar.MONTH) + 1
-    val day = calendar.get(Calendar.DAY_OF_MONTH)
-
-    val serializedDate =
-      "$year".padStart(4, '0') + '-' + "$month".padStart(2, '0') + '-' + "$day".padStart(2, '0')
-    encoder.encodeString(serializedDate)
-  }
-
-  override fun deserialize(decoder: Decoder): Date {
-    val serializedDate = decoder.decodeString()
-
-    val matcher = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})$").matcher(serializedDate)
-    require(matcher.matches()) { "date does not match regular expression: ${matcher.pattern()}" }
-
-    fun Matcher.groupToIntIgnoringLeadingZeroes(index: Int): Int {
-      val groupText = group(index)!!.trimStart('0')
-      return if (groupText.isEmpty()) 0 else groupText.toInt()
-    }
-
-    val year = matcher.groupToIntIgnoringLeadingZeroes(1)
-    val month = matcher.groupToIntIgnoringLeadingZeroes(2)
-    val day = matcher.groupToIntIgnoringLeadingZeroes(3)
-
-    return GregorianCalendar(TimeZone.getTimeZone("UTC"))
-      .apply {
-        set(year, month - 1, day, 0, 0, 0)
-        set(Calendar.MILLISECOND, 0)
-      }
-      .time
-  }
-}

From 4574d4e4c2edb012d987b135b974c470e66b3b71 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 21:25:41 +0000
Subject: [PATCH 07/13] firebase-dataconnect/api.txt updated by running
 ./gradlew :firebase-dataconnect:generateApiTxtFile

---
 firebase-dataconnect/api.txt | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt
index e1ebc29d5b8..11e44aa0dae 100644
--- a/firebase-dataconnect/api.txt
+++ b/firebase-dataconnect/api.txt
@@ -310,14 +310,6 @@ package com.google.firebase.dataconnect.serializers {
     field @NonNull public static final com.google.firebase.dataconnect.serializers.AnyValueSerializer INSTANCE;
   }
 
-  public final class DateSerializer implements kotlinx.serialization.KSerializer<java.util.Date> {
-    method @NonNull public java.util.Date deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
-    method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
-    method public void serialize(@NonNull kotlinx.serialization.encoding.Encoder encoder, @NonNull java.util.Date value);
-    property @NonNull public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
-    field @NonNull public static final com.google.firebase.dataconnect.serializers.DateSerializer INSTANCE;
-  }
-
   public final class LocalDateSerializer implements kotlinx.serialization.KSerializer<com.google.firebase.dataconnect.LocalDate> {
     method @NonNull public com.google.firebase.dataconnect.LocalDate deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
     method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();

From b806a077d07e867fd7e3e9637471c5f7f8a3e0fc Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 21:34:18 +0000
Subject: [PATCH 08/13] firebase-dataconnect/CHANGELOG.md updated

---
 firebase-dataconnect/CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md
index 8acc4d43026..d7840f19c2b 100644
--- a/firebase-dataconnect/CHANGELOG.md
+++ b/firebase-dataconnect/CHANGELOG.md
@@ -23,6 +23,13 @@
 * [changed] Replaced java.util.Date with
   com.google.firebase.dataconnect.LocalDate.
   ([#6434](https://github.com/firebase/firebase-android-sdk/pull/6434))
+* [changed] `DateSerializer` removed, as it is superceded by
+  `LocalDateSerializer`.
+  As of Data Connect emulator version 1.7.0, the generated Kotlin code uses
+  `com.google.firebase.dataconnect.LocalDate` instead of `java.util.Date`.
+  Therefore, this version of the SDK must be paired with an appropriate version
+  of the Data Connect emulator.
+  ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN))
 
 # 16.0.0-beta02
 * [changed] Updated protobuf dependency to `3.25.5` to fix

From c2c9273a1435b3722731df35945e9f8a02616707 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Wed, 13 Nov 2024 21:38:40 +0000
Subject: [PATCH 09/13] firebase-dataconnect/CHANGELOG.md: update PR number

---
 firebase-dataconnect/CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md
index d7840f19c2b..449f1ddd3a1 100644
--- a/firebase-dataconnect/CHANGELOG.md
+++ b/firebase-dataconnect/CHANGELOG.md
@@ -29,7 +29,7 @@
   `com.google.firebase.dataconnect.LocalDate` instead of `java.util.Date`.
   Therefore, this version of the SDK must be paired with an appropriate version
   of the Data Connect emulator.
-  ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN))
+  ([#6513](https://github.com/firebase/firebase-android-sdk/pull/6513))
 
 # 16.0.0-beta02
 * [changed] Updated protobuf dependency to `3.25.5` to fix

From 990c43ad58ac9cd742aca8612713a8251009920e Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Thu, 14 Nov 2024 03:16:22 +0000
Subject: [PATCH 10/13] DateScalarIntegrationTest.kt rewritten

---
 .../demo/DateScalarIntegrationTest.kt         | 1137 ++++++++++++-----
 .../dataconnect/connector/demo/demo_ops.gql   |   52 -
 .../dataconnect/schema/demo_schema.gql        |   24 -
 .../testutil/property/arbitrary/dates.kt      |    7 +
 4 files changed, 830 insertions(+), 390 deletions(-)

diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
index 67eac700af0..31478621e69 100644
--- a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
+++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/DateScalarIntegrationTest.kt
@@ -14,28 +14,50 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalFirebaseDataConnect::class)
+@file:OptIn(ExperimentalKotest::class, ExperimentalFirebaseDataConnect::class)
 
 package com.google.firebase.dataconnect.connectors.demo
 
 import com.google.firebase.dataconnect.DataConnectException
 import com.google.firebase.dataconnect.ExperimentalFirebaseDataConnect
 import com.google.firebase.dataconnect.LocalDate
+import com.google.firebase.dataconnect.MutationResult
+import com.google.firebase.dataconnect.QueryResult
 import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase
-import com.google.firebase.dataconnect.generated.GeneratedMutation
 import com.google.firebase.dataconnect.generated.GeneratedQuery
-import com.google.firebase.dataconnect.testutil.executeWithEmptyVariables
+import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData
 import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
 import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString
+import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate
+import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
+import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate
+import io.kotest.assertions.asClue
 import io.kotest.assertions.assertSoftly
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.assertions.withClue
+import io.kotest.common.ExperimentalKotest
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldBeIn
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
+import io.kotest.matchers.nulls.shouldBeNull
+import io.kotest.matchers.nulls.shouldNotBeNull
 import io.kotest.matchers.shouldBe
 import io.kotest.property.Arb
-import io.kotest.property.arbitrary.int
+import io.kotest.property.EdgeConfig
+import io.kotest.property.PropTestConfig
 import io.kotest.property.arbitrary.next
+import io.kotest.property.arbitrary.withEdgecases
 import io.kotest.property.checkAll
+import java.util.UUID
+import kotlin.time.Duration.Companion.minutes
 import kotlinx.coroutines.test.runTest
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.serializer
@@ -43,416 +65,903 @@ import org.junit.Test
 
 class DateScalarIntegrationTest : DemoConnectorIntegrationTestBase() {
 
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for CRUD operations on this table:
+  // type DateNonNullable @table { value: Date!, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
   @Test
-  fun nonNullDate_insert_NormalCases() = runTest {
-    checkAll(20, Arb.dataConnect.dateTestData()) {
-      val key = connector.insertNonNullDate.execute(it.date).data.key
-      assertNonNullDateByKeyEquals(key, it.string)
+  fun dateNonNullable_MutationLocalDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.dateTestData()) { testData ->
+        val insertResult = connector.dateNonNullableInsert.execute(testData.date)
+        val returnedString =
+          connector.dateNonNullableGetByKey.executeWithStringData(insertResult.data.key)
+        returnedString shouldBe testData.string
+      }
     }
-  }
 
   @Test
-  fun nonNullDate_insert_EdgeCases() = runTest {
-    assertSoftly {
-      EdgeCases.dates.all().forEach {
-        val key = connector.insertNonNullDate.execute(it.date).data.key
-        assertNonNullDateByKeyEquals(key, it.string)
+  fun dateNonNullable_MutationStringVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.dateTestData()) { testData ->
+        val insertResult = connector.dateNonNullableInsert.execute(testData.string)
+        val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key)
+        queryResult.data.item?.value shouldBe testData.date
       }
     }
-  }
 
   @Test
-  fun nonNullDate_insert_ShouldIgnoreTimeZone() = runTest {
-    // Use a date that, when converted to UTC, in on a different date to verify that the server does
-    // the expected thing; that is, that it _drops_ the time zone information (rather than
-    // converting the date to UTC then taking the YYYY-MM-DD of that). The server would use the date
-    // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD.
-    val date = "2024-03-26T19:48:00.144-07:00"
-    val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key
-    assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26))
-  }
+  fun dateNonNullable_QueryLocalDateVariable() =
+    dateNonNullable_QueryVariable { tag, dateTestData ->
+      connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, dateTestData.date)
+    }
 
   @Test
-  fun nonNullDate_insert_ShouldIgnoreTime() = runTest {
-    val date = "2024-03-26T19:48:00.144Z"
-    val key = connector.insertNonNullDate.executeWithStringVariables(date).data.key
-    assertNonNullDateByKeyEquals(key, LocalDate(2024, 3, 26))
-  }
+  fun dateNonNullable_QueryStringVariable() = dateNonNullable_QueryVariable { tag, dateTestData ->
+    connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, value = dateTestData.string)
+  }
+
+  private fun dateNonNullable_QueryVariable(
+    executeQuery:
+      suspend (tag: String, date: DateTestData) -> QueryResult<
+          DateNonNullableGetAllByTagAndValueQuery.Data, *
+        >
+  ) =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val queryResult = executeQuery(tag, testDatas.selected!!)
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
 
   @Test
-  fun nonNullDatesWithDefaults_insert_ShouldUseDefaultValuesIfNoVariablesSpecified() = runTest {
-    val key = connector.insertNonNullDatesWithDefaults.execute {}.data.key
-    val queryResult = connector.getNonNullDatesWithDefaultsByKey.execute(key)
+  fun dateNonNullable_MutationNullVariableShouldThrow() = runTest {
+    val exception =
+      shouldThrow<DataConnectException> { connector.dateNonNullableInsert.execute(null) }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
 
-    // Since we can't know the exact value of `request.time` just make sure that the exact same
-    // value is used for both fields to which it is set.
-    val expectedRequestTime = queryResult.data.nonNullDatesWithDefaults!!.requestTime1
+  @Test
+  fun dateNonNullable_QueryNullVariableShouldThrow() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val exception =
+      shouldThrow<DataConnectException> {
+        connector.dateNonNullableGetAllByTagAndValue.execute(tag = tag, value = null)
+      }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
 
-    queryResult.data shouldBe
-      GetNonNullDatesWithDefaultsByKeyQuery.Data(
-        GetNonNullDatesWithDefaultsByKeyQuery.Data.NonNullDatesWithDefaults(
-          valueWithVariableDefault = LocalDate(6904, 11, 30),
-          valueWithSchemaDefault = LocalDate(2112, 1, 31),
-          epoch = EdgeCases.dates.epoch.date,
-          requestTime1 = expectedRequestTime,
-          requestTime2 = expectedRequestTime,
-        )
+  @Test
+  fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+    val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {}
+    queryResult.data.items
+      .map { it.id }
+      .shouldContainExactlyInAnyOrder(
+        insertResult.data.key1.id,
+        insertResult.data.key2.id,
+        insertResult.data.key3.id
       )
   }
 
   @Test
-  fun nonNullDate_insert_ShouldFailIfDateVariableIsNull() = runTest {
-    shouldThrow<DataConnectException> {
-      connector.insertNonNullDate.executeWithStringVariables(null).data.key
-    }
+  fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    connector.dateNonNullableInsert3.execute(tag, testDatas)
+    val queryResult =
+      connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) { value = null }
+    queryResult.data.items.shouldBeEmpty()
   }
 
   @Test
-  fun nonNullDate_insert_ShouldFailIfDateVariableIsAnInt() = runTest {
-    shouldThrow<DataConnectException> {
-      connector.insertNonNullDate.executeWithIntVariables(Arb.int().next(rs)).data.key
+  fun dateNonNullable_Update() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.localDate(), Arb.dataConnect.localDate()) {
+        date1,
+        date2 ->
+        val insertResult = connector.dateNonNullableInsert.execute(date1)
+        val updateResult =
+          connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) { value = date2 }
+        updateResult.asClue { it.data.key shouldBe insertResult.data.key }
+        val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key)
+        val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+        item.value shouldBe date2
+      }
     }
-  }
 
   @Test
-  fun nonNullDate_insert_ShouldFailIfDateVariableIsOmitted() = runTest {
-    shouldThrow<DataConnectException> {
-      connector.insertNonNullDate.executeWithEmptyVariables().data.key
+  fun dateNonNullable_UpdateToNullShouldFail() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.localDate()) { date ->
+        val insertResult = connector.dateNonNullableInsert.execute(date)
+        shouldThrow<DataConnectException> {
+          connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) { value = null }
+        }
+      }
     }
-  }
 
   @Test
-  fun nonNullDate_insert_ShouldFailIfDateVariableIsMalformed() = runTest {
-    for (invalidDate in invalidDates) {
-      shouldThrow<DataConnectException> {
-        connector.insertNonNullDate.executeWithStringVariables(invalidDate).data.key
+  fun dateNonNullable_UpdateToOmittedShouldLeaveValueUnchanged() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.localDate()) { date ->
+        val insertResult = connector.dateNonNullableInsert.execute(date)
+        val updateResult = connector.dateNonNullableUpdateByKey.execute(insertResult.data.key) {}
+        updateResult.asClue { it.data.key shouldBe insertResult.data.key }
+        val queryResult = connector.dateNonNullableGetByKey.execute(insertResult.data.key)
+        val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+        item.value shouldBe date
       }
     }
-  }
 
   @Test
-  fun nonNullDate_update_NormalCases() = runTest {
-    checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 ->
-      val key = connector.insertNonNullDate.execute(date1.date).data.key
-      connector.updateNonNullDate.execute(key) { value = date2.date }
-      assertNonNullDateByKeyEquals(key, date2.string)
+  fun dateNonNullable_UpdateMany() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData(),
+        Arb.dataConnect.localDate()
+      ) { tag, testDatas, date2 ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected!!
+        val updateResult =
+          connector.dateNonNullableUpdateByTagAndValue.execute(tag) {
+            value = selectedDate.date
+            newValue = date2
+          }
+        withClue("updateResult.data.count") {
+          updateResult.data.count shouldBe testDatas.numMatchingSelected
+        }
+        val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, date2)
+        val matchingIds1 = testDatas.idsMatchingSelected(insertResult)
+        val matchingIds2 = testDatas.idsMatching(insertResult, date2)
+        val matchingIds = (matchingIds1 + matchingIds2).distinct()
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
     }
-  }
 
   @Test
-  fun nonNullDate_update_EdgeCases() = runTest {
-    val edgeCases = EdgeCases.dates.all()
-    val dates1 =
-      edgeCases + List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases
-    val dates2 =
-      List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases + edgeCases
+  fun dateNonNullable_UpdateManyNullValueShouldUpdateNone() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData(),
+        Arb.dataConnect.localDate()
+      ) { tag, testDatas, date2 ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val updateResult =
+          connector.dateNonNullableUpdateByTagAndValue.execute(tag) {
+            value = null
+            newValue = date2
+          }
+        withClue("updateResult.data.count") { updateResult.data.count shouldBe 0 }
+        val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {}
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id,
+          )
+      }
+    }
 
-    assertSoftly {
-      for ((date1, date2) in dates1.zip(dates2)) {
-        withClue("date1=${date1.string} date2=${date2.string}") {
-          val key = connector.insertNonNullDate.execute(date1.date).data.key
-          connector.updateNonNullDate.execute(key) { value = date2.date }
-          assertNonNullDateByKeyEquals(key, date2.string)
+  @Test
+  fun dateNonNullable_UpdateManyNullNewValueShouldThrow() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData(),
+      ) { tag, testDatas ->
+        connector.dateNonNullableInsert3.execute(tag, testDatas)
+        shouldThrow<DataConnectException> {
+          connector.dateNonNullableUpdateByTagAndValue.execute(tag) { newValue = null }
         }
       }
     }
-  }
 
   @Test
-  fun nonNullDate_update_DateVariableOmitted() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs)
-    val key = connector.insertNonNullDate.execute(date.date).data.key
-    connector.updateNonNullDate.execute(key) {}
-    assertNonNullDateByKeyEquals(key, date.date)
-  }
+  fun dateNonNullable_UpdateManyOmittedValueShouldUpdateAll() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData(),
+        Arb.dataConnect.localDate()
+      ) { tag, testDatas, date2 ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val updateResult =
+          connector.dateNonNullableUpdateByTagAndValue.execute(tag) { newValue = date2 }
+        withClue("updateResult.data.count") { updateResult.data.count shouldBe 3 }
+        val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, date2)
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id,
+          )
+      }
+    }
 
   @Test
-  fun nullableDate_insert_NormalCases() = runTest {
-    checkAll(20, Arb.dataConnect.dateTestData()) {
-      val key = connector.insertNullableDate.execute { value = it.date }.data.key
-      assertNullableDateByKeyEquals(key, it.string)
+  fun dateNonNullable_UpdateManyOmittedNewValueShouldNotChangeAny() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected!!.date
+        val updateResult =
+          connector.dateNonNullableUpdateByTagAndValue.execute(tag) { value = selectedDate }
+        withClue("updateResult.data.count") {
+          updateResult.data.count shouldBe testDatas.numMatchingSelected
+        }
+        val queryResult = connector.dateNonNullableGetAllByTagAndValue.execute(tag, selectedDate)
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
     }
-  }
 
   @Test
-  fun nullableDate_insert_EdgeCases() = runTest {
-    val edgeCases = EdgeCases.dates.all() + listOf(null)
-    assertSoftly {
-      edgeCases.forEach {
-        val key = connector.insertNullableDate.execute { value = it?.date }.data.key
-        if (it === null) {
-          assertNullableDateByKeyHasNullInnerValue(key)
-        } else {
-          assertNullableDateByKeyEquals(key, it.string)
+  fun dateNonNullable_DeleteMany() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected!!.date
+        val deleteResult =
+          connector.dateNonNullableDeleteByTagAndValue.execute(tag) { value = selectedDate }
+        withClue("deleteResult.data.count") {
+          deleteResult.data.count shouldBe testDatas.numMatchingSelected
         }
+        val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {}
+        val insertedIds = insertResult.data.run { listOf(key1, key2, key3).map { it.id } }
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        val remainingIds = insertedIds.filterNot { it in matchingIds }
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder remainingIds
       }
     }
-  }
 
   @Test
-  fun nullableDate_insert_ShouldUseNullIfDateVariableIsOmitted() = runTest {
-    val key = connector.insertNullableDate.execute {}.data.key
-    assertNullableDateByKeyHasNullInnerValue(key)
-  }
+  fun dateNonNullable_DeleteManyNullValueShouldDeleteNone() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val deleteResult =
+          connector.dateNonNullableDeleteByTagAndValue.execute(tag) { value = null }
+        withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 0 }
+        val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {}
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id,
+          )
+      }
+    }
 
   @Test
-  fun nullableDate_insert_ShouldIgnoreTimeZone() = runTest {
-    // Use a date that, when converted to UTC, in on a different date to verify that the server does
-    // the expected thing; that is, that it _drops_ the time zone information (rather than
-    // converting the date to UTC then taking the YYYY-MM-DD of that). The server would use the date
-    // "2024-03-27" if it did the erroneous conversion to UTC before taking the YYYY-MM-DD.
-    val date = "2024-03-26T19:48:00.144-07:00"
-    val key = connector.insertNullableDate.executeWithStringVariables(date).data.key
-    assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26))
-  }
+  fun dateNonNullable_DeleteManyOmittedValueShouldDeleteAll() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData(),
+      ) { tag, testDatas ->
+        connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val deleteResult = connector.dateNonNullableDeleteByTagAndValue.execute(tag) {}
+        withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 3 }
+        val queryResult = connector.dateNonNullableGetAllByTagAndMaybeValue.execute(tag) {}
+        queryResult.data.items.shouldBeEmpty()
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for inserting into and querying this table:
+  // type DateNullable @table { value: Date, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
 
   @Test
-  fun nullableDate_insert_ShouldIgnoreTime() = runTest {
-    val date = "2024-03-26T19:48:00.144Z"
-    val key = connector.insertNullableDate.executeWithStringVariables(date).data.key
-    assertNullableDateByKeyEquals(key, LocalDate(2024, 3, 26))
-  }
+  fun dateNullable_MutationLocalDateVariable() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.dateTestData().orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates) { testData ->
+        val insertResult = connector.dateNullableInsert.execute { value = testData.ref?.date }
+        val returnedString =
+          connector.dateNullableGetByKey.executeWithStringData(insertResult.data.key)
+        returnedString shouldBe testData.ref?.string
+      }
+    }
 
   @Test
-  fun nullableDate_insert_ShouldFailIfDateVariableIsAnInt() = runTest {
-    shouldThrow<DataConnectException> {
-      connector.insertNullableDate.executeWithIntVariables(Arb.int().next(rs)).data.key
+  fun dateNullable_MutationStringVariable() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.dateTestData().orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates) { testData ->
+        val insertResult = connector.dateNullableInsert.execute(testData.ref?.string)
+        val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key)
+        queryResult.data.item?.value shouldBe testData.ref?.date
+      }
     }
+
+  @Test
+  fun dateNullable_QueryLocalDateVariable() = dateNullable_QueryVariable { tag, dateTestData ->
+    connector.dateNullableGetAllByTagAndValue.execute(tag = tag) { value = dateTestData?.date }
   }
 
   @Test
-  fun nullableDate_insert_ShouldFailIfDateVariableIsMalformed() = runTest {
-    for (invalidDate in invalidDates) {
-      shouldThrow<DataConnectException> {
-        connector.insertNonNullDate.executeWithStringVariables(invalidDate).data.key
+  fun dateNullable_QueryStringVariable() = dateNullable_QueryVariable { tag, dateTestData ->
+    connector.dateNullableGetAllByTagAndValue.execute(tag = tag, value = dateTestData?.string)
+  }
+
+  private fun dateNullable_QueryVariable(
+    executeQuery:
+      suspend (tag: String, date: DateTestData?) -> QueryResult<
+          DateNullableGetAllByTagAndValueQuery.Data, *
+        >
+  ) =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val queryResult = executeQuery(tag, testDatas.selected)
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
       }
     }
-  }
 
   @Test
-  fun nullableDatesWithDefaults_insert_ShouldUseDefaultValuesIfNoVariablesSpecified() = runTest {
-    val key = connector.insertNullableDatesWithDefaults.execute {}.data.key
-    val queryResult = connector.getNullableDatesWithDefaultsByKey.execute(key)
+  fun dateNullable_QueryOmittedVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {}
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id
+          )
+      }
+    }
 
-    // Since we can't know the exact value of `request.time` just make sure that the exact same
-    // value is used for both fields to which it is set.
-    val expectedRequestTime = queryResult.data.nullableDatesWithDefaults!!.requestTime1
+  @Test
+  fun dateNullable_Update() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates, localDates) { date1, date2 ->
+        val insertResult = connector.dateNullableInsert.execute { value = date1.ref }
+        val updateResult =
+          connector.dateNullableUpdateByKey.execute(insertResult.data.key) { value = date2.ref }
+        updateResult.asClue { it.data.key shouldBe insertResult.data.key }
+        val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key)
+        val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+        item.value shouldBe date2.ref
+      }
+    }
 
-    queryResult.data shouldBe
-      GetNullableDatesWithDefaultsByKeyQuery.Data(
-        GetNullableDatesWithDefaultsByKeyQuery.Data.NullableDatesWithDefaults(
-          valueWithVariableDefault = LocalDate(8113, 2, 9),
-          valueWithSchemaDefault = LocalDate(1921, 12, 2),
-          epoch = EdgeCases.dates.epoch.date,
-          requestTime1 = expectedRequestTime,
-          requestTime2 = expectedRequestTime,
-        )
-      )
-  }
+  @Test
+  fun dateNullable_UpdateToOmittedShouldLeaveValueUnchanged() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates) { date ->
+        val insertResult = connector.dateNullableInsert.execute { value = date.ref }
+        val updateResult = connector.dateNullableUpdateByKey.execute(insertResult.data.key) {}
+        updateResult.asClue { it.data.key shouldBe insertResult.data.key }
+        val queryResult = connector.dateNullableGetByKey.execute(insertResult.data.key)
+        val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+        item.value shouldBe date.ref
+      }
+    }
 
   @Test
-  fun nullableDate_update_NormalCases() = runTest {
-    checkAll(20, Arb.dataConnect.dateTestData(), Arb.dataConnect.dateTestData()) { date1, date2 ->
-      val key = connector.insertNullableDate.execute { value = date1.date }.data.key
-      connector.updateNullableDate.execute(key) { value = date2.date }
-      assertNullableDateByKeyEquals(key, date2.string)
+  fun dateNullable_UpdateMany() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData(),
+        localDates
+      ) { tag, testDatas, date2 ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected?.date
+        val updateResult =
+          connector.dateNullableUpdateByTagAndValue.execute(tag) {
+            value = selectedDate
+            newValue = date2.ref
+          }
+        withClue("updateResult.data.count") {
+          updateResult.data.count shouldBe testDatas.numMatchingSelected
+        }
+        val queryResult =
+          connector.dateNullableGetAllByTagAndValue.execute(tag) { value = date2.ref }
+        val matchingIds1 = testDatas.idsMatchingSelected(insertResult)
+        val matchingIds2 = testDatas.idsMatching(insertResult, date2.ref)
+        val matchingIds = (matchingIds1 + matchingIds2).distinct()
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
     }
-  }
 
   @Test
-  fun nullableDate_update_EdgeCases() = runTest {
-    val edgeCases = EdgeCases.dates.all()
-    val dates1 =
-      edgeCases + List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases
-    val dates2 =
-      List(edgeCases.size) { Arb.dataConnect.dateTestData().next(rs) } + edgeCases + edgeCases
+  fun dateNullable_UpdateManyOmittedValueShouldUpdateAll() =
+    runTest(timeout = 1.minutes) {
+      val localDates = Arb.dataConnect.localDate().orNullableReference(nullProbability = 0.2)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData(),
+        localDates
+      ) { tag, testDatas, date2 ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val updateResult =
+          connector.dateNullableUpdateByTagAndValue.execute(tag) { newValue = date2.ref }
+        withClue("updateResult.data.count") { updateResult.data.count shouldBe 3 }
+        val queryResult =
+          connector.dateNullableGetAllByTagAndValue.execute(tag) { value = date2.ref }
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id,
+          )
+      }
+    }
+
+  @Test
+  fun dateNullable_UpdateManyOmittedNewValueShouldNotChangeAny() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected?.date
+        val updateResult =
+          connector.dateNullableUpdateByTagAndValue.execute(tag) { value = selectedDate }
+        withClue("updateResult.data.count") {
+          updateResult.data.count shouldBe testDatas.numMatchingSelected
+        }
+        val queryResult =
+          connector.dateNullableGetAllByTagAndValue.execute(tag) { value = selectedDate }
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_DeleteMany() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData(),
+      ) { tag, testDatas ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val selectedDate = testDatas.selected?.date
+        val deleteResult =
+          connector.dateNullableDeleteByTagAndValue.execute(tag) { value = selectedDate }
+        withClue("deleteResult.data.count") {
+          deleteResult.data.count shouldBe testDatas.numMatchingSelected
+        }
+        val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {}
+        val insertedIds = insertResult.data.run { listOf(key1, key2, key3).map { it.id } }
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        val remainingIds = insertedIds.filterNot { it in matchingIds }
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder remainingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_DeleteManyOmittedValueShouldDeleteAll() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData(),
+      ) { tag, testDatas ->
+        connector.dateNullableInsert3.execute(tag, testDatas)
+        val deleteResult = connector.dateNullableDeleteByTagAndValue.execute(tag) {}
+        withClue("deleteResult.data.count") { deleteResult.data.count shouldBe 3 }
+        val queryResult = connector.dateNullableGetAllByTagAndValue.execute(tag) {}
+        queryResult.data.items.shouldBeEmpty()
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for default `Date` variable values.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationVariableDefaults() = runTest {
+    val insertResult = connector.dateNonNullableWithDefaultsInsert.execute {}
+    val queryResult = connector.dateNonNullableWithDefaultsGetByKey.execute(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
 
     assertSoftly {
-      for ((date1, date2) in dates1.zip(dates2)) {
-        withClue("date1=${date1.string} date2=${date2.string}") {
-          val key = connector.insertNullableDate.execute { value = date1.date }.data.key
-          connector.updateNullableDate.execute(key) { value = date2.date }
-          assertNullableDateByKeyEquals(key, date2.string)
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe LocalDate(6904, 11, 30)
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe LocalDate(2112, 1, 31)
         }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
       }
     }
+
+    withClue("requestTime validation") {
+      val today = connector.requestTime().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
   }
 
   @Test
-  fun nullableDate_update_UpdateNonNullValueToNull() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs)
-    val key = connector.insertNullableDate.execute { value = date.date }.data.key
-    connector.updateNullableDate.execute(key) { value = null }
-    assertNullableDateByKeyHasNullInnerValue(key)
-  }
+  fun dateNonNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(LocalDate(2692, 5, 21), "2692-05-21")
+      val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threeNonNullDatesTestData(localDateArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = connector.dateNonNullableInsert3.execute(tag, testDatas)
+        val queryResult = connector.dateNonNullableGetAllByTagAndDefaultValue.execute(tag) {}
+        val matchingIds = testDatas.idsMatching(insertResult, defaultTestData.date)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
 
   @Test
-  fun nullableDate_update_UpdateNullValueToNonNull() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs)
-    val key = connector.insertNullableDate.execute { value = null }.data.key
-    connector.updateNullableDate.execute(key) { value = date.date }
-    assertNullableDateByKeyEquals(key, date.date)
+  fun dateNullable_MutationVariableDefaults() = runTest {
+    val insertResult = connector.dateNullableWithDefaultsInsert.execute {}
+    val queryResult = connector.dateNullableWithDefaultsGetByKey.execute(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+
+    assertSoftly {
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe LocalDate(8113, 2, 9)
+        }
+        withClue("valueWithVariableNullDefault") {
+          item.valueWithVariableNullDefault.shouldBeNull()
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe LocalDate(1921, 12, 2)
+        }
+        withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() }
+        withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date }
+        withClue("requestTime1") { item.requestTime1.shouldNotBeNull() }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
+      }
+    }
+
+    withClue("requestTime validation") {
+      val today = connector.requestTime().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
   }
 
   @Test
-  fun nullableDate_update_DateVariableOmitted() = runTest {
-    val date = Arb.dataConnect.dateTestData().next(rs)
-    val key = connector.insertNullableDate.execute { value = date.date }.data.key
-    connector.updateNullableDate.execute(key) {}
-    assertNullableDateByKeyEquals(key, date.date)
-  }
+  fun dateNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(LocalDate(1771, 10, 28), "1771-10-28")
+      val dateTestDataArb =
+        Arb.dataConnect
+          .dateTestData()
+          .withEdgecases(defaultTestData)
+          .orNullableReference(nullProbability = 0.333)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = connector.dateNullableInsert3.execute(tag, testDatas)
+        val queryResult = connector.dateNullableGetAllByTagAndDefaultValue.execute(tag) {}
+        val matchingIds = testDatas.idsMatching(insertResult, defaultTestData.date)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
 
-  private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: String) {
-    val queryResult =
-      connector.getNonNullDateByKey
-        .withDataDeserializer(serializer<GetDateByKeyQueryStringData>())
-        .execute(key)
-    queryResult.data shouldBe GetDateByKeyQueryStringData(expected)
-  }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Invalid Date String Tests
+  //////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private suspend fun assertNonNullDateByKeyEquals(key: NonNullDateKey, expected: LocalDate) {
-    val queryResult = connector.getNonNullDateByKey.execute(key)
-    queryResult.data shouldBe
-      GetNonNullDateByKeyQuery.Data(GetNonNullDateByKeyQuery.Data.Value(expected))
-  }
+  @Test
+  fun dateNonNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            connector.dateNonNullableInsert.execute(testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
 
-  private suspend fun assertNullableDateByKeyHasNullInnerValue(key: NullableDateKey) {
-    val queryResult =
-      connector.getNullableDateByKey
-        .withDataDeserializer(serializer<GetDateByKeyQueryStringData>())
-        .execute(key)
-    queryResult.data shouldBe
-      GetDateByKeyQueryStringData(GetDateByKeyQueryStringData.DateStringValue(null))
-  }
+  @Test
+  fun dateNonNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            connector.dateNonNullableGetAllByTagAndValue.execute(
+              tag = tag,
+              value = testData.toDateScalarString()
+            )
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
 
-  private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: String) {
-    val queryResult =
-      connector.getNullableDateByKey
-        .withDataDeserializer(serializer<GetDateByKeyQueryStringData>())
-        .execute(key)
-    queryResult.data shouldBe GetDateByKeyQueryStringData(expected)
-  }
+  @Test
+  fun dateNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            connector.dateNullableInsert.execute(testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
 
-  private suspend fun assertNullableDateByKeyEquals(key: NullableDateKey, expected: LocalDate) {
-    val queryResult = connector.getNullableDateByKey.execute(key)
-    queryResult.data shouldBe
-      GetNullableDateByKeyQuery.Data(GetNullableDateByKeyQuery.Data.Value(expected))
-  }
+  @Test
+  fun dateNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            connector.dateNullableGetAllByTagAndValue.execute(
+              tag = tag,
+              value = testData.toDateScalarString()
+            )
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
 
-  /**
-   * A `Data` type that can be used in place of [GetNonNullDateByKeyQuery.Data] that types the value
-   * as a [String] instead of a [LocalDate], allowing verification of the data sent over the wire
-   * without possible confounding from date deserialization.
-   */
-  @Serializable
-  private data class GetDateByKeyQueryStringData(val value: DateStringValue?) {
-    constructor(value: String) : this(DateStringValue(value))
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Helper methods and classes.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
 
-    @Serializable data class DateStringValue(val value: String?)
+  @Serializable
+  private data class StringItemData(val item: Item?) {
+    @Serializable data class Item(val value: String?)
   }
 
-  /**
-   * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that
-   * types the value as a [String] instead of a [LocalDate], allowing verification of the data sent
-   * over the wire without possible confounding from date serialization.
-   */
-  @Serializable private data class InsertDateStringVariables(val value: String?)
+  @Serializable private data class NullValueVariables(val value: Nothing?)
+
+  @Serializable private data class StringValueVariables(val value: String?)
+
+  @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String?)
 
-  /**
-   * A `Variables` type that can be used in place of [InsertNonNullDateMutation.Variables] that
-   * types the value as a [Int] instead of a [LocalDate], allowing verification that the server
-   * fails with an expected error (rather than crashing, for example).
-   */
-  @Serializable private data class InsertDateIntVariables(val value: Int)
+  @Serializable private data class TagAndNullValueVariables(val tag: String, val value: Nothing?)
+
+  private suspend fun DemoConnector.requestTime(): LocalDate {
+    val insertResult = exprValuesInsert.execute()
+    val queryResult = exprValuesGetByKey.execute(insertResult.data.key)
+    return withClue("exprValuesGetByKey queryResult.data.item") {
+        queryResult.data.item.shouldNotBeNull()
+      }
+      .requestTimeAsDate
+  }
 
   private companion object {
 
-    suspend fun <Data> GeneratedMutation<*, Data, *>.executeWithStringVariables(value: String?) =
-      withVariablesSerializer(serializer<InsertDateStringVariables>())
-        .ref(InsertDateStringVariables(value))
+    val propTestConfig =
+      PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5))
+
+    suspend fun DateNullableInsert3Mutation.execute(
+      tag: String,
+      testDatas: ThreeDateTestDatas,
+    ): MutationResult<DateNullableInsert3Mutation.Data, DateNullableInsert3Mutation.Variables> =
+      execute(tag = tag) {
+        value1 = testDatas.testData1?.date
+        value2 = testDatas.testData2?.date
+        value3 = testDatas.testData3?.date
+      }
+
+    suspend fun DateNonNullableInsert3Mutation.execute(
+      tag: String,
+      testDatas: ThreeDateTestDatas,
+    ): MutationResult<
+      DateNonNullableInsert3Mutation.Data, DateNonNullableInsert3Mutation.Variables
+    > =
+      execute(
+        tag = tag,
+        value1 = testDatas.testData1!!.date,
+        value2 = testDatas.testData2!!.date,
+        value3 = testDatas.testData3!!.date,
+      )
+
+    suspend fun DateNonNullableGetByKeyQuery.executeWithStringData(
+      key: DateNonNullableKey
+    ): String? = withDataDeserializer(serializer<StringItemData>()).execute(key).data.item?.value
+
+    suspend fun DateNullableGetByKeyQuery.executeWithStringData(key: DateNullableKey): String? =
+      withDataDeserializer(serializer<StringItemData>()).execute(key).data.item?.value
+
+    suspend fun DateNonNullableInsertMutation.execute(
+      date: String
+    ): MutationResult<DateNonNullableInsertMutation.Data, StringValueVariables> =
+      withVariablesSerializer(serializer<StringValueVariables>())
+        .ref(StringValueVariables(date))
         .execute()
 
-    suspend fun <Data> GeneratedMutation<*, Data, *>.executeWithIntVariables(value: Int) =
-      withVariablesSerializer(serializer<InsertDateIntVariables>())
-        .ref(InsertDateIntVariables(value))
+    suspend fun DateNullableInsertMutation.execute(
+      date: String?
+    ): MutationResult<DateNullableInsertMutation.Data, StringValueVariables> =
+      withVariablesSerializer(serializer<StringValueVariables>())
+        .ref(StringValueVariables(date))
         .execute()
 
-    suspend fun <Data> GeneratedQuery<*, Data, GetNonNullDateByKeyQuery.Variables>.execute(
-      key: NonNullDateKey
-    ) = ref(GetNonNullDateByKeyQuery.Variables(key)).execute()
-
-    suspend fun <Data> GeneratedQuery<*, Data, GetNullableDateByKeyQuery.Variables>.execute(
-      key: NullableDateKey
-    ) = ref(GetNullableDateByKeyQuery.Variables(key)).execute()
-
-    val invalidDates =
-      listOf(
-        // Partial dates
-        "2",
-        "20",
-        "202",
-        "2024",
-        "2024-",
-        "2024-0",
-        "2024-01",
-        "2024-01-",
-        "2024-01-0",
-        "2024-01-04T",
-
-        // Missing components
-        "",
-        "2024-",
-        "-05-17",
-        "2024-05",
-        "2024--17",
-        "-05-",
-
-        // Invalid year
-        "2-05-17",
-        "20-05-17",
-        "202-05-17",
-        "20245-05-17",
-        "02024-05-17",
-        "ABCD-05-17",
-        "-123-05-17",
-
-        // Invalid month
-        "2024-1-17",
-        "2024-012-17",
-        "2024-123-17",
-        "2024-00-17",
-        "2024-13-17",
-        "2024-M-17",
-        "2024-MA-17",
-
-        // Invalid day
-        "2024-05-1",
-        "2024-05-123",
-        "2024-05-012",
-        "2024-05-00",
-        "2024-05-32",
-        "2024-05-A",
-        "2024-05-AB",
-        "2024-05-ABC",
-
-        // Out-of-range Values
-        "0000-01-01",
-        "2024-00-22",
-        "2024-13-22",
-        "2024-11-00",
-        "2024-01-32",
-        "2025-02-29",
-        "2024-02-30",
-        "2024-03-32",
-        "2024-04-31",
-        "2024-05-32",
-        "2024-06-31",
-        "2024-07-32",
-        "2024-08-32",
-        "2024-09-31",
-        "2024-10-32",
-        "2024-11-31",
-        "2024-12-32",
-      )
+    suspend fun DateNonNullableInsertMutation.execute(
+      date: Nothing?
+    ): MutationResult<DateNonNullableInsertMutation.Data, NullValueVariables> =
+      withVariablesSerializer(serializer<NullValueVariables>())
+        .ref(NullValueVariables(date))
+        .execute()
+
+    suspend fun DateNonNullableGetAllByTagAndValueQuery.execute(
+      tag: String,
+      value: String,
+    ): QueryResult<DateNonNullableGetAllByTagAndValueQuery.Data, TagAndStringValueVariables> =
+      withVariablesSerializer(serializer<TagAndStringValueVariables>())
+        .ref(TagAndStringValueVariables(tag = tag, value = value))
+        .execute()
+
+    suspend fun DateNonNullableGetAllByTagAndValueQuery.execute(
+      tag: String,
+      value: Nothing?,
+    ): QueryResult<DateNonNullableGetAllByTagAndValueQuery.Data, TagAndNullValueVariables> =
+      withVariablesSerializer(serializer<TagAndNullValueVariables>())
+        .ref(TagAndNullValueVariables(tag = tag, value = value))
+        .execute()
+
+    suspend fun DateNullableGetAllByTagAndValueQuery.execute(
+      tag: String,
+      value: String?,
+    ): QueryResult<DateNullableGetAllByTagAndValueQuery.Data, TagAndStringValueVariables> =
+      withVariablesSerializer(serializer<TagAndStringValueVariables>())
+        .ref(TagAndStringValueVariables(tag = tag, value = value))
+        .execute()
+
+    @JvmName("idsMatching_DateNonNullable")
+    fun ThreeDateTestDatas.idsMatching(
+      result: MutationResult<DateNonNullableInsert3Mutation.Data, *>,
+      localDate: LocalDate?,
+    ): List<UUID> = idsMatching(result.data, localDate)
+
+    @JvmName("idsMatching_DateNonNullable")
+    fun ThreeDateTestDatas.idsMatching(
+      data: DateNonNullableInsert3Mutation.Data,
+      localDate: LocalDate?,
+    ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) }
+
+    @JvmName("idsMatchingSelected_DateNonNullable")
+    fun ThreeDateTestDatas.idsMatchingSelected(
+      result: MutationResult<DateNonNullableInsert3Mutation.Data, *>
+    ): List<UUID> = idsMatchingSelected(result.data)
+
+    @JvmName("idsMatchingSelected_DateNonNullable")
+    fun ThreeDateTestDatas.idsMatchingSelected(
+      data: DateNonNullableInsert3Mutation.Data
+    ): List<UUID> = idsMatchingSelected { data.uuidFromItemNumber(it) }
+
+    fun DateNonNullableInsert3Mutation.Data.uuidFromItemNumber(itemNumber: ItemNumber): UUID =
+      when (itemNumber) {
+        ItemNumber.ONE -> key1
+        ItemNumber.TWO -> key2
+        ItemNumber.THREE -> key3
+      }.id
+
+    @JvmName("idsMatching_DateNullable")
+    fun ThreeDateTestDatas.idsMatching(
+      result: MutationResult<DateNullableInsert3Mutation.Data, *>,
+      localDate: LocalDate?,
+    ): List<UUID> = idsMatching(result.data, localDate)
+
+    @JvmName("idsMatching_DateNullable")
+    fun ThreeDateTestDatas.idsMatching(
+      data: DateNullableInsert3Mutation.Data,
+      localDate: LocalDate?,
+    ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) }
+
+    @JvmName("idsMatchingSelected_DateNullable")
+    fun ThreeDateTestDatas.idsMatchingSelected(
+      result: MutationResult<DateNullableInsert3Mutation.Data, *>
+    ): List<UUID> = idsMatchingSelected(result.data)
+
+    @JvmName("idsMatchingSelected_DateNullable")
+    fun ThreeDateTestDatas.idsMatchingSelected(data: DateNullableInsert3Mutation.Data): List<UUID> =
+      idsMatchingSelected {
+        data.uuidFromItemNumber(it)
+      }
+
+    fun DateNullableInsert3Mutation.Data.uuidFromItemNumber(itemNumber: ItemNumber): UUID =
+      when (itemNumber) {
+        ItemNumber.ONE -> key1
+        ItemNumber.TWO -> key2
+        ItemNumber.THREE -> key3
+      }.id
+
+    suspend fun <Data> GeneratedQuery<*, Data, DateNonNullableGetByKeyQuery.Variables>.execute(
+      key: DateNonNullableKey
+    ) = ref(DateNonNullableGetByKeyQuery.Variables(key)).execute()
+
+    suspend fun <Data> GeneratedQuery<*, Data, DateNullableGetByKeyQuery.Variables>.execute(
+      key: DateNullableKey
+    ) = ref(DateNullableGetByKeyQuery.Variables(key)).execute()
   }
 }
diff --git a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql
index 2f22ed6210a..907f95778e4 100644
--- a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql
+++ b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql
@@ -1306,58 +1306,6 @@ query DateNullableWithDefaults_GetByKey($key: DateNullableWithDefaults_Key!) @au
   }
 }
 
-mutation InsertNonNullDate($value: Date!) @auth(level: PUBLIC) {
-  nonNullDate_insert(data: { value: $value })
-}
-
-mutation UpdateNonNullDate($key: NonNullDate_Key!, $value: Date) @auth(level: PUBLIC) {
-  nonNullDate_update(key: $key, data: { value: $value })
-}
-
-query GetNonNullDateByKey($key: NonNullDate_Key!) @auth(level: PUBLIC) {
-  value: nonNullDate(key: $key) { value }
-}
-
-mutation InsertNonNullDatesWithDefaults($value: Date! = "6904-11-30") @auth(level: PUBLIC) {
-  nonNullDatesWithDefaults_insert(data: { valueWithVariableDefault: $value })
-}
-
-query GetNonNullDatesWithDefaultsByKey($key: NonNullDatesWithDefaults_Key!) @auth(level: PUBLIC) {
-  nonNullDatesWithDefaults(key: $key) {
-    valueWithVariableDefault
-    valueWithSchemaDefault
-    epoch
-    requestTime1
-    requestTime2
-  }
-}
-
-mutation InsertNullableDate($value: Date) @auth(level: PUBLIC) {
-  nullableDate_insert(data: { value: $value })
-}
-
-mutation UpdateNullableDate($key: NullableDate_Key!, $value: Date) @auth(level: PUBLIC) {
-  nullableDate_update(key: $key, data: { value: $value })
-}
-
-query GetNullableDateByKey($key: NullableDate_Key!) @auth(level: PUBLIC) {
-  value: nullableDate(key: $key) { value }
-}
-
-mutation InsertNullableDatesWithDefaults($value: Date = "8113-02-09") @auth(level: PUBLIC) {
-  nullableDatesWithDefaults_insert(data: { valueWithVariableDefault: $value })
-}
-
-query GetNullableDatesWithDefaultsByKey($key: NullableDatesWithDefaults_Key!) @auth(level: PUBLIC) {
-  nullableDatesWithDefaults(key: $key) {
-    valueWithVariableDefault
-    valueWithSchemaDefault
-    epoch
-    requestTime1
-    requestTime2
-  }
-}
-
 ###############################################################################
 # Operations for table: NonNullTimestamp
 ###############################################################################
diff --git a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql
index 49ba107a134..4bad02dc62f 100644
--- a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql
+++ b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql
@@ -287,30 +287,6 @@ type DateNullableWithDefaults @table {
   requestTime2: Date @default(expr: "request.time")
 }
 
-type NonNullDate @table {
-  value: Date!
-}
-
-type NullableDate @table {
-  value: Date
-}
-
-type NonNullDatesWithDefaults @table {
-  valueWithVariableDefault: Date!
-  valueWithSchemaDefault: Date! @default(value: "2112-01-31")
-  epoch: Date! @default(sql: "'epoch'::date")
-  requestTime1: Date! @default(expr: "request.time")
-  requestTime2: Date! @default(expr: "request.time")
-}
-
-type NullableDatesWithDefaults @table {
-  valueWithVariableDefault: Date
-  valueWithSchemaDefault: Date @default(value: "1921-12-02")
-  epoch: Date @default(sql: "'epoch'::date")
-  requestTime1: Date @default(expr: "request.time")
-  requestTime2: Date @default(expr: "request.time")
-}
-
 type NonNullTimestamp @table @index(fields: ["tag"]) {
   value: Timestamp!
   tag: String
diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
index 6ed685f0476..1535b12a167 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
@@ -139,6 +139,13 @@ data class ThreeDateTestDatas(
       else -> throw Exception("internal error: unknown index: $index")
     }
 
+  val numMatchingSelected: Int = run {
+    val v1 = if (testData1 == selected) 1 else 0
+    val v2 = if (testData2 == selected) 1 else 0
+    val v3 = if (testData3 == selected) 1 else 0
+    v1 + v2 + v3
+  }
+
   fun idsMatchingSelected(getter: (ItemNumber) -> UUID): List<UUID> =
     idsMatching(selected?.date, getter)
 

From 75e552051147f72cd0ee8f238beff88c0b3d2e18 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Thu, 14 Nov 2024 05:57:17 +0000
Subject: [PATCH 11/13] JavaTimeLocalDateSerializer.kt and
 KotlinxDatetimeLocalDateSerializer.kt added

---
 .../JavaTimeLocalDateIntegrationTest.kt       | 720 ++++++++++++++++++
 ...KotlinxDatetimeLocalDateIntegrationTest.kt | 720 ++++++++++++++++++
 .../dataconnect/LocalDateIntegrationTest.kt   |  12 +
 .../JavaTimeLocalDateSerializer.kt            |  51 ++
 .../KotlinxDatetimeLocalDateSerializer.kt     |  52 ++
 .../serializers/LocalDateSerializer.kt        |   3 +
 .../JavaTimeLocalDateSerializerUnitTest.kt    | 261 +++++++
 ...linxDatetimeLocalDateSerializerUnitTest.kt | 264 +++++++
 .../LocalDateSerializerUnitTest.kt            |  12 +
 .../firebase/dataconnect/testutil/JavaTime.kt |  23 +-
 .../testutil/property/arbitrary/dates.kt      |  28 +-
 11 files changed, 2138 insertions(+), 8 deletions(-)
 create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
 create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
 create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt
 create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt
 create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
 create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt

diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
new file mode 100644
index 00000000000..e99238cf00e
--- /dev/null
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
@@ -0,0 +1,720 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.
+ */
+
+@file:OptIn(ExperimentalKotest::class)
+@file:UseSerializers(UUIDSerializer::class, JavaTimeLocalDateSerializer::class)
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+package com.google.firebase.dataconnect
+
+import com.google.firebase.dataconnect.serializers.JavaTimeLocalDateSerializer
+import com.google.firebase.dataconnect.serializers.UUIDSerializer
+import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
+import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber
+import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
+import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString
+import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate
+import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData
+import com.google.firebase.dataconnect.testutil.requestTimeAsDate
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
+import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate
+import io.kotest.assertions.assertSoftly
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.assertions.withClue
+import io.kotest.common.ExperimentalKotest
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldBeIn
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
+import io.kotest.matchers.nulls.shouldBeNull
+import io.kotest.matchers.nulls.shouldNotBeNull
+import io.kotest.matchers.shouldBe
+import io.kotest.property.Arb
+import io.kotest.property.EdgeConfig
+import io.kotest.property.PropTestConfig
+import io.kotest.property.arbitrary.map
+import io.kotest.property.arbitrary.next
+import io.kotest.property.arbitrary.withEdgecases
+import io.kotest.property.checkAll
+import java.util.UUID
+import kotlin.time.Duration.Companion.minutes
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.serializer
+import org.junit.Test
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+class JavaTimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() {
+
+  private val dataConnect: FirebaseDataConnect by lazy {
+    val connectorConfig = testConnectorConfig.copy(connector = "demo")
+    dataConnectFactory.newInstance(connectorConfig)
+  }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for inserting into and querying this table:
+  // type DateNonNullable @table { value: Date!, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.localDate().map { it.toJavaLocalDate() }) { localDate
+        ->
+        val insertResult = nonNullableDate.insert(localDate)
+        val queryResult = nonNullableDate.getByKey(insertResult.data.key)
+        queryResult.data.item?.value shouldBe localDate
+      }
+    }
+
+  @Test
+  fun dateNonNullable_QueryVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nonNullableDate.insert3(tag, testDatas)
+        val queryResult =
+          nonNullableDate.getAllByTagAndValue(tag, testDatas.selected!!.date.toJavaLocalDate())
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNonNullable_MutationNullVariableShouldThrow() = runTest {
+    val exception = shouldThrow<DataConnectException> { nonNullableDate.insert(null) }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryNullVariableShouldThrow() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val exception =
+      shouldThrow<DataConnectException> { nonNullableDate.getAllByTagAndValue(tag, null) }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    val insertResult = nonNullableDate.insert3(tag, testDatas)
+    val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag)
+    queryResult.data.items
+      .map { it.id }
+      .shouldContainExactlyInAnyOrder(
+        insertResult.data.key1.id,
+        insertResult.data.key2.id,
+        insertResult.data.key3.id
+      )
+  }
+
+  @Test
+  fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    nonNullableDate.insert3(tag, testDatas)
+    val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag, null)
+    queryResult.data.items.shouldBeEmpty()
+  }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for inserting into and querying this table:
+  // type DateNullable @table { value: Date, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNullable_MutationVariable() =
+    runTest(timeout = 1.minutes) {
+      val localDates =
+        Arb.dataConnect
+          .localDate()
+          .map { it.toJavaLocalDate() }
+          .orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates) { localDate ->
+        val insertResult = nullableDate.insert(localDate.ref)
+        val queryResult = nullableDate.getByKey(insertResult.data.key)
+        queryResult.data.item?.value shouldBe localDate.ref
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult =
+          nullableDate.getAllByTagAndValue(tag, testDatas.selected?.date?.toJavaLocalDate())
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryOmittedVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult = nullableDate.getAllByTagAndMaybeValue(tag)
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id
+          )
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for default `Date` variable values.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationVariableDefaults() = runTest {
+    val insertResult = nonNullableDate.insertWithDefaults()
+    val queryResult = nonNullableDate.getInsertedWithDefaultsByKey(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+
+    assertSoftly {
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe java.time.LocalDate.of(6904, 11, 30)
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe java.time.LocalDate.of(2112, 1, 31)
+        }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toJavaLocalDate() }
+        withClue("requestTime1") { item.requestTime1.shouldNotBeNull() }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
+      }
+    }
+
+    withClue("requestTime validation") {
+      val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(java.time.LocalDate.of(2692, 5, 21), "2692-05-21")
+      val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threeNonNullDatesTestData(localDateArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = nonNullableDate.insert3(tag, testDatas)
+        val queryResult = nonNullableDate.getAllByTagAndDefaultValue(tag)
+        val matchingIds =
+          testDatas.idsMatching(insertResult, defaultTestData.date.toJavaLocalDate())
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_MutationVariableDefaults() = runTest {
+    val insertResult = nullableDate.insertWithDefaults()
+    val queryResult = nullableDate.getInsertedWithDefaultsByKey(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+
+    assertSoftly {
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe java.time.LocalDate.of(8113, 2, 9)
+        }
+        withClue("valueWithVariableNullDefault") {
+          item.valueWithVariableNullDefault.shouldBeNull()
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe java.time.LocalDate.of(1921, 12, 2)
+        }
+        withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() }
+        withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toJavaLocalDate() }
+        withClue("requestTime1") { item.requestTime1.shouldNotBeNull() }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
+      }
+    }
+
+    withClue("requestTime validation") {
+      val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
+  }
+
+  @Test
+  fun dateNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(java.time.LocalDate.of(1771, 10, 28), "1771-10-28")
+      val dateTestDataArb =
+        Arb.dataConnect
+          .dateTestData()
+          .withEdgecases(defaultTestData)
+          .orNullableReference(nullProbability = 0.333)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult = nullableDate.getAllByTagAndDefaultValue(tag)
+        val matchingIds =
+          testDatas.idsMatching(insertResult, defaultTestData.date.toJavaLocalDate())
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Invalid Date String Tests
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nonNullableDate.insert(testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNonNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nonNullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> { nullableDate.insert(testData.toDateScalarString()) }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Helper methods and classes.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Serializable private data class SingleKeyVariables(val key: Key)
+
+  @Serializable private data class SingleKeyData(val key: Key)
+
+  @Serializable
+  private data class MultipleKeysData(val items: List<Item>) {
+    @Serializable data class Item(val id: UUID)
+  }
+
+  @Serializable private data class ThreeKeysData(val key1: Key, val key2: Key, val key3: Key)
+
+  @Serializable private data class InsertVariables(val value: java.time.LocalDate?)
+
+  @Serializable private data class InsertStringVariables(val value: String)
+
+  @Serializable
+  private data class Insert3Variables(
+    val tag: String,
+    val value1: java.time.LocalDate?,
+    val value2: java.time.LocalDate?,
+    val value3: java.time.LocalDate?,
+  )
+
+  @Serializable private data class TagVariables(val tag: String)
+
+  @Serializable
+  private data class TagAndValueVariables(val tag: String, val value: java.time.LocalDate?)
+
+  @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String)
+
+  @Serializable
+  private data class QueryData(val item: Item?) {
+    @Serializable data class Item(val value: java.time.LocalDate?)
+  }
+
+  @Serializable
+  private data class GetInsertedWithDefaultsByKeyQueryData(val item: Item?) {
+    @Serializable
+    data class Item(
+      val valueWithVariableDefault: java.time.LocalDate,
+      val valueWithVariableNullDefault: java.time.LocalDate?,
+      val valueWithSchemaDefault: java.time.LocalDate,
+      val valueWithSchemaNullDefault: java.time.LocalDate?,
+      val valueWithNoDefault: java.time.LocalDate?,
+      val epoch: java.time.LocalDate?,
+      val requestTime1: java.time.LocalDate?,
+      val requestTime2: java.time.LocalDate?,
+    )
+  }
+
+  @Serializable private data class Key(val id: UUID)
+
+  /** Operations for querying and mutating the table that stores non-nullable Date scalar values. */
+  private val nonNullableDate =
+    Operations(
+      getByKeyQueryName = "DateNonNullable_GetByKey",
+      getAllByTagAndValueQueryName = "DateNonNullable_GetAllByTagAndValue",
+      getAllByTagAndMaybeValueQueryName = "DateNonNullable_GetAllByTagAndMaybeValue",
+      getAllByTagAndDefaultValueQueryName = "DateNonNullable_GetAllByTagAndDefaultValue",
+      insertMutationName = "DateNonNullable_Insert",
+      insert3MutationName = "DateNonNullable_Insert3",
+      insertWithDefaultsMutationName = "DateNonNullableWithDefaults_Insert",
+      getInsertedWithDefaultsByKeyQueryName = "DateNonNullableWithDefaults_GetByKey",
+    )
+
+  /** Operations for querying and mutating the table that stores nullable Date scalar values. */
+  private val nullableDate =
+    Operations(
+      getByKeyQueryName = "DateNullable_GetByKey",
+      getAllByTagAndValueQueryName = "DateNullable_GetAllByTagAndValue",
+      getAllByTagAndMaybeValueQueryName = "DateNullable_GetAllByTagAndValue",
+      getAllByTagAndDefaultValueQueryName = "DateNullable_GetAllByTagAndDefaultValue",
+      insertMutationName = "DateNullable_Insert",
+      insert3MutationName = "DateNullable_Insert3",
+      insertWithDefaultsMutationName = "DateNullableWithDefaults_Insert",
+      getInsertedWithDefaultsByKeyQueryName = "DateNullableWithDefaults_GetByKey",
+    )
+
+  private inner class Operations(
+    getByKeyQueryName: String,
+    getAllByTagAndValueQueryName: String,
+    getAllByTagAndMaybeValueQueryName: String,
+    getAllByTagAndDefaultValueQueryName: String,
+    insertMutationName: String,
+    insert3MutationName: String,
+    insertWithDefaultsMutationName: String,
+    getInsertedWithDefaultsByKeyQueryName: String,
+  ) {
+
+    suspend fun insert(
+      localDate: java.time.LocalDate?
+    ): MutationResult<SingleKeyData, InsertVariables> = insert(InsertVariables(localDate))
+
+    suspend fun insert(variables: InsertVariables): MutationResult<SingleKeyData, InsertVariables> =
+      mutations.insert(variables).execute()
+
+    suspend fun insert(localDate: String): MutationResult<SingleKeyData, InsertStringVariables> =
+      insert(InsertStringVariables(localDate))
+
+    suspend fun insert(
+      variables: InsertStringVariables
+    ): MutationResult<SingleKeyData, InsertStringVariables> = mutations.insert(variables).execute()
+
+    suspend fun insert3(
+      tag: String,
+      testDatas: ThreeDateTestDatas,
+    ): MutationResult<ThreeKeysData, Insert3Variables> =
+      insert3(
+        tag = tag,
+        value1 = testDatas.testData1?.date?.toJavaLocalDate(),
+        value2 = testDatas.testData2?.date?.toJavaLocalDate(),
+        value3 = testDatas.testData3?.date?.toJavaLocalDate()
+      )
+
+    suspend fun insert3(
+      tag: String,
+      value1: java.time.LocalDate?,
+      value2: java.time.LocalDate?,
+      value3: java.time.LocalDate?,
+    ): MutationResult<ThreeKeysData, Insert3Variables> =
+      insert3(Insert3Variables(tag = tag, value1 = value1, value2 = value2, value3 = value3))
+
+    suspend fun insert3(
+      variables: Insert3Variables
+    ): MutationResult<ThreeKeysData, Insert3Variables> = mutations.insert3(variables).execute()
+
+    suspend fun getByKey(key: Key): QueryResult<QueryData, SingleKeyVariables> =
+      getByKey(SingleKeyVariables(key))
+
+    suspend fun getByKey(
+      variables: SingleKeyVariables
+    ): QueryResult<QueryData, SingleKeyVariables> = queries.getByKey(variables).execute()
+
+    suspend fun getAllByTagAndValue(
+      tag: String,
+      value: java.time.LocalDate?
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      getAllByTagAndValue(TagAndValueVariables(tag, value))
+
+    suspend fun getAllByTagAndValue(
+      variables: TagAndValueVariables
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      queries.getAllByTagAndValue(variables).execute()
+
+    suspend fun getAllByTagAndValue(
+      tag: String,
+      value: String
+    ): QueryResult<MultipleKeysData, TagAndStringValueVariables> =
+      getAllByTagAndValue(TagAndStringValueVariables(tag, value))
+
+    suspend fun getAllByTagAndValue(
+      variables: TagAndStringValueVariables
+    ): QueryResult<MultipleKeysData, TagAndStringValueVariables> =
+      queries.getAllByTagAndValue(variables).execute()
+
+    suspend fun getAllByTagAndMaybeValue(
+      tag: String,
+    ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndMaybeValue(TagVariables(tag))
+
+    suspend fun getAllByTagAndMaybeValue(
+      variables: TagVariables
+    ): QueryResult<MultipleKeysData, TagVariables> =
+      queries.getAllByTagAndMaybeValue(variables).execute()
+
+    suspend fun getAllByTagAndMaybeValue(
+      tag: String,
+      value: Nothing?,
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      getAllByTagAndMaybeValue(TagAndValueVariables(tag, value))
+
+    suspend fun getAllByTagAndMaybeValue(
+      variables: TagAndValueVariables
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      queries.getAllByTagAndMaybeValue(variables).execute()
+
+    suspend fun getAllByTagAndDefaultValue(
+      tag: String
+    ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndDefaultValue(TagVariables(tag))
+
+    suspend fun getAllByTagAndDefaultValue(
+      variables: TagVariables
+    ): QueryResult<MultipleKeysData, TagVariables> =
+      queries.getAllByTagAndDefaultValue(variables).execute()
+
+    suspend fun insertWithDefaults(): MutationResult<SingleKeyData, Unit> =
+      mutations.insertWithDefaults().execute()
+
+    suspend fun getInsertedWithDefaultsByKey(
+      key: Key
+    ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+      getInsertedWithDefaultsByKey(SingleKeyVariables(key))
+
+    suspend fun getInsertedWithDefaultsByKey(
+      variables: SingleKeyVariables
+    ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+      queries.getInsertedWithDefaultsByKey(variables).execute()
+
+    private val queries =
+      object {
+        fun getByKey(variables: SingleKeyVariables): QueryRef<QueryData, SingleKeyVariables> =
+          dataConnect.query(
+            getByKeyQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        inline fun <reified Variables> getAllByTagAndValue(
+          variables: Variables
+        ): QueryRef<MultipleKeysData, Variables> =
+          dataConnect.query(
+            getAllByTagAndValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndMaybeValue(
+          variables: TagVariables
+        ): QueryRef<MultipleKeysData, TagVariables> =
+          dataConnect.query(
+            getAllByTagAndMaybeValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndMaybeValue(
+          variables: TagAndValueVariables
+        ): QueryRef<MultipleKeysData, TagAndValueVariables> =
+          dataConnect.query(
+            getAllByTagAndMaybeValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndDefaultValue(
+          variables: TagVariables
+        ): QueryRef<MultipleKeysData, TagVariables> =
+          dataConnect.query(
+            getAllByTagAndDefaultValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getInsertedWithDefaultsByKey(
+          variables: SingleKeyVariables
+        ): QueryRef<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+          dataConnect.query(
+            getInsertedWithDefaultsByKeyQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+      }
+
+    private val mutations =
+      object {
+        inline fun <reified Variables> insert(
+          variables: Variables
+        ): MutationRef<SingleKeyData, Variables> =
+          dataConnect.mutation(
+            insertMutationName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun insert3(variables: Insert3Variables): MutationRef<ThreeKeysData, Insert3Variables> =
+          dataConnect.mutation(
+            insert3MutationName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun insertWithDefaults(): MutationRef<SingleKeyData, Unit> =
+          dataConnect.mutation(
+            insertWithDefaultsMutationName,
+            Unit,
+            serializer(),
+            serializer(),
+          )
+      }
+  }
+
+  private companion object {
+    val propTestConfig =
+      PropTestConfig(
+        iterations = 20,
+        edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5),
+      )
+
+    fun ThreeDateTestDatas.idsMatchingSelected(
+      result: MutationResult<ThreeKeysData, *>
+    ): List<UUID> = idsMatchingSelected(result.data)
+
+    fun ThreeDateTestDatas.idsMatchingSelected(data: ThreeKeysData): List<UUID> =
+      idsMatchingSelected {
+        data.uuidFromItemNumber(it)
+      }
+
+    fun ThreeDateTestDatas.idsMatching(
+      result: MutationResult<ThreeKeysData, *>,
+      localDate: java.time.LocalDate?,
+    ): List<UUID> = idsMatching(result.data, localDate)
+
+    fun ThreeDateTestDatas.idsMatching(
+      data: ThreeKeysData,
+      localDate: java.time.LocalDate?,
+    ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) }
+
+    fun ThreeKeysData.uuidFromItemNumber(itemNumber: ItemNumber): UUID =
+      when (itemNumber) {
+        ItemNumber.ONE -> key1
+        ItemNumber.TWO -> key2
+        ItemNumber.THREE -> key3
+      }.id
+  }
+}
diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
new file mode 100644
index 00000000000..a4ea009a7c7
--- /dev/null
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
@@ -0,0 +1,720 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.
+ */
+
+@file:OptIn(ExperimentalKotest::class)
+@file:UseSerializers(UUIDSerializer::class, KotlinxDatetimeLocalDateSerializer::class)
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+package com.google.firebase.dataconnect
+
+import com.google.firebase.dataconnect.serializers.KotlinxDatetimeLocalDateSerializer
+import com.google.firebase.dataconnect.serializers.UUIDSerializer
+import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
+import com.google.firebase.dataconnect.testutil.property.arbitrary.DateTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.EdgeCases
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas
+import com.google.firebase.dataconnect.testutil.property.arbitrary.ThreeDateTestDatas.ItemNumber
+import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
+import com.google.firebase.dataconnect.testutil.property.arbitrary.dateTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.invalidDateScalarString
+import com.google.firebase.dataconnect.testutil.property.arbitrary.localDate
+import com.google.firebase.dataconnect.testutil.property.arbitrary.orNullableReference
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threeNonNullDatesTestData
+import com.google.firebase.dataconnect.testutil.property.arbitrary.threePossiblyNullDatesTestData
+import com.google.firebase.dataconnect.testutil.requestTimeAsDate
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText
+import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
+import com.google.firebase.dataconnect.testutil.toTheeTenAbpJavaLocalDate
+import io.kotest.assertions.assertSoftly
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.assertions.withClue
+import io.kotest.common.ExperimentalKotest
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldBeIn
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
+import io.kotest.matchers.nulls.shouldBeNull
+import io.kotest.matchers.nulls.shouldNotBeNull
+import io.kotest.matchers.shouldBe
+import io.kotest.property.Arb
+import io.kotest.property.EdgeConfig
+import io.kotest.property.PropTestConfig
+import io.kotest.property.arbitrary.map
+import io.kotest.property.arbitrary.next
+import io.kotest.property.arbitrary.withEdgecases
+import io.kotest.property.checkAll
+import java.util.UUID
+import kotlin.time.Duration.Companion.minutes
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.serializer
+import org.junit.Test
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+class KotlinxDatetimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() {
+
+  private val dataConnect: FirebaseDataConnect by lazy {
+    val connectorConfig = testConnectorConfig.copy(connector = "demo")
+    dataConnectFactory.newInstance(connectorConfig)
+  }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for inserting into and querying this table:
+  // type DateNonNullable @table { value: Date!, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig, Arb.dataConnect.localDate().map { it.toKotlinxLocalDate() }) {
+        localDate ->
+        val insertResult = nonNullableDate.insert(localDate)
+        val queryResult = nonNullableDate.getByKey(insertResult.data.key)
+        queryResult.data.item?.value shouldBe localDate
+      }
+    }
+
+  @Test
+  fun dateNonNullable_QueryVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threeNonNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nonNullableDate.insert3(tag, testDatas)
+        val queryResult =
+          nonNullableDate.getAllByTagAndValue(tag, testDatas.selected!!.date.toKotlinxLocalDate())
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNonNullable_MutationNullVariableShouldThrow() = runTest {
+    val exception = shouldThrow<DataConnectException> { nonNullableDate.insert(null) }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryNullVariableShouldThrow() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val exception =
+      shouldThrow<DataConnectException> { nonNullableDate.getAllByTagAndValue(tag, null) }
+    assertSoftly {
+      exception.message shouldContainWithNonAbuttingText "\$value"
+      exception.message shouldContainWithNonAbuttingText "is null"
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryOmittedVariableShouldMatchAll() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    val insertResult = nonNullableDate.insert3(tag, testDatas)
+    val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag)
+    queryResult.data.items
+      .map { it.id }
+      .shouldContainExactlyInAnyOrder(
+        insertResult.data.key1.id,
+        insertResult.data.key2.id,
+        insertResult.data.key3.id
+      )
+  }
+
+  @Test
+  fun dateNonNullable_QueryNullVariableShouldMatchNone() = runTest {
+    val tag = Arb.dataConnect.tag().next(rs)
+    val testDatas = Arb.dataConnect.threeNonNullDatesTestData().next(rs)
+    nonNullableDate.insert3(tag, testDatas)
+    val queryResult = nonNullableDate.getAllByTagAndMaybeValue(tag, null)
+    queryResult.data.items.shouldBeEmpty()
+  }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for inserting into and querying this table:
+  // type DateNullable @table { value: Date, tag: String }
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNullable_MutationVariable() =
+    runTest(timeout = 1.minutes) {
+      val localDates =
+        Arb.dataConnect
+          .localDate()
+          .map { it.toKotlinxLocalDate() }
+          .orNullableReference(nullProbability = 0.2)
+      checkAll(propTestConfig, localDates) { localDate ->
+        val insertResult = nullableDate.insert(localDate.ref)
+        val queryResult = nullableDate.getByKey(insertResult.data.key)
+        queryResult.data.item?.value shouldBe localDate.ref
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult =
+          nullableDate.getAllByTagAndValue(tag, testDatas.selected?.date?.toKotlinxLocalDate())
+        val matchingIds = testDatas.idsMatchingSelected(insertResult)
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryOmittedVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.threePossiblyNullDatesTestData()
+      ) { tag, testDatas ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult = nullableDate.getAllByTagAndMaybeValue(tag)
+        queryResult.data.items
+          .map { it.id }
+          .shouldContainExactlyInAnyOrder(
+            insertResult.data.key1.id,
+            insertResult.data.key2.id,
+            insertResult.data.key3.id
+          )
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Tests for default `Date` variable values.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationVariableDefaults() = runTest {
+    val insertResult = nonNullableDate.insertWithDefaults()
+    val queryResult = nonNullableDate.getInsertedWithDefaultsByKey(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+
+    assertSoftly {
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe kotlinx.datetime.LocalDate(6904, 11, 30)
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe kotlinx.datetime.LocalDate(2112, 1, 31)
+        }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toKotlinxLocalDate() }
+        withClue("requestTime1") { item.requestTime1.shouldNotBeNull() }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
+      }
+    }
+
+    withClue("requestTime validation") {
+      val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
+  }
+
+  @Test
+  fun dateNonNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(kotlinx.datetime.LocalDate(2692, 5, 21), "2692-05-21")
+      val localDateArb = Arb.dataConnect.dateTestData().withEdgecases(defaultTestData)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threeNonNullDatesTestData(localDateArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = nonNullableDate.insert3(tag, testDatas)
+        val queryResult = nonNullableDate.getAllByTagAndDefaultValue(tag)
+        val matchingIds =
+          testDatas.idsMatching(insertResult, defaultTestData.date.toKotlinxLocalDate())
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  @Test
+  fun dateNullable_MutationVariableDefaults() = runTest {
+    val insertResult = nullableDate.insertWithDefaults()
+    val queryResult = nullableDate.getInsertedWithDefaultsByKey(insertResult.data.key)
+    val item = withClue("queryResult.data.item") { queryResult.data.item.shouldNotBeNull() }
+
+    assertSoftly {
+      withClue(item) {
+        withClue("valueWithVariableDefault") {
+          item.valueWithVariableDefault shouldBe kotlinx.datetime.LocalDate(8113, 2, 9)
+        }
+        withClue("valueWithVariableNullDefault") {
+          item.valueWithVariableNullDefault.shouldBeNull()
+        }
+        withClue("valueWithSchemaDefault") {
+          item.valueWithSchemaDefault shouldBe kotlinx.datetime.LocalDate(1921, 12, 2)
+        }
+        withClue("valueWithSchemaNullDefault") { item.valueWithSchemaNullDefault.shouldBeNull() }
+        withClue("valueWithNoDefault") { item.valueWithNoDefault.shouldBeNull() }
+        withClue("epoch") { item.epoch shouldBe EdgeCases.dates.epoch.date.toKotlinxLocalDate() }
+        withClue("requestTime1") { item.requestTime1.shouldNotBeNull() }
+        withClue("requestTime2") { item.requestTime2 shouldBe item.requestTime1 }
+      }
+    }
+
+    withClue("requestTime validation") {
+      val today = dataConnect.requestTimeAsDate().toTheeTenAbpJavaLocalDate()
+      val yesterday = today.minusDays(1)
+      val tomorrow = today.plusDays(1)
+      val requestTime = item.requestTime1!!.toTheeTenAbpJavaLocalDate()
+      requestTime.shouldBeIn(yesterday, today, tomorrow)
+    }
+  }
+
+  @Test
+  fun dateNullable_QueryVariableDefaults() =
+    runTest(timeout = 1.minutes) {
+      val defaultTestData = DateTestData(kotlinx.datetime.LocalDate(1771, 10, 28), "1771-10-28")
+      val dateTestDataArb =
+        Arb.dataConnect
+          .dateTestData()
+          .withEdgecases(defaultTestData)
+          .orNullableReference(nullProbability = 0.333)
+      checkAll(
+        propTestConfig,
+        Arb.dataConnect.threePossiblyNullDatesTestData(dateTestDataArb),
+        Arb.dataConnect.tag()
+      ) { testDatas, tag ->
+        val insertResult = nullableDate.insert3(tag, testDatas)
+        val queryResult = nullableDate.getAllByTagAndDefaultValue(tag)
+        val matchingIds =
+          testDatas.idsMatching(insertResult, defaultTestData.date.toKotlinxLocalDate())
+        queryResult.data.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Invalid Date String Tests
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  fun dateNonNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nonNullableDate.insert(testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNonNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nonNullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNullable_MutationInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(propTestConfig.copy(iterations = 500), Arb.dataConnect.invalidDateScalarString()) {
+        testData ->
+        val exception =
+          shouldThrow<DataConnectException> { nullableDate.insert(testData.toDateScalarString()) }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  @Test
+  fun dateNullable_QueryInvalidDateVariable() =
+    runTest(timeout = 1.minutes) {
+      checkAll(
+        propTestConfig.copy(iterations = 500),
+        Arb.dataConnect.tag(),
+        Arb.dataConnect.invalidDateScalarString()
+      ) { tag, testData ->
+        val exception =
+          shouldThrow<DataConnectException> {
+            nullableDate.getAllByTagAndValue(tag = tag, value = testData.toDateScalarString())
+          }
+        assertSoftly {
+          exception.message shouldContainWithNonAbuttingText "\$value"
+          exception.message shouldContainWithNonAbuttingTextIgnoringCase "invalid"
+        }
+      }
+    }
+
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+  // Helper methods and classes.
+  //////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Serializable private data class SingleKeyVariables(val key: Key)
+
+  @Serializable private data class SingleKeyData(val key: Key)
+
+  @Serializable
+  private data class MultipleKeysData(val items: List<Item>) {
+    @Serializable data class Item(val id: UUID)
+  }
+
+  @Serializable private data class ThreeKeysData(val key1: Key, val key2: Key, val key3: Key)
+
+  @Serializable private data class InsertVariables(val value: kotlinx.datetime.LocalDate?)
+
+  @Serializable private data class InsertStringVariables(val value: String)
+
+  @Serializable
+  private data class Insert3Variables(
+    val tag: String,
+    val value1: kotlinx.datetime.LocalDate?,
+    val value2: kotlinx.datetime.LocalDate?,
+    val value3: kotlinx.datetime.LocalDate?,
+  )
+
+  @Serializable private data class TagVariables(val tag: String)
+
+  @Serializable
+  private data class TagAndValueVariables(val tag: String, val value: kotlinx.datetime.LocalDate?)
+
+  @Serializable private data class TagAndStringValueVariables(val tag: String, val value: String)
+
+  @Serializable
+  private data class QueryData(val item: Item?) {
+    @Serializable data class Item(val value: kotlinx.datetime.LocalDate?)
+  }
+
+  @Serializable
+  private data class GetInsertedWithDefaultsByKeyQueryData(val item: Item?) {
+    @Serializable
+    data class Item(
+      val valueWithVariableDefault: kotlinx.datetime.LocalDate,
+      val valueWithVariableNullDefault: kotlinx.datetime.LocalDate?,
+      val valueWithSchemaDefault: kotlinx.datetime.LocalDate,
+      val valueWithSchemaNullDefault: kotlinx.datetime.LocalDate?,
+      val valueWithNoDefault: kotlinx.datetime.LocalDate?,
+      val epoch: kotlinx.datetime.LocalDate?,
+      val requestTime1: kotlinx.datetime.LocalDate?,
+      val requestTime2: kotlinx.datetime.LocalDate?,
+    )
+  }
+
+  @Serializable private data class Key(val id: UUID)
+
+  /** Operations for querying and mutating the table that stores non-nullable Date scalar values. */
+  private val nonNullableDate =
+    Operations(
+      getByKeyQueryName = "DateNonNullable_GetByKey",
+      getAllByTagAndValueQueryName = "DateNonNullable_GetAllByTagAndValue",
+      getAllByTagAndMaybeValueQueryName = "DateNonNullable_GetAllByTagAndMaybeValue",
+      getAllByTagAndDefaultValueQueryName = "DateNonNullable_GetAllByTagAndDefaultValue",
+      insertMutationName = "DateNonNullable_Insert",
+      insert3MutationName = "DateNonNullable_Insert3",
+      insertWithDefaultsMutationName = "DateNonNullableWithDefaults_Insert",
+      getInsertedWithDefaultsByKeyQueryName = "DateNonNullableWithDefaults_GetByKey",
+    )
+
+  /** Operations for querying and mutating the table that stores nullable Date scalar values. */
+  private val nullableDate =
+    Operations(
+      getByKeyQueryName = "DateNullable_GetByKey",
+      getAllByTagAndValueQueryName = "DateNullable_GetAllByTagAndValue",
+      getAllByTagAndMaybeValueQueryName = "DateNullable_GetAllByTagAndValue",
+      getAllByTagAndDefaultValueQueryName = "DateNullable_GetAllByTagAndDefaultValue",
+      insertMutationName = "DateNullable_Insert",
+      insert3MutationName = "DateNullable_Insert3",
+      insertWithDefaultsMutationName = "DateNullableWithDefaults_Insert",
+      getInsertedWithDefaultsByKeyQueryName = "DateNullableWithDefaults_GetByKey",
+    )
+
+  private inner class Operations(
+    getByKeyQueryName: String,
+    getAllByTagAndValueQueryName: String,
+    getAllByTagAndMaybeValueQueryName: String,
+    getAllByTagAndDefaultValueQueryName: String,
+    insertMutationName: String,
+    insert3MutationName: String,
+    insertWithDefaultsMutationName: String,
+    getInsertedWithDefaultsByKeyQueryName: String,
+  ) {
+
+    suspend fun insert(
+      localDate: kotlinx.datetime.LocalDate?
+    ): MutationResult<SingleKeyData, InsertVariables> = insert(InsertVariables(localDate))
+
+    suspend fun insert(variables: InsertVariables): MutationResult<SingleKeyData, InsertVariables> =
+      mutations.insert(variables).execute()
+
+    suspend fun insert(localDate: String): MutationResult<SingleKeyData, InsertStringVariables> =
+      insert(InsertStringVariables(localDate))
+
+    suspend fun insert(
+      variables: InsertStringVariables
+    ): MutationResult<SingleKeyData, InsertStringVariables> = mutations.insert(variables).execute()
+
+    suspend fun insert3(
+      tag: String,
+      testDatas: ThreeDateTestDatas,
+    ): MutationResult<ThreeKeysData, Insert3Variables> =
+      insert3(
+        tag = tag,
+        value1 = testDatas.testData1?.date?.toKotlinxLocalDate(),
+        value2 = testDatas.testData2?.date?.toKotlinxLocalDate(),
+        value3 = testDatas.testData3?.date?.toKotlinxLocalDate()
+      )
+
+    suspend fun insert3(
+      tag: String,
+      value1: kotlinx.datetime.LocalDate?,
+      value2: kotlinx.datetime.LocalDate?,
+      value3: kotlinx.datetime.LocalDate?,
+    ): MutationResult<ThreeKeysData, Insert3Variables> =
+      insert3(Insert3Variables(tag = tag, value1 = value1, value2 = value2, value3 = value3))
+
+    suspend fun insert3(
+      variables: Insert3Variables
+    ): MutationResult<ThreeKeysData, Insert3Variables> = mutations.insert3(variables).execute()
+
+    suspend fun getByKey(key: Key): QueryResult<QueryData, SingleKeyVariables> =
+      getByKey(SingleKeyVariables(key))
+
+    suspend fun getByKey(
+      variables: SingleKeyVariables
+    ): QueryResult<QueryData, SingleKeyVariables> = queries.getByKey(variables).execute()
+
+    suspend fun getAllByTagAndValue(
+      tag: String,
+      value: kotlinx.datetime.LocalDate?
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      getAllByTagAndValue(TagAndValueVariables(tag, value))
+
+    suspend fun getAllByTagAndValue(
+      variables: TagAndValueVariables
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      queries.getAllByTagAndValue(variables).execute()
+
+    suspend fun getAllByTagAndValue(
+      tag: String,
+      value: String
+    ): QueryResult<MultipleKeysData, TagAndStringValueVariables> =
+      getAllByTagAndValue(TagAndStringValueVariables(tag, value))
+
+    suspend fun getAllByTagAndValue(
+      variables: TagAndStringValueVariables
+    ): QueryResult<MultipleKeysData, TagAndStringValueVariables> =
+      queries.getAllByTagAndValue(variables).execute()
+
+    suspend fun getAllByTagAndMaybeValue(
+      tag: String,
+    ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndMaybeValue(TagVariables(tag))
+
+    suspend fun getAllByTagAndMaybeValue(
+      variables: TagVariables
+    ): QueryResult<MultipleKeysData, TagVariables> =
+      queries.getAllByTagAndMaybeValue(variables).execute()
+
+    suspend fun getAllByTagAndMaybeValue(
+      tag: String,
+      value: Nothing?,
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      getAllByTagAndMaybeValue(TagAndValueVariables(tag, value))
+
+    suspend fun getAllByTagAndMaybeValue(
+      variables: TagAndValueVariables
+    ): QueryResult<MultipleKeysData, TagAndValueVariables> =
+      queries.getAllByTagAndMaybeValue(variables).execute()
+
+    suspend fun getAllByTagAndDefaultValue(
+      tag: String
+    ): QueryResult<MultipleKeysData, TagVariables> = getAllByTagAndDefaultValue(TagVariables(tag))
+
+    suspend fun getAllByTagAndDefaultValue(
+      variables: TagVariables
+    ): QueryResult<MultipleKeysData, TagVariables> =
+      queries.getAllByTagAndDefaultValue(variables).execute()
+
+    suspend fun insertWithDefaults(): MutationResult<SingleKeyData, Unit> =
+      mutations.insertWithDefaults().execute()
+
+    suspend fun getInsertedWithDefaultsByKey(
+      key: Key
+    ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+      getInsertedWithDefaultsByKey(SingleKeyVariables(key))
+
+    suspend fun getInsertedWithDefaultsByKey(
+      variables: SingleKeyVariables
+    ): QueryResult<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+      queries.getInsertedWithDefaultsByKey(variables).execute()
+
+    private val queries =
+      object {
+        fun getByKey(variables: SingleKeyVariables): QueryRef<QueryData, SingleKeyVariables> =
+          dataConnect.query(
+            getByKeyQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        inline fun <reified Variables> getAllByTagAndValue(
+          variables: Variables
+        ): QueryRef<MultipleKeysData, Variables> =
+          dataConnect.query(
+            getAllByTagAndValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndMaybeValue(
+          variables: TagVariables
+        ): QueryRef<MultipleKeysData, TagVariables> =
+          dataConnect.query(
+            getAllByTagAndMaybeValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndMaybeValue(
+          variables: TagAndValueVariables
+        ): QueryRef<MultipleKeysData, TagAndValueVariables> =
+          dataConnect.query(
+            getAllByTagAndMaybeValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getAllByTagAndDefaultValue(
+          variables: TagVariables
+        ): QueryRef<MultipleKeysData, TagVariables> =
+          dataConnect.query(
+            getAllByTagAndDefaultValueQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun getInsertedWithDefaultsByKey(
+          variables: SingleKeyVariables
+        ): QueryRef<GetInsertedWithDefaultsByKeyQueryData, SingleKeyVariables> =
+          dataConnect.query(
+            getInsertedWithDefaultsByKeyQueryName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+      }
+
+    private val mutations =
+      object {
+        inline fun <reified Variables> insert(
+          variables: Variables
+        ): MutationRef<SingleKeyData, Variables> =
+          dataConnect.mutation(
+            insertMutationName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun insert3(variables: Insert3Variables): MutationRef<ThreeKeysData, Insert3Variables> =
+          dataConnect.mutation(
+            insert3MutationName,
+            variables,
+            serializer(),
+            serializer(),
+          )
+
+        fun insertWithDefaults(): MutationRef<SingleKeyData, Unit> =
+          dataConnect.mutation(
+            insertWithDefaultsMutationName,
+            Unit,
+            serializer(),
+            serializer(),
+          )
+      }
+  }
+
+  private companion object {
+    val propTestConfig =
+      PropTestConfig(
+        iterations = 20,
+        edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.5),
+      )
+
+    fun ThreeDateTestDatas.idsMatchingSelected(
+      result: MutationResult<ThreeKeysData, *>
+    ): List<UUID> = idsMatchingSelected(result.data)
+
+    fun ThreeDateTestDatas.idsMatchingSelected(data: ThreeKeysData): List<UUID> =
+      idsMatchingSelected {
+        data.uuidFromItemNumber(it)
+      }
+
+    fun ThreeDateTestDatas.idsMatching(
+      result: MutationResult<ThreeKeysData, *>,
+      localDate: kotlinx.datetime.LocalDate?,
+    ): List<UUID> = idsMatching(result.data, localDate)
+
+    fun ThreeDateTestDatas.idsMatching(
+      data: ThreeKeysData,
+      localDate: kotlinx.datetime.LocalDate?,
+    ): List<UUID> = idsMatching(localDate) { data.uuidFromItemNumber(it) }
+
+    fun ThreeKeysData.uuidFromItemNumber(itemNumber: ItemNumber): UUID =
+      when (itemNumber) {
+        ItemNumber.ONE -> key1
+        ItemNumber.TWO -> key2
+        ItemNumber.THREE -> key3
+      }.id
+  }
+}
diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
index 05055ba4e39..490fea04961 100644
--- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
@@ -17,6 +17,12 @@
 @file:OptIn(ExperimentalKotest::class)
 @file:UseSerializers(UUIDSerializer::class)
 
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and
+// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE
+// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
+// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect
 
 import com.google.firebase.dataconnect.serializers.UUIDSerializer
@@ -60,6 +66,12 @@ import kotlinx.serialization.UseSerializers
 import kotlinx.serialization.serializer
 import org.junit.Test
 
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and
+// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE
+// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
+// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+////////////////////////////////////////////////////////////////////////////////
 class LocalDateIntegrationTest : DataConnectIntegrationTestBase() {
 
   private val dataConnect: FirebaseDataConnect by lazy {
diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt
new file mode 100644
index 00000000000..3d7722b318c
--- /dev/null
+++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializer.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.firebase.dataconnect.serializers
+
+import com.google.firebase.dataconnect.toDataConnectLocalDate
+import com.google.firebase.dataconnect.toJavaLocalDate
+import java.time.LocalDate
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+/**
+ * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the
+ * wire format expected by the Firebase Data Connect backend.
+ *
+ * Be sure to _only_ call this method if [java.time.LocalDate] is available. See the documentation
+ * for [toJavaLocalDate] for details.
+ *
+ * @see LocalDateSerializer
+ * @see KotlinxDatetimeLocalDateSerializer
+ */
+public object JavaTimeLocalDateSerializer : KSerializer<LocalDate> {
+
+  override val descriptor: SerialDescriptor =
+    PrimitiveSerialDescriptor("java.time.LocalDate", PrimitiveKind.STRING)
+
+  override fun serialize(encoder: Encoder, value: LocalDate) {
+    LocalDateSerializer.serialize(encoder, value.toDataConnectLocalDate())
+  }
+
+  override fun deserialize(decoder: Decoder): LocalDate {
+    return LocalDateSerializer.deserialize(decoder).toJavaLocalDate()
+  }
+}
diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt
new file mode 100644
index 00000000000..803fb3457e9
--- /dev/null
+++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializer.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.firebase.dataconnect.serializers
+
+import com.google.firebase.dataconnect.toDataConnectLocalDate
+import com.google.firebase.dataconnect.toKotlinxLocalDate
+import kotlinx.datetime.LocalDate
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+/**
+ * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the
+ * wire format expected by the Firebase Data Connect backend.
+ *
+ * Be sure to _only_ use this class if your application has a dependency on
+ * `org.jetbrains.kotlinx:kotlinx-datetime`. See the documentation for [toKotlinxLocalDate] for
+ * details.
+ *
+ * @see LocalDateSerializer
+ * @see JavaTimeLocalDateSerializer
+ */
+public object KotlinxDatetimeLocalDateSerializer : KSerializer<LocalDate> {
+
+  override val descriptor: SerialDescriptor =
+    PrimitiveSerialDescriptor("java.time.LocalDate", PrimitiveKind.STRING)
+
+  override fun serialize(encoder: Encoder, value: LocalDate) {
+    LocalDateSerializer.serialize(encoder, value.toDataConnectLocalDate())
+  }
+
+  override fun deserialize(decoder: Decoder): LocalDate {
+    return LocalDateSerializer.deserialize(decoder).toKotlinxLocalDate()
+  }
+}
diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt
index 0b699a1a6d3..22cb087cd3c 100644
--- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt
+++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializer.kt
@@ -29,6 +29,9 @@ import kotlinx.serialization.encoding.Encoder
 /**
  * An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the
  * wire format expected by the Firebase Data Connect backend.
+ *
+ * @see JavaTimeLocalDateSerializer
+ * @see KotlinxDatetimeLocalDateSerializer
  */
 public object LocalDateSerializer : KSerializer<LocalDate> {
 
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
new file mode 100644
index 00000000000..4b46994985f
--- /dev/null
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.
+ */
+@file:OptIn(ExperimentalKotest::class)
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+package com.google.firebase.dataconnect.serializers
+
+import com.google.firebase.dataconnect.testutil.dayRangeInYear
+import com.google.firebase.dataconnect.testutil.property.arbitrary.intWithEvenNumDigitsDistribution
+import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromValue
+import com.google.firebase.dataconnect.util.ProtoUtil.encodeToValue
+import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.common.ExperimentalKotest
+import io.kotest.matchers.shouldBe
+import io.kotest.property.Arb
+import io.kotest.property.EdgeConfig
+import io.kotest.property.PropTestConfig
+import io.kotest.property.arbitrary.Codepoint
+import io.kotest.property.arbitrary.arabic
+import io.kotest.property.arbitrary.arbitrary
+import io.kotest.property.arbitrary.ascii
+import io.kotest.property.arbitrary.boolean
+import io.kotest.property.arbitrary.booleanArray
+import io.kotest.property.arbitrary.constant
+import io.kotest.property.arbitrary.cyrillic
+import io.kotest.property.arbitrary.egyptianHieroglyphs
+import io.kotest.property.arbitrary.enum
+import io.kotest.property.arbitrary.filterNot
+import io.kotest.property.arbitrary.greekCoptic
+import io.kotest.property.arbitrary.int
+import io.kotest.property.arbitrary.katakana
+import io.kotest.property.arbitrary.long
+import io.kotest.property.arbitrary.merge
+import io.kotest.property.arbitrary.next
+import io.kotest.property.arbitrary.string
+import io.kotest.property.arbitrary.triple
+import io.kotest.property.arbitrary.withEdgecases
+import io.kotest.property.checkAll
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.random.nextInt
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.encoding.Decoder
+import org.junit.Test
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+class JavaTimeLocalDateSerializerUnitTest {
+
+  @Test
+  fun `serialize() should produce the expected serialized string`() = runTest {
+    checkAll(propTestConfig, Arb.localDate()) { localDate ->
+      val value = encodeToValue(localDate, JavaTimeLocalDateSerializer, serializersModule = null)
+      value.stringValue shouldBe localDate.toYYYYMMDDWithZeroPadding()
+    }
+  }
+
+  @Test
+  fun `deserialize() should produce the expected LocalDate object`() = runTest {
+    val numPaddingCharsArb = Arb.int(0..10)
+    val arb = Arb.triple(numPaddingCharsArb, numPaddingCharsArb, numPaddingCharsArb)
+    checkAll(propTestConfig, Arb.localDate(), arb) { localDate, paddingCharsTriple ->
+      val (yearPadding, monthPadding, dayPadding) = paddingCharsTriple
+      val value =
+        localDate
+          .toYYYYMMDDWithZeroPadding(
+            yearPadding = yearPadding,
+            monthPadding = monthPadding,
+            dayPadding = dayPadding
+          )
+          .toValueProto()
+
+      val decodedLocalDate =
+        decodeFromValue(value, JavaTimeLocalDateSerializer, serializersModule = null)
+      decodedLocalDate shouldBe localDate
+    }
+  }
+
+  @Test
+  fun `deserialize() should throw IllegalArgumentException when given unparseable strings`() =
+    runTest {
+      checkAll(propTestConfig, Arb.unparseableDate()) { encodedDate ->
+        val decoder: Decoder = mockk { every { decodeString() } returns encodedDate }
+        shouldThrow<IllegalArgumentException> { JavaTimeLocalDateSerializer.deserialize(decoder) }
+      }
+    }
+
+  private companion object {
+    val propTestConfig =
+      PropTestConfig(
+        iterations = 500,
+        edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2)
+      )
+
+    fun java.time.LocalDate.toYYYYMMDDWithZeroPadding(
+      yearPadding: Int = 4,
+      monthPadding: Int = 2,
+      dayPadding: Int = 2,
+    ): String {
+      val yearString = year.toZeroPaddedString(yearPadding)
+      val monthString = month.value.toZeroPaddedString(monthPadding)
+      val dayString = dayOfMonth.toZeroPaddedString(dayPadding)
+      return "$yearString-$monthString-$dayString"
+    }
+
+    fun Int.toZeroPaddedString(length: Int): String = buildString {
+      append(this@toZeroPaddedString)
+      val signChar =
+        firstOrNull()?.let {
+          if (it == '-') {
+            deleteCharAt(0)
+            it
+          } else {
+            null
+          }
+        }
+
+      while (this.length < length) {
+        insert(0, '0')
+      }
+
+      if (signChar !== null) {
+        insert(0, signChar)
+      }
+    }
+
+    fun Arb.Companion.localDate(
+      year: Arb<Int> =
+        intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE),
+      month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12),
+      day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31),
+    ): Arb<java.time.LocalDate> {
+      fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int {
+        val monthObject = org.threeten.bp.Month.of(month)
+        val yearObject = org.threeten.bp.Year.of(year)
+        val dayRange = monthObject.dayRangeInYear(yearObject)
+        return coerceIn(dayRange)
+      }
+      return arbitrary(
+        edgecaseFn = { rs ->
+          val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!!
+          val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!!
+          val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!!
+          val coercedDayInt =
+            dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt)
+          java.time.LocalDate.of(yearInt, monthInt, coercedDayInt)
+        },
+        sampleFn = {
+          val yearInt = year.bind()
+          val monthInt = month.bind()
+          val dayInt = day.bind()
+          val coercedDayInt =
+            dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt)
+          java.time.LocalDate.of(yearInt, monthInt, coercedDayInt)
+        }
+      )
+    }
+
+    private enum class UnparseableNumberReason {
+      EmptyString,
+      InvalidChars,
+      GreaterThanIntMax,
+      LessThanIntMin,
+    }
+
+    private val codepoints =
+      Codepoint.ascii()
+        .merge(Codepoint.egyptianHieroglyphs())
+        .merge(Codepoint.arabic())
+        .merge(Codepoint.cyrillic())
+        .merge(Codepoint.greekCoptic())
+        .merge(Codepoint.katakana())
+
+    fun Arb.Companion.unparseableNumber(): Arb<String> {
+      val reasonArb = enum<UnparseableNumberReason>()
+      val validIntArb = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE)
+      val validChars = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-').map { it.code }
+      val invalidString =
+        string(1..5, codepoints.filterNot { validChars.contains(it.value) }).withEdgecases("-")
+      val tooLargeValues = long(Int.MAX_VALUE.toLong() + 1L..Long.MAX_VALUE)
+      val tooSmallValues = long(Long.MIN_VALUE until Int.MIN_VALUE.toLong())
+      return arbitrary { rs ->
+        when (reasonArb.bind()) {
+          UnparseableNumberReason.EmptyString -> ""
+          UnparseableNumberReason.GreaterThanIntMax -> "${tooLargeValues.bind()}"
+          UnparseableNumberReason.LessThanIntMin -> "${tooSmallValues.bind()}"
+          UnparseableNumberReason.InvalidChars -> {
+            val flags = Array(3) { rs.random.nextBoolean() }
+            if (!flags[0]) {
+              flags[2] = true
+            }
+            val prefix = if (flags[0]) invalidString.bind() else ""
+            val mid = if (flags[1]) validIntArb.bind() else ""
+            val suffix = if (flags[2]) invalidString.bind() else ""
+            "$prefix$mid$suffix"
+          }
+        }
+      }
+    }
+
+    fun Arb.Companion.unparseableDash(): Arb<String> {
+      val invalidString = string(1..5, codepoints.filterNot { it.value == '-'.code })
+      return arbitrary { rs ->
+        val flags = Array(3) { rs.random.nextBoolean() }
+        if (!flags[0]) {
+          flags[2] = true
+        }
+
+        val prefix = if (flags[0]) invalidString.bind() else ""
+        val mid = if (flags[1]) "-" else ""
+        val suffix = if (flags[2]) invalidString.bind() else ""
+
+        "$prefix$mid$suffix"
+      }
+    }
+
+    fun Arb.Companion.unparseableDate(): Arb<String> {
+      val validNumber = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE)
+      val unparseableNumber = unparseableNumber()
+      val unparseableDash = unparseableDash()
+      val booleanArray = booleanArray(Arb.constant(5), Arb.boolean())
+      return arbitrary(edgecases = listOf("", "-", "--", "---")) { rs ->
+        val invalidCharFlags = booleanArray.bind()
+        if (invalidCharFlags.count { it } == 0) {
+          invalidCharFlags[rs.random.nextInt(invalidCharFlags.indices)] = true
+        }
+
+        val year = if (invalidCharFlags[0]) unparseableNumber.bind() else validNumber.bind()
+        val dash1 = if (invalidCharFlags[1]) unparseableDash.bind() else "-"
+        val month = if (invalidCharFlags[2]) unparseableNumber.bind() else validNumber.bind()
+        val dash2 = if (invalidCharFlags[3]) unparseableDash.bind() else "-"
+        val day = if (invalidCharFlags[4]) unparseableNumber.bind() else validNumber.bind()
+
+        "$year$dash1$month$dash2$day"
+      }
+    }
+  }
+}
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt
new file mode 100644
index 00000000000..3ab96a648a9
--- /dev/null
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2024 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
+ *
+ *      http://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.
+ */
+@file:OptIn(ExperimentalKotest::class)
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+package com.google.firebase.dataconnect.serializers
+
+import com.google.firebase.dataconnect.testutil.dayRangeInYear
+import com.google.firebase.dataconnect.testutil.property.arbitrary.intWithEvenNumDigitsDistribution
+import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromValue
+import com.google.firebase.dataconnect.util.ProtoUtil.encodeToValue
+import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.common.ExperimentalKotest
+import io.kotest.matchers.shouldBe
+import io.kotest.property.Arb
+import io.kotest.property.EdgeConfig
+import io.kotest.property.PropTestConfig
+import io.kotest.property.arbitrary.Codepoint
+import io.kotest.property.arbitrary.arabic
+import io.kotest.property.arbitrary.arbitrary
+import io.kotest.property.arbitrary.ascii
+import io.kotest.property.arbitrary.boolean
+import io.kotest.property.arbitrary.booleanArray
+import io.kotest.property.arbitrary.constant
+import io.kotest.property.arbitrary.cyrillic
+import io.kotest.property.arbitrary.egyptianHieroglyphs
+import io.kotest.property.arbitrary.enum
+import io.kotest.property.arbitrary.filterNot
+import io.kotest.property.arbitrary.greekCoptic
+import io.kotest.property.arbitrary.int
+import io.kotest.property.arbitrary.katakana
+import io.kotest.property.arbitrary.long
+import io.kotest.property.arbitrary.merge
+import io.kotest.property.arbitrary.next
+import io.kotest.property.arbitrary.string
+import io.kotest.property.arbitrary.triple
+import io.kotest.property.arbitrary.withEdgecases
+import io.kotest.property.checkAll
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.random.nextInt
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.encoding.Decoder
+import org.junit.Test
+
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
+// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
+// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt,
+// if appropriate.
+////////////////////////////////////////////////////////////////////////////////
+class KotlinxDatetimeLocalDateSerializerUnitTest {
+
+  @Test
+  fun `serialize() should produce the expected serialized string`() = runTest {
+    checkAll(propTestConfig, Arb.localDate()) { localDate ->
+      val value =
+        encodeToValue(localDate, KotlinxDatetimeLocalDateSerializer, serializersModule = null)
+      value.stringValue shouldBe localDate.toYYYYMMDDWithZeroPadding()
+    }
+  }
+
+  @Test
+  fun `deserialize() should produce the expected LocalDate object`() = runTest {
+    val numPaddingCharsArb = Arb.int(0..10)
+    val arb = Arb.triple(numPaddingCharsArb, numPaddingCharsArb, numPaddingCharsArb)
+    checkAll(propTestConfig, Arb.localDate(), arb) { localDate, paddingCharsTriple ->
+      val (yearPadding, monthPadding, dayPadding) = paddingCharsTriple
+      val value =
+        localDate
+          .toYYYYMMDDWithZeroPadding(
+            yearPadding = yearPadding,
+            monthPadding = monthPadding,
+            dayPadding = dayPadding
+          )
+          .toValueProto()
+
+      val decodedLocalDate =
+        decodeFromValue(value, KotlinxDatetimeLocalDateSerializer, serializersModule = null)
+      decodedLocalDate shouldBe localDate
+    }
+  }
+
+  @Test
+  fun `deserialize() should throw IllegalArgumentException when given unparseable strings`() =
+    runTest {
+      checkAll(propTestConfig, Arb.unparseableDate()) { encodedDate ->
+        val decoder: Decoder = mockk { every { decodeString() } returns encodedDate }
+        shouldThrow<IllegalArgumentException> {
+          KotlinxDatetimeLocalDateSerializer.deserialize(decoder)
+        }
+      }
+    }
+
+  private companion object {
+    val propTestConfig =
+      PropTestConfig(
+        iterations = 500,
+        edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2)
+      )
+
+    fun kotlinx.datetime.LocalDate.toYYYYMMDDWithZeroPadding(
+      yearPadding: Int = 4,
+      monthPadding: Int = 2,
+      dayPadding: Int = 2,
+    ): String {
+      val yearString = year.toZeroPaddedString(yearPadding)
+      val monthString = month.value.toZeroPaddedString(monthPadding)
+      val dayString = dayOfMonth.toZeroPaddedString(dayPadding)
+      return "$yearString-$monthString-$dayString"
+    }
+
+    fun Int.toZeroPaddedString(length: Int): String = buildString {
+      append(this@toZeroPaddedString)
+      val signChar =
+        firstOrNull()?.let {
+          if (it == '-') {
+            deleteCharAt(0)
+            it
+          } else {
+            null
+          }
+        }
+
+      while (this.length < length) {
+        insert(0, '0')
+      }
+
+      if (signChar !== null) {
+        insert(0, signChar)
+      }
+    }
+
+    fun Arb.Companion.localDate(
+      year: Arb<Int> =
+        intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE),
+      month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12),
+      day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31),
+    ): Arb<kotlinx.datetime.LocalDate> {
+      fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int {
+        val monthObject = org.threeten.bp.Month.of(month)
+        val yearObject = org.threeten.bp.Year.of(year)
+        val dayRange = monthObject.dayRangeInYear(yearObject)
+        return coerceIn(dayRange)
+      }
+      return arbitrary(
+        edgecaseFn = { rs ->
+          val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!!
+          val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!!
+          val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!!
+          val coercedDayInt =
+            dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt)
+          kotlinx.datetime.LocalDate(yearInt, monthInt, coercedDayInt)
+        },
+        sampleFn = {
+          val yearInt = year.bind()
+          val monthInt = month.bind()
+          val dayInt = day.bind()
+          val coercedDayInt =
+            dayInt.coerceDayOfMonthIntoValidRangeFor(month = monthInt, year = yearInt)
+          kotlinx.datetime.LocalDate(yearInt, monthInt, coercedDayInt)
+        }
+      )
+    }
+
+    private enum class UnparseableNumberReason {
+      EmptyString,
+      InvalidChars,
+      GreaterThanIntMax,
+      LessThanIntMin,
+    }
+
+    private val codepoints =
+      Codepoint.ascii()
+        .merge(Codepoint.egyptianHieroglyphs())
+        .merge(Codepoint.arabic())
+        .merge(Codepoint.cyrillic())
+        .merge(Codepoint.greekCoptic())
+        .merge(Codepoint.katakana())
+
+    fun Arb.Companion.unparseableNumber(): Arb<String> {
+      val reasonArb = enum<UnparseableNumberReason>()
+      val validIntArb = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE)
+      val validChars = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-').map { it.code }
+      val invalidString =
+        string(1..5, codepoints.filterNot { validChars.contains(it.value) }).withEdgecases("-")
+      val tooLargeValues = long(Int.MAX_VALUE.toLong() + 1L..Long.MAX_VALUE)
+      val tooSmallValues = long(Long.MIN_VALUE until Int.MIN_VALUE.toLong())
+      return arbitrary { rs ->
+        when (reasonArb.bind()) {
+          UnparseableNumberReason.EmptyString -> ""
+          UnparseableNumberReason.GreaterThanIntMax -> "${tooLargeValues.bind()}"
+          UnparseableNumberReason.LessThanIntMin -> "${tooSmallValues.bind()}"
+          UnparseableNumberReason.InvalidChars -> {
+            val flags = Array(3) { rs.random.nextBoolean() }
+            if (!flags[0]) {
+              flags[2] = true
+            }
+            val prefix = if (flags[0]) invalidString.bind() else ""
+            val mid = if (flags[1]) validIntArb.bind() else ""
+            val suffix = if (flags[2]) invalidString.bind() else ""
+            "$prefix$mid$suffix"
+          }
+        }
+      }
+    }
+
+    fun Arb.Companion.unparseableDash(): Arb<String> {
+      val invalidString = string(1..5, codepoints.filterNot { it.value == '-'.code })
+      return arbitrary { rs ->
+        val flags = Array(3) { rs.random.nextBoolean() }
+        if (!flags[0]) {
+          flags[2] = true
+        }
+
+        val prefix = if (flags[0]) invalidString.bind() else ""
+        val mid = if (flags[1]) "-" else ""
+        val suffix = if (flags[2]) invalidString.bind() else ""
+
+        "$prefix$mid$suffix"
+      }
+    }
+
+    fun Arb.Companion.unparseableDate(): Arb<String> {
+      val validNumber = intWithEvenNumDigitsDistribution(0..Int.MAX_VALUE)
+      val unparseableNumber = unparseableNumber()
+      val unparseableDash = unparseableDash()
+      val booleanArray = booleanArray(Arb.constant(5), Arb.boolean())
+      return arbitrary(edgecases = listOf("", "-", "--", "---")) { rs ->
+        val invalidCharFlags = booleanArray.bind()
+        if (invalidCharFlags.count { it } == 0) {
+          invalidCharFlags[rs.random.nextInt(invalidCharFlags.indices)] = true
+        }
+
+        val year = if (invalidCharFlags[0]) unparseableNumber.bind() else validNumber.bind()
+        val dash1 = if (invalidCharFlags[1]) unparseableDash.bind() else "-"
+        val month = if (invalidCharFlags[2]) unparseableNumber.bind() else validNumber.bind()
+        val dash2 = if (invalidCharFlags[3]) unparseableDash.bind() else "-"
+        val day = if (invalidCharFlags[4]) unparseableNumber.bind() else validNumber.bind()
+
+        "$year$dash1$month$dash2$day"
+      }
+    }
+  }
+}
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
index 6d94b9ca477..6bc0c45d3bc 100644
--- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
@@ -15,6 +15,12 @@
  */
 @file:OptIn(ExperimentalKotest::class)
 
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and
+// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE
+// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
+// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect.serializers
 
 import com.google.firebase.dataconnect.LocalDate
@@ -56,6 +62,12 @@ import kotlinx.coroutines.test.runTest
 import kotlinx.serialization.encoding.Decoder
 import org.junit.Test
 
+////////////////////////////////////////////////////////////////////////////////
+// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and
+// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE
+// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
+// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+////////////////////////////////////////////////////////////////////////////////
 class LocalDateSerializerUnitTest {
 
   @Test
diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt
index 7e1fc14c8fc..0e9837ee37b 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/JavaTime.kt
@@ -16,11 +16,22 @@
 
 package com.google.firebase.dataconnect.testutil
 
-import com.google.firebase.Timestamp
-import com.google.firebase.dataconnect.LocalDate
-import org.threeten.bp.Instant
+import android.annotation.SuppressLint
 
-fun Instant.toTimestamp(): Timestamp = Timestamp(epochSecond, nano)
+fun org.threeten.bp.Instant.toTimestamp(): com.google.firebase.Timestamp =
+  com.google.firebase.Timestamp(epochSecond, nano)
 
-fun LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate =
-  org.threeten.bp.LocalDate.of(year, month, day)
+fun com.google.firebase.dataconnect.LocalDate.toTheeTenAbpJavaLocalDate():
+  org.threeten.bp.LocalDate = org.threeten.bp.LocalDate.of(year, month, day)
+
+@SuppressLint("NewApi")
+fun java.time.LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate {
+  val threeTenBpMonth = org.threeten.bp.Month.of(monthValue)
+  return org.threeten.bp.LocalDate.of(year, threeTenBpMonth, dayOfMonth)
+}
+
+@SuppressLint("NewApi")
+fun kotlinx.datetime.LocalDate.toTheeTenAbpJavaLocalDate(): org.threeten.bp.LocalDate {
+  val threeTenBpMonth = org.threeten.bp.Month.of(monthNumber)
+  return org.threeten.bp.LocalDate.of(year, threeTenBpMonth, dayOfMonth)
+}
diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
index 1535b12a167..662062e4069 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/dates.kt
@@ -23,6 +23,7 @@ import com.google.firebase.dataconnect.testutil.NullableReference
 import com.google.firebase.dataconnect.testutil.dayRangeInYear
 import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MAX_YEAR
 import com.google.firebase.dataconnect.testutil.property.arbitrary.DateEdgeCases.MIN_YEAR
+import com.google.firebase.dataconnect.toDataConnectLocalDate
 import io.kotest.property.Arb
 import io.kotest.property.RandomSource
 import io.kotest.property.Sample
@@ -99,7 +100,17 @@ private class DateTestDataArb : Arb<DateTestData>() {
 data class DateTestData(
   val date: LocalDate,
   val string: String,
-)
+) {
+  constructor(
+    date: java.time.LocalDate,
+    string: String
+  ) : this(date.toDataConnectLocalDate(), string)
+
+  constructor(
+    date: kotlinx.datetime.LocalDate,
+    string: String
+  ) : this(date.toDataConnectLocalDate(), string)
+}
 
 @Suppress("MemberVisibilityCanBePrivate")
 object DateEdgeCases {
@@ -149,11 +160,24 @@ data class ThreeDateTestDatas(
   fun idsMatchingSelected(getter: (ItemNumber) -> UUID): List<UUID> =
     idsMatching(selected?.date, getter)
 
-  fun idsMatching(localDate: LocalDate?, getter: (ItemNumber) -> UUID): List<UUID> {
+  fun idsMatching(
+    localDate: LocalDate?,
+    getter: (ItemNumber) -> UUID,
+  ): List<UUID> {
     val ids = listOf(getter(ItemNumber.ONE), getter(ItemNumber.TWO), getter(ItemNumber.THREE))
     return ids.filterIndexed { index, _ -> all[index]?.date == localDate }
   }
 
+  fun idsMatching(
+    localDate: java.time.LocalDate?,
+    getter: (ItemNumber) -> UUID,
+  ): List<UUID> = idsMatching(localDate?.toDataConnectLocalDate(), getter)
+
+  fun idsMatching(
+    localDate: kotlinx.datetime.LocalDate?,
+    getter: (ItemNumber) -> UUID,
+  ): List<UUID> = idsMatching(localDate?.toDataConnectLocalDate(), getter)
+
   enum class ItemNumber {
     ONE,
     TWO,

From 2eb3f3b80636b21fd55d643ea289a8b8ff03cd4d Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Thu, 14 Nov 2024 06:20:31 +0000
Subject: [PATCH 12/13] Generate code for java.time and kotlinx.datetime
 LocalDate tests

---
 .../firebase-dataconnect.gradle.kts           | 298 ++++++++++++++++++
 .../JavaTimeLocalDateIntegrationTest.kt       |  16 +-
 ...KotlinxDatetimeLocalDateIntegrationTest.kt |  16 +-
 .../dataconnect/LocalDateIntegrationTest.kt   |  18 +-
 .../JavaTimeLocalDateSerializerUnitTest.kt    |  16 +-
 ...linxDatetimeLocalDateSerializerUnitTest.kt |  16 +-
 .../LocalDateSerializerUnitTest.kt            |  28 +-
 7 files changed, 359 insertions(+), 49 deletions(-)

diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts
index 87ffe572916..9ee826c6f6a 100644
--- a/firebase-dataconnect/firebase-dataconnect.gradle.kts
+++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts
@@ -163,3 +163,301 @@ tasks.withType<KotlinCompile>().all {
     }
   }
 }
+
+/**
+ * Performs various transformations on a mutable list of strings. This class is _not_ thread safe;
+ * any concurrent use must be synchronized externally or else the behavior is undefined.
+ * @property lines the lines to mutate; this list is modified in-place.
+ */
+class TextLinesTransformer(val lines: MutableList<String>) {
+  constructor(lines: Iterable<String>) : this(lines.toMutableList())
+
+  fun indexOf(predicateDescription: String, predicate: (String) -> Boolean): Int {
+    val index = lines.indexOfFirst(predicate)
+    if (index < 0) {
+      throw TextLinesTransformerException("unable to find a line that $predicateDescription")
+    }
+    return index
+  }
+
+  fun atLineThatStartsWith(prefix: String): IndexBasedOperations {
+    val index = indexOf("starts with \"$prefix\"") { it.startsWith(prefix) }
+    return IndexBasedOperations(index)
+  }
+
+  fun removeLine(line: String) {
+    lines.removeAll { it.trim() == line }
+  }
+
+  fun replaceLine(line: String, replacementLine: String) {
+    lines.replaceAll { originalLine -> originalLine.takeIf { it != line } ?: replacementLine }
+  }
+
+  fun replaceWord(
+    original: String,
+    replacement: String,
+    predicate: (line: String) -> Boolean = { true }
+  ) {
+    val regex = Regex("""(\W|^)${Regex.escape(original)}(\W|$)""")
+    lines.replaceAll { line ->
+      if (!predicate(line)) {
+        line
+      } else {
+        regex.replace(line) { matchResult ->
+          val prefix = matchResult.groupValues[1]
+          val suffix = matchResult.groupValues[2]
+          "$prefix${Regex.escapeReplacement(replacement)}$suffix"
+        }
+      }
+    }
+  }
+
+  fun replaceText(original: String, replacement: String) {
+    lines.replaceAll { it.replace(original, replacement) }
+  }
+
+  fun replaceRegex(pattern: String, replacement: String) {
+    val regex = Regex(pattern)
+    lines.replaceAll { regex.replace(it, replacement) }
+  }
+
+  fun applyReplacements(linesByReplacementId: Map<String, List<String>>) {
+    for (index in lines.indices.reversed()) {
+      val line = lines[index]
+      val matchResult = replacementsRegex.matchEntire(line.trim()) ?: continue
+      val lineDeleteCount = matchResult.groupValues[1].toInt() + 1
+      val replacementId = matchResult.groupValues[2]
+
+      val replacementLines =
+        linesByReplacementId[replacementId]
+          ?: throw Exception(
+            "Replacement ID \"$replacementId\" is not known; " +
+              "there are ${linesByReplacementId.size} known replacementIds: " +
+              linesByReplacementId.keys.sorted().joinToString(", ") +
+              " (error code zgcc257b23)"
+          )
+
+      repeat(lineDeleteCount) { lines.removeAt(index) }
+      lines.addAll(index, replacementLines)
+    }
+  }
+
+  inner class IndexBasedOperations(private var index: Int) {
+    fun deleteLinesAboveThatStartWith(prefix: String): IndexBasedOperations = apply {
+      while (lines[index - 1].startsWith(prefix)) {
+        lines.removeAt(index - 1)
+        index--
+      }
+    }
+
+    fun insertAbove(line: String): IndexBasedOperations = apply { lines.add(index, line) }
+
+    fun insertAbove(lines: Collection<String>): IndexBasedOperations = apply {
+      this@TextLinesTransformer.lines.addAll(index, lines)
+    }
+  }
+
+  private class TextLinesTransformerException(message: String) : Exception(message)
+
+  companion object {
+    private val replacementsRegex: Regex = run {
+      fun StringBuilder.appendRegexEscaped(s: String) = append(Regex.escape(s))
+      val pattern = buildString {
+        appendRegexEscaped("//")
+        append("""\s*""")
+        appendRegexEscaped("""ReplaceLinesBelow(numLines=""")
+        append("""\s*(\d+)\s*,\s*""")
+        appendRegexEscaped("""replacementId=""")
+        append("""(\w+)""")
+        appendRegexEscaped(""")""")
+      }
+      Regex(pattern)
+    }
+
+    fun getGeneratedFileWarningLines(srcFile: File) =
+      listOf(
+        "/".repeat(80),
+        "// WARNING: THIS FILE IS GENERATED FROM ${srcFile.name}",
+        "// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN",
+        "// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:",
+        "// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources",
+        "/".repeat(80),
+      )
+  }
+}
+
+fun generateLocalDateSerializerUnitTest(
+  srcFile: File,
+  classNameUnderTest: String,
+  localDateFullyQualifiedClassName: String,
+  localDateFactoryCall: String,
+  logger: Logger,
+) {
+  logger.info("Reading {}", srcFile.absolutePath)
+  val transformer = TextLinesTransformer(srcFile.readLines(Charsets.UTF_8))
+
+  val linesByReplacementId =
+    mapOf(
+      "CoerceDayOfMonthIntoValidRangeFor" to
+        listOf(
+          "fun Int.coerceDayOfMonthIntoValidRangeFor(month: Int, year: Int): Int {",
+          "  val monthObject = org.threeten.bp.Month.of(month)",
+          "  val yearObject = org.threeten.bp.Year.of(year)",
+          "  val dayRange = monthObject.dayRangeInYear(yearObject)",
+          "  return coerceIn(dayRange)",
+          "}",
+        ),
+      "LocalDateSample" to
+        listOf(
+          "val coercedDayInt = dayInt.coerceDayOfMonthIntoValidRangeFor(month=monthInt, year=yearInt)",
+          "$localDateFullyQualifiedClassName$localDateFactoryCall(yearInt, monthInt, coercedDayInt)",
+        ),
+    )
+
+  val generatedFileWarningLines = TextLinesTransformer.getGeneratedFileWarningLines(srcFile)
+
+  transformer.run {
+    atLineThatStartsWith("import ")
+      .insertAbove("import com.google.firebase.dataconnect.testutil.dayRangeInYear")
+    removeLine("import com.google.firebase.dataconnect.LocalDate")
+    replaceWord("LocalDate", localDateFullyQualifiedClassName) { !it.contains('`') }
+    replaceText("LocalDateSerializer", classNameUnderTest)
+    replaceText(
+      "val monthString = month.toZeroPaddedString(monthPadding)",
+      "val monthString = month.value.toZeroPaddedString(monthPadding)"
+    )
+    replaceText(
+      "val dayString = day.toZeroPaddedString(dayPadding)",
+      "val dayString = dayOfMonth.toZeroPaddedString(dayPadding)"
+    )
+    replaceText(
+      "year: Arb<Int> = intWithEvenNumDigitsDistribution()",
+      "year: Arb<Int> = intWithEvenNumDigitsDistribution(java.time.Year.MIN_VALUE..java.time.Year.MAX_VALUE)"
+    )
+    replaceText(
+      "month: Arb<Int> = intWithEvenNumDigitsDistribution()",
+      "month: Arb<Int> = intWithEvenNumDigitsDistribution(1..12)"
+    )
+    replaceText(
+      "day: Arb<Int> = intWithEvenNumDigitsDistribution()",
+      "day: Arb<Int> = intWithEvenNumDigitsDistribution(1..31)"
+    )
+    applyReplacements(linesByReplacementId)
+
+    atLineThatStartsWith("package ")
+      .deleteLinesAboveThatStartWith("//")
+      .insertAbove(generatedFileWarningLines)
+
+    atLineThatStartsWith("class ")
+      .deleteLinesAboveThatStartWith("//")
+      .insertAbove(generatedFileWarningLines)
+  }
+
+  val destFile = File(srcFile.parentFile, "${classNameUnderTest}UnitTest.kt")
+  logger.info("Writing {}", destFile.absolutePath)
+  destFile.writeText(transformer.lines.joinToString("\n"))
+}
+
+fun generateLocalDateSerializerIntegrationTest(
+  srcFile: File,
+  destClassName: String,
+  localDateFullyQualifiedClassName: String,
+  localDateFactoryCall: String,
+  convertFromDataConnectLocalDateFunctionName: String,
+  serializerClassName: String,
+  logger: Logger,
+) {
+  logger.info("Reading {}", srcFile.absolutePath)
+  val transformer = TextLinesTransformer(srcFile.readLines(Charsets.UTF_8))
+
+  val generatedFileWarningLines = TextLinesTransformer.getGeneratedFileWarningLines(srcFile)
+
+  transformer.run {
+    removeLine("import com.google.firebase.dataconnect.LocalDate")
+    replaceLine(
+      "@file:UseSerializers(UUIDSerializer::class)",
+      "@file:UseSerializers(UUIDSerializer::class, $serializerClassName::class)"
+    )
+    atLineThatStartsWith("import ")
+      .insertAbove("import com.google.firebase.dataconnect.serializers.$serializerClassName")
+      .insertAbove("import io.kotest.property.arbitrary.map")
+    replaceWord("LocalDate", localDateFullyQualifiedClassName) { !it.contains('`') }
+    replaceWord("LocalDateIntegrationTest", destClassName)
+    replaceWord(
+      "Arb.dataConnect.localDate()",
+      "Arb.dataConnect.localDate().map{it.$convertFromDataConnectLocalDateFunctionName()}"
+    )
+    replaceRegex("""\?\.date(\W|$)""", "?.date?.$convertFromDataConnectLocalDateFunctionName()$1")
+    replaceRegex(
+      """([^?])\.date(\W|${'$'})""",
+      "$1.date.$convertFromDataConnectLocalDateFunctionName()$2"
+    )
+    replaceText(
+      "$localDateFullyQualifiedClassName(",
+      "$localDateFullyQualifiedClassName$localDateFactoryCall("
+    )
+
+    atLineThatStartsWith("package ")
+      .deleteLinesAboveThatStartWith("//")
+      .insertAbove(generatedFileWarningLines)
+
+    atLineThatStartsWith("class ")
+      .deleteLinesAboveThatStartWith("//")
+      .insertAbove(generatedFileWarningLines)
+  }
+
+  val destFile = File(srcFile.parentFile, "$destClassName.kt")
+  logger.info("Writing {}", destFile.absolutePath)
+  destFile.writeText(transformer.lines.joinToString("\n"))
+}
+
+tasks.register("generateDataConnectUnitTestingSources") {
+  val dir = file("src/test/kotlin/com/google/firebase/dataconnect/serializers")
+  val srcFile = File(dir, "LocalDateSerializerUnitTest.kt")
+  doLast {
+    generateLocalDateSerializerUnitTest(
+      srcFile = srcFile,
+      classNameUnderTest = "JavaTimeLocalDateSerializer",
+      localDateFullyQualifiedClassName = "java.time.LocalDate",
+      localDateFactoryCall = ".of",
+      logger = logger,
+    )
+    generateLocalDateSerializerUnitTest(
+      srcFile = srcFile,
+      classNameUnderTest = "KotlinxDatetimeLocalDateSerializer",
+      localDateFullyQualifiedClassName = "kotlinx.datetime.LocalDate",
+      localDateFactoryCall = "",
+      logger = logger,
+    )
+  }
+}
+
+tasks.register("generateDataConnectIntegrationTestingSources") {
+  val dir = file("src/androidTest/kotlin/com/google/firebase/dataconnect")
+  val srcFile = File(dir, "LocalDateIntegrationTest.kt")
+  doLast {
+    generateLocalDateSerializerIntegrationTest(
+      srcFile = srcFile,
+      destClassName = "JavaTimeLocalDateIntegrationTest",
+      localDateFullyQualifiedClassName = "java.time.LocalDate",
+      localDateFactoryCall = ".of",
+      convertFromDataConnectLocalDateFunctionName = "toJavaLocalDate",
+      serializerClassName = "JavaTimeLocalDateSerializer",
+      logger = logger,
+    )
+    generateLocalDateSerializerIntegrationTest(
+      srcFile = srcFile,
+      destClassName = "KotlinxDatetimeLocalDateIntegrationTest",
+      localDateFullyQualifiedClassName = "kotlinx.datetime.LocalDate",
+      localDateFactoryCall = "",
+      convertFromDataConnectLocalDateFunctionName = "toKotlinxLocalDate",
+      serializerClassName = "KotlinxDatetimeLocalDateSerializer",
+      logger = logger,
+    )
+  }
+}
+
+tasks.register("generateDataConnectTestingSources") {
+  dependsOn("generateDataConnectUnitTestingSources")
+  dependsOn("generateDataConnectIntegrationTestingSources")
+}
diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
index e99238cf00e..41622330084 100644
--- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/JavaTimeLocalDateIntegrationTest.kt
@@ -18,10 +18,10 @@
 @file:UseSerializers(UUIDSerializer::class, JavaTimeLocalDateSerializer::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect
 
@@ -69,10 +69,10 @@ import kotlinx.serialization.serializer
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO LocalDateIntegrationTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class JavaTimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() {
 
diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
index a4ea009a7c7..0b5bd5e7ec0 100644
--- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/KotlinxDatetimeLocalDateIntegrationTest.kt
@@ -18,10 +18,10 @@
 @file:UseSerializers(UUIDSerializer::class, KotlinxDatetimeLocalDateSerializer::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect
 
@@ -69,10 +69,10 @@ import kotlinx.serialization.serializer
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateIntegrationTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateIntegrationTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateIntegrationTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class KotlinxDatetimeLocalDateIntegrationTest : DataConnectIntegrationTestBase() {
 
diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
index 490fea04961..10631084900 100644
--- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
+++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/LocalDateIntegrationTest.kt
@@ -18,10 +18,11 @@
 @file:UseSerializers(UUIDSerializer::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and
-// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE
-// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
-// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE
+// JavaTimeLocalDateIntegrationTest.kt and
+// KotlinxDatetimeLocalDateIntegrationTest.kt. IF ANY CHANGES MADE TO THIS FILE
+// THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect
 
@@ -67,10 +68,11 @@ import kotlinx.serialization.serializer
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED TO JavaTimeLocalDateIntegrationTest.kt and
-// KotlinxDatetimeLocalDateIntegrationTest.kt AND ADAPTED TO TEST THE
-// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
-// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE
+// JavaTimeLocalDateIntegrationTest.kt and
+// KotlinxDatetimeLocalDateIntegrationTest.kt. IF ANY CHANGES MADE TO THIS FILE
+// THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class LocalDateIntegrationTest : DataConnectIntegrationTestBase() {
 
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
index 4b46994985f..9bd34f3a3f8 100644
--- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/JavaTimeLocalDateSerializerUnitTest.kt
@@ -16,10 +16,10 @@
 @file:OptIn(ExperimentalKotest::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect.serializers
 
@@ -63,10 +63,10 @@ import kotlinx.serialization.encoding.Decoder
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO KotlinxDatetimeLocalDateSerializerUnitTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class JavaTimeLocalDateSerializerUnitTest {
 
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt
index 3ab96a648a9..d3d775e7c9f 100644
--- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/KotlinxDatetimeLocalDateSerializerUnitTest.kt
@@ -16,10 +16,10 @@
 @file:OptIn(ExperimentalKotest::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect.serializers
 
@@ -63,10 +63,10 @@ import kotlinx.serialization.encoding.Decoder
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED AND ADAPTED FROM LocalDateSerializerUnitTest.kt
-// MAKE SURE THAT ANY CHANGES TO THIS FILE ARE BACKPORTED TO
-// LocalDateIntegrationTest.kt AND PORTED TO JavaTimeLocalDateSerializerUnitTest.kt,
-// if appropriate.
+// WARNING: THIS FILE IS GENERATED FROM LocalDateSerializerUnitTest.kt
+// DO NOT MODIFY THIS FILE BY HAND BECAUSE MANUAL CHANGES WILL GET OVERWRITTEN
+// THE NEXT TIME THAT THIS FILE IS REGENERATED. TO REGENERATE THIS FILE, RUN:
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class KotlinxDatetimeLocalDateSerializerUnitTest {
 
diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
index 6bc0c45d3bc..1b256800ee2 100644
--- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
+++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/LocalDateSerializerUnitTest.kt
@@ -16,10 +16,11 @@
 @file:OptIn(ExperimentalKotest::class)
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and
-// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE
-// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
-// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE
+// JavaTimeLocalDateSerializerUnitTest.kt and
+// KotlinxDatetimeLocalDateSerializerUnitTest.kt. IF ANY CHANGES MADE TO THIS
+// FILE THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 package com.google.firebase.dataconnect.serializers
 
@@ -63,10 +64,11 @@ import kotlinx.serialization.encoding.Decoder
 import org.junit.Test
 
 ////////////////////////////////////////////////////////////////////////////////
-// THIS FILE WAS COPIED TO JavaTimeLocalDateSerializerUnitTest.kt and
-// KotlinxDatetimeLocalDateSerializerUnitTest.kt AND ADAPTED TO TEST THE
-// CORRESPONDING IMPLEMENTATIONS OF LocalDate. ANY CHANGES MADE TO THIS FILE
-// MUST ALSO BE PORTED TO THOSE OTHER FILES, IF APPROPRIATE.
+// NOTE: THIS FILE IS USED AS A TEMPLATE TO GENERATE
+// JavaTimeLocalDateSerializerUnitTest.kt and
+// KotlinxDatetimeLocalDateSerializerUnitTest.kt. IF ANY CHANGES MADE TO THIS
+// FILE THEN THOSE TWO FILES SHOULD ALSO BE REGENERATED BY RUNNING
+// ./gradlew :firebase-dataconnect:generateDataConnectTestingSources
 ////////////////////////////////////////////////////////////////////////////////
 class LocalDateSerializerUnitTest {
 
@@ -151,14 +153,22 @@ class LocalDateSerializerUnitTest {
       month: Arb<Int> = intWithEvenNumDigitsDistribution(),
       day: Arb<Int> = intWithEvenNumDigitsDistribution(),
     ): Arb<LocalDate> {
+      // ReplaceLinesBelow(numLines=0, replacementId=CoerceDayOfMonthIntoValidRangeFor)
       return arbitrary(
         edgecaseFn = { rs ->
           val yearInt = if (rs.random.nextBoolean()) year.next(rs) else year.edgecase(rs)!!
           val monthInt = if (rs.random.nextBoolean()) month.next(rs) else month.edgecase(rs)!!
           val dayInt = if (rs.random.nextBoolean()) day.next(rs) else day.edgecase(rs)!!
+          // ReplaceLinesBelow(numLines=1, replacementId=LocalDateSample)
           LocalDate(year = yearInt, month = monthInt, day = dayInt)
         },
-        sampleFn = { LocalDate(year = year.bind(), month = month.bind(), day = day.bind()) }
+        sampleFn = {
+          val yearInt = year.bind()
+          val monthInt = month.bind()
+          val dayInt = day.bind()
+          // ReplaceLinesBelow(numLines=1, replacementId=LocalDateSample)
+          LocalDate(year = yearInt, month = monthInt, day = dayInt)
+        }
       )
     }
 

From 02d17557702a1a0cea3a35b52a562df625272d8b Mon Sep 17 00:00:00 2001
From: Denver Coneybeare <dconeybe@google.com>
Date: Thu, 12 Dec 2024 22:12:26 +0000
Subject: [PATCH 13/13] emulator.sh updated to use firebase-tools

---
 firebase-dataconnect/emulator/emulator.sh | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/firebase-dataconnect/emulator/emulator.sh b/firebase-dataconnect/emulator/emulator.sh
index 68ccf3331a7..f27b6f2f6fe 100755
--- a/firebase-dataconnect/emulator/emulator.sh
+++ b/firebase-dataconnect/emulator/emulator.sh
@@ -24,8 +24,10 @@ readonly FIREBASE_ARGS=(
   firebase
   --debug
   emulators:start
-  --only auth,dataconnect
 )
 
-echo "[$0] Running command: ${FIREBASE_ARGS[*]}"
+set -x
+
+export FIREBASE_DATACONNECT_POSTGRESQL_STRING=postgresql://postgres:postgres@localhost:5432?sslmode=disable
+export DATACONNECT_EMULATOR_BINARY_PATH="${SELF_DIR}/cli"
 exec "${FIREBASE_ARGS[@]}"