diff --git a/sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.scala b/sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.scala index 5a64e065eac4..a354cee66efc 100644 --- a/sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.scala +++ b/sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.scala @@ -9,4 +9,9 @@ private[cosmos] abstract class CosmosItemSerializerNoExceptionWrapping extends C .CosmosItemSerializerHelper .getCosmosItemSerializerAccessor .setShouldWrapSerializationExceptions(this, false) + + ImplementationBridgeHelpers + .CosmosItemSerializerHelper + .getCosmosItemSerializerAccessor + .setCanSerialize(this, false) } diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.java index a2c15a5af177..7a79068bba7c 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerNoExceptionWrapping.java @@ -14,5 +14,10 @@ public CosmosItemSerializerNoExceptionWrapping() { .CosmosItemSerializerHelper .getCosmosItemSerializerAccessor() .setShouldWrapSerializationExceptions(this, false); + + ImplementationBridgeHelpers + .CosmosItemSerializerHelper + .getCosmosItemSerializerAccessor() + .setCanSerialize(this, false); } } diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerTest.java index b2612fa7f152..722da5ad28e1 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosItemSerializerTest.java @@ -28,19 +28,26 @@ import com.azure.cosmos.models.FeedRange; import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.rx.TestSuiteBase; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Factory; import org.testng.annotations.Test; +import reactor.core.publisher.Flux; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -91,7 +98,8 @@ public static Object[][] clientBuildersWithDirectSessionIncludeComputeGatewayAnd CosmosItemSerializer[] itemSerializers = new CosmosItemSerializer[] { null, CosmosItemSerializer.DEFAULT_SERIALIZER, - EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION + EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION, + BasicCustomItemSerializer.INSTANCE }; List providers = new ArrayList<>(); @@ -199,13 +207,15 @@ public String resolveTestNameSuffix(Object[] row) { ? useTrackingIdForCreateAndReplace ? "WriteRetriesWithTrackingId|" : "WriteRetriesNoTrackingId|" : "NoWriteRetries|"; - CosmosItemSerializer requestOptionsSerializer = (CosmosItemSerializer) row[0]; - if (requestOptionsSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { - prefix += "RequestOptions_DEFAULT"; - } else if (requestOptionsSerializer == null) { - prefix += "RequestOptions_NULL"; - } else { - prefix += "RequestOptions_" + requestOptionsSerializer.getClass().getSimpleName(); + if (row != null && row.length > 0) { + CosmosItemSerializer requestOptionsSerializer = (CosmosItemSerializer) row[0]; + if (requestOptionsSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + prefix += "RequestOptions_DEFAULT"; + } else if (requestOptionsSerializer == null) { + prefix += "RequestOptions_NULL"; + } else { + prefix += "RequestOptions_" + requestOptionsSerializer.getClass().getSimpleName(); + } } if (this.getClientBuilder().getCustomItemSerializer() == null) { @@ -319,11 +329,11 @@ private void runPointOperationAndQueryTestCase( Class classType) { boolean useEnvelopeWrapper = - requestLevelSerializer == EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION + requestLevelSerializer instanceof EnvelopWrappingItemSerializer || (requestLevelSerializer == null && this.getClientBuilder() - .getCustomItemSerializer() == EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION); - if (requestLevelSerializer == EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION + .getCustomItemSerializer() instanceof EnvelopWrappingItemSerializer); + if (requestLevelSerializer instanceof EnvelopWrappingItemSerializer && isContentOnWriteEnabled && nonIdempotentWriteRetriesEnabled && useTrackingIdForCreateAndReplace) { @@ -693,6 +703,376 @@ public void handleCustomDeserializationExceptionObjectNode(CosmosItemSerializer } } + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithOrderByAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + String pkValue = UUID.randomUUID().toString(); + List createdIds = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + String id = String.format("%s_%03d", pkValue, i); + TestDocument doc = TestDocument.create(id, pkValue); + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(pkValue), requestOptions); + createdIds.add(id); + } + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + List results = container + .queryItems( + "SELECT * FROM c WHERE c.mypk = '" + pkValue + "' ORDER BY c.id ASC", + queryRequestOptions, + TestDocument.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(3); + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).id).isEqualTo(createdIds.get(i)); + assertThat(results.get(i).mypk).isEqualTo(pkValue); + } + } finally { + for (String id : createdIds) { + try { + container.deleteItem(id, new PartitionKey(pkValue), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithAggregateAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer; + if (isEnvelopeWrapper) { + return; + } + + String pkValue = UUID.randomUUID().toString(); + List createdIds = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + String id = UUID.randomUUID().toString(); + TestDocument doc = TestDocument.create(id, pkValue); + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(pkValue), requestOptions); + createdIds.add(id); + } + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + // SELECT COUNT(1) returns an object (e.g. {"$1": 3}) rather than a scalar. + // Custom serializers work with Map which is object-level + // deserialization - they cannot handle scalar VALUE queries like + // SELECT VALUE COUNT(1) because the aggregate pipeline wraps the scalar + // in a {"_value": N} Document that can't be deserialized as Integer. + List results = container + .queryItems( + "SELECT COUNT(1) FROM c WHERE c.mypk = '" + pkValue + "'", + queryRequestOptions, + ObjectNode.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertThat(results.get(0).get("$1").asInt()).isEqualTo(3); + } finally { + for (String id : createdIds) { + try { + container.deleteItem(id, new PartitionKey(pkValue), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithDistinctAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer; + if (isEnvelopeWrapper) { + return; + } + + String pkValue = UUID.randomUUID().toString(); + List createdIds = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + String id = UUID.randomUUID().toString(); + TestDocument doc = TestDocument.create(id, pkValue); + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(pkValue), requestOptions); + createdIds.add(id); + } + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + List results = container + .queryItems( + "SELECT DISTINCT c.mypk FROM c WHERE c.mypk = '" + pkValue + "'", + queryRequestOptions, + ObjectNode.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertThat(results.get(0).get("mypk").asText()).isEqualTo(pkValue); + } finally { + for (String id : createdIds) { + try { + container.deleteItem(id, new PartitionKey(pkValue), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithGroupByAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer; + if (isEnvelopeWrapper) { + return; + } + + String pkValue = UUID.randomUUID().toString(); + List createdIds = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + String id = UUID.randomUUID().toString(); + TestDocument doc = TestDocument.create(id, pkValue); + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(pkValue), requestOptions); + createdIds.add(id); + } + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + List results = container + .queryItems( + "SELECT c.mypk, COUNT(1) as cnt FROM c WHERE c.mypk = '" + pkValue + "' GROUP BY c.mypk", + queryRequestOptions, + ObjectNode.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertThat(results.get(0).get("mypk").asText()).isEqualTo(pkValue); + assertThat(results.get(0).get("cnt").asInt()).isEqualTo(3); + } finally { + for (String id : createdIds) { + try { + container.deleteItem(id, new PartitionKey(pkValue), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithSqlParameterAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + String id = UUID.randomUUID().toString(); + TestDocument doc = TestDocument.create(id); + try { + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(id), requestOptions); + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + SqlQuerySpec querySpec = new SqlQuerySpec( + "SELECT * FROM c WHERE c.id = @id", + new SqlParameter("@id", id)); + + List results = container + .queryItems(querySpec, queryRequestOptions, TestDocument.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertSameDocument(doc, results.get(0)); + } finally { + try { + container.deleteItem(id, new PartitionKey(id), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithSqlParameterDateTimeAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer; + if (isEnvelopeWrapper) { + return; + } + + // This test validates that when a custom serializer changes how values are + // stored (e.g. Instant as ISO-8601 string instead of a timestamp number), + // SqlParameter correctly applies the same serializer so that query filters + // match the stored representation. + String id = UUID.randomUUID().toString(); + String pkValue = id; + Instant createdAt = Instant.parse("2026-03-15T10:30:00Z"); + TestDocumentWithTimestamp doc = new TestDocumentWithTimestamp(); + doc.id = id; + doc.mypk = pkValue; + doc.createdAt = createdAt; + doc.description = "test-datetime-serialization"; + + try { + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + container.createItem(doc, new PartitionKey(pkValue), requestOptions); + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + // Query using SqlParameter with the same Instant value — the custom serializer + // must serialize the parameter the same way as the document field. + SqlQuerySpec querySpec = new SqlQuerySpec( + "SELECT * FROM c WHERE c.createdAt = @createdAt AND c.id = @id", + new SqlParameter("@createdAt", createdAt), + new SqlParameter("@id", id)); + + List results = container + .queryItems(querySpec, queryRequestOptions, TestDocumentWithTimestamp.class) + .stream().collect(Collectors.toList()); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertThat(results.get(0).id).isEqualTo(id); + assertThat(results.get(0).createdAt).isEqualTo(createdAt); + assertThat(results.get(0).description).isEqualTo("test-datetime-serialization"); + } finally { + try { + container.deleteItem(id, new PartitionKey(pkValue), new CosmosItemRequestOptions()); + } catch (Exception ignored) { } + } + } + + @Test(groups = { "fast", "emulator" }, timeOut = TIMEOUT * 1000000) + public void queryWithConcurrentSqlQuerySpecReuseAndCustomSerializer() { + CosmosItemSerializer clientSerializer = this.getClientBuilder().getCustomItemSerializer(); + if (clientSerializer == null || clientSerializer == CosmosItemSerializer.DEFAULT_SERIALIZER) { + return; + } + + boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer; + if (isEnvelopeWrapper) { + return; + } + + CosmosAsyncClient asyncClient = client.asyncClient(); + CosmosAsyncContainer asyncContainer = asyncClient + .getDatabase(container.asyncContainer.getDatabase().getId()) + .getContainer(container.asyncContainer.getId()); + + // Create multiple documents with Instant fields + String pkValue = UUID.randomUUID().toString(); + int docCount = 5; + List createdDocs = new ArrayList<>(); + Instant createdAt = Instant.parse("2026-03-15T10:30:00Z"); + + try { + CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions() + .setCustomItemSerializer(clientSerializer); + + for (int i = 0; i < docCount; i++) { + TestDocumentWithTimestamp doc = new TestDocumentWithTimestamp(); + doc.id = UUID.randomUUID().toString(); + doc.mypk = pkValue; + doc.createdAt = createdAt; + doc.description = "concurrent-test-" + i; + asyncContainer.createItem(doc, new PartitionKey(pkValue), requestOptions).block(); + createdDocs.add(doc); + } + + // Build a single shared SqlQuerySpec — this is the scenario under test: + // the same instance is reused across concurrent queries. + SqlQuerySpec sharedQuerySpec = new SqlQuerySpec( + "SELECT * FROM c WHERE c.createdAt = @createdAt AND c.mypk = @pk", + new SqlParameter("@createdAt", createdAt), + new SqlParameter("@pk", pkValue)); + + // Capture original parameter values before concurrent execution + List originalParams = sharedQuerySpec.getParameters(); + Object originalCreatedAtValue = originalParams.get(0).getValue(Object.class); + Object originalPkValue = originalParams.get(1).getValue(Object.class); + + CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions() + .setCustomItemSerializer(clientSerializer); + + int concurrentQueries = 10; + + List> allResults = + Flux.range(0, concurrentQueries) + .flatMap(i -> + asyncContainer + .queryItems(sharedQuerySpec, queryRequestOptions, TestDocumentWithTimestamp.class) + .byPage() + .flatMapIterable(FeedResponse::getResults) + .collectList(), + concurrentQueries) + .collectList() + .block(); + + assertThat(allResults).isNotNull(); + assertThat(allResults).hasSize(concurrentQueries); + for (List results : allResults) { + assertThat(results).hasSize(docCount); + for (TestDocumentWithTimestamp result : results) { + assertThat(result.createdAt).isEqualTo(createdAt); + assertThat(result.mypk).isEqualTo(pkValue); + } + } + + // Verify the original SqlQuerySpec parameters are unmodified + assertThat(sharedQuerySpec.getParameters()).hasSize(2); + assertThat(sharedQuerySpec.getParameters().get(0).getValue(Object.class)) + .isEqualTo(originalCreatedAtValue); + assertThat(sharedQuerySpec.getParameters().get(1).getValue(Object.class)) + .isEqualTo(originalPkValue); + } finally { + for (TestDocumentWithTimestamp doc : createdDocs) { + try { + asyncContainer.deleteItem(doc.id, new PartitionKey(pkValue), new CosmosItemRequestOptions()).block(); + } catch (Exception ignored) { } + } + } + } + private void runBatchAndChangeFeedTestCase( Function docGenerator, CosmosItemSerializer requestLevelSerializer, @@ -923,6 +1303,13 @@ private static class TestDocumentWithNullableField { public String nullableField; } + private static class TestDocumentWithTimestamp { + public String id; + public String mypk; + public Instant createdAt; + public String description; + } + private static class TestDocumentWrappedInEnvelope { public String id; @@ -959,6 +1346,13 @@ public Map serialize(T item) { } Map unwrappedJsonTree = CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item); + + // Non-document values (e.g., SQL parameter values like strings or numbers) + // don't have "id" — pass them through without wrapping. + if (!unwrappedJsonTree.containsKey("id")) { + return unwrappedJsonTree; + } + if (unwrappedJsonTree.containsKey("wrappedContent")) { throw new IllegalStateException("Double wrapping"); } @@ -1018,4 +1412,55 @@ public T deserialize(Map jsonNodeMap, Class classType) { classType); } } + + /** + * A simple custom item serializer that mirrors the real-world use case from + * issue #45521. + * Uses a custom ObjectMapper with different settings (e.g., dates as ISO strings) + * without transforming the document structure (no wrapping/unwrapping). + */ + @SuppressWarnings("unchecked") + private static class BasicCustomItemSerializer extends CosmosItemSerializer { + public static final BasicCustomItemSerializer INSTANCE = new BasicCustomItemSerializer(); + + private static final ObjectMapper customMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private BasicCustomItemSerializer() { + } + + @Override + public Map serialize(T item) { + if (item == null) { + return null; + } + + JsonNode jsonNode = customMapper.convertValue(item, JsonNode.class); + if (jsonNode == null) { + return null; + } + + if (jsonNode.isObject()) { + return customMapper.convertValue(jsonNode, Map.class); + } + + // For scalar values (e.g. Instant, String, numbers), return a single-entry + // map using the well-known primitive value key so the framework correctly + // sets it as a scalar in the JSON property bag. + Map primitiveMap = new HashMap<>(); + primitiveMap.put("__primitive_json-node_value__", customMapper.convertValue(jsonNode, Object.class)); + return primitiveMap; + } + + @Override + public T deserialize(Map jsonNodeMap, Class classType) { + if (jsonNodeMap == null) { + return null; + } + return customMapper.convertValue(jsonNodeMap, classType); + } + } } diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index e8ea564fab7b..8b19b3c5e700 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -10,6 +10,8 @@ * Fixed an issue where the throughput control `throughputQueryMono` was always subscribed even when `targetThroughput` is used (not `targetThroughputThreshold`), causing unnecessary `throughputSettings/read` permission requirement for AAD principals. - See [PR 48800](https://github.com/Azure/azure-sdk-for-java/pull/48800) * Fixed an issue where change feed with `startFrom` point-in-time returned `400` on merged partitions by enabling the `CHANGE_FEED_WITH_START_TIME_POST_MERGE` SDK capability. * Fixed JVM `` deadlock when multiple threads concurrently trigger Cosmos SDK class loading for the first time. - See [PR 48689](https://github.com/Azure/azure-sdk-for-java/pull/48689) +* Fixed an issue where `CustomItemSerializer` was incorrectly applied to internal SDK query pipeline structures (e.g., `OrderByRowResult`, `Document`), causing deserialization failures in ORDER BY, GROUP BY, aggregate, DISTINCT, and hybrid search queries. - See [PR 48811](https://github.com/Azure/azure-sdk-for-java/pull/48811) +* Fixed an issue where `SqlParameter` ignored the configured `CustomItemSerializer`, always using the internal default serializer instead. - See [PR 48811](https://github.com/Azure/azure-sdk-for-java/pull/48811) #### Other Changes diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosItemSerializer.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosItemSerializer.java index a81ed9fa15e2..1a6a96511a77 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosItemSerializer.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosItemSerializer.java @@ -3,27 +3,16 @@ package com.azure.cosmos; -import com.azure.cosmos.implementation.ApiType; import com.azure.cosmos.implementation.BadRequestException; import com.azure.cosmos.implementation.Configs; -import com.azure.cosmos.implementation.ConnectionPolicy; -import com.azure.cosmos.implementation.CosmosClientMetadataCachesSnapshot; import com.azure.cosmos.implementation.DefaultCosmosItemSerializer; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.ImplementationBridgeHelpers; -import com.azure.cosmos.implementation.JsonSerializable; -import com.azure.cosmos.implementation.ObjectNodeMap; -import com.azure.cosmos.implementation.PrimitiveJsonNodeMap; import com.azure.cosmos.implementation.Utils; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.IOException; import java.util.Map; -import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; - /** * The {@link CosmosItemSerializer} allows customizing the serialization of Cosmos Items - either to transform payload (for * example wrap/unwrap in custom envelopes) or use custom serialization settings or json serializer stacks. @@ -47,6 +36,7 @@ public abstract class CosmosItemSerializer { new DefaultCosmosItemSerializer(Utils.getSimpleObjectMapper()); private boolean shouldWrapSerializationExceptions; + private boolean canSerialize; private ObjectMapper mapper = Utils.getSimpleObjectMapper(); @@ -55,6 +45,7 @@ public abstract class CosmosItemSerializer { */ protected CosmosItemSerializer() { this.shouldWrapSerializationExceptions = true; + this.canSerialize = true; } /** @@ -139,6 +130,14 @@ void setShouldWrapSerializationExceptions(boolean enabled) { this.shouldWrapSerializationExceptions = enabled; } + void setCanSerialize(boolean canSerialize) { + this.canSerialize = canSerialize; + } + + boolean canSerialize() { + return this.canSerialize; + } + /////////////////////////////////////////////////////////////////////////////////////////// // the following helper/accessor only helps to access this class outside of this package.// @@ -172,6 +171,15 @@ public ObjectMapper getItemObjectMapper(CosmosItemSerializer serializer) { } @Override + public boolean canSerialize(CosmosItemSerializer serializer) { + return serializer.canSerialize(); + } + + @Override + public void setCanSerialize(CosmosItemSerializer serializer, boolean canSerialize) { + serializer.setCanSerialize(canSerialize); + } + public CosmosItemSerializer getInternalDefaultSerializer() { return INTERNAL_DEFAULT_SERIALIZER; } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java index 10b1644fd7d2..ee98360d2af4 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java @@ -77,6 +77,7 @@ import com.azure.cosmos.models.PartitionKeyDefinition; import com.azure.cosmos.models.PriorityLevel; import com.azure.cosmos.models.ShowQueryMode; +import com.azure.cosmos.models.SqlParameter; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.util.CosmosPagedFlux; import com.azure.cosmos.util.UtilBridgeInternal; @@ -1893,6 +1894,8 @@ void setShouldWrapSerializationExceptions( void setItemObjectMapper(CosmosItemSerializer serializer, ObjectMapper mapper); ObjectMapper getItemObjectMapper(CosmosItemSerializer serializer); CosmosItemSerializer getInternalDefaultSerializer(); + void setCanSerialize(CosmosItemSerializer serializer, boolean canSerialize); + boolean canSerialize(CosmosItemSerializer serializer); } } @@ -1934,4 +1937,72 @@ ReadConsistencyStrategy getEffectiveReadConsistencyStrategy( ReadConsistencyStrategy clientLevelReadConsistencyStrategy); } } + + public static final class SqlQuerySpecHelper { + private static final AtomicReference accessor = new AtomicReference<>(); + private static final AtomicBoolean sqlQuerySpecClassLoaded = new AtomicBoolean(false); + + private SqlQuerySpecHelper() {} + + public static void setSqlQuerySpecAccessor(final SqlQuerySpecAccessor newAccessor) { + if (!accessor.compareAndSet(null, newAccessor)) { + logger.debug("SqlQuerySpecAccessor already initialized!"); + } else { + logger.debug("Setting SqlQuerySpecAccessor..."); + sqlQuerySpecClassLoaded.set(true); + } + } + + public static SqlQuerySpecAccessor getSqlQuerySpecAccessor() { + if (!sqlQuerySpecClassLoaded.get()) { + logger.debug("Initializing SqlQuerySpecAccessor..."); + initializeAllAccessors(); + } + + SqlQuerySpecAccessor snapshot = accessor.get(); + if (snapshot == null) { + logger.error("SqlQuerySpecAccessor is not initialized yet!"); + } + + return snapshot; + } + + public interface SqlQuerySpecAccessor { + void applySerializerToParameters(SqlQuerySpec sqlQuerySpec, CosmosItemSerializer serializer); + } + } + + public static final class SqlParameterHelper { + private static final AtomicReference accessor = new AtomicReference<>(); + private static final AtomicBoolean sqlParameterClassLoaded = new AtomicBoolean(false); + + private SqlParameterHelper() {} + + public static void setSqlParameterAccessor(final SqlParameterAccessor newAccessor) { + if (!accessor.compareAndSet(null, newAccessor)) { + logger.debug("SqlParameterAccessor already initialized!"); + } else { + logger.debug("Setting SqlParameterAccessor..."); + sqlParameterClassLoaded.set(true); + } + } + + public static SqlParameterAccessor getSqlParameterAccessor() { + if (!sqlParameterClassLoaded.get()) { + logger.debug("Initializing SqlParameterAccessor..."); + initializeAllAccessors(); + } + + SqlParameterAccessor snapshot = accessor.get(); + if (snapshot == null) { + logger.error("SqlParameterAccessor is not initialized yet!"); + } + + return snapshot; + } + + public interface SqlParameterAccessor { + SqlParameter createCopy(SqlParameter original); + } + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryExecutionContext.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryExecutionContext.java index ef434447d376..21eb8276de2f 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryExecutionContext.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryExecutionContext.java @@ -63,19 +63,18 @@ private static BiFunction, Flux { CosmosQueryRequestOptions orderByCosmosQueryRequestOptions = - qryOptAccessor().clone(requestOptions); + cloneOptionsForInternalPipeline(requestOptions); if (queryInfo.hasNonStreamingOrderBy()) { if (continuationToken != null) { throw new NonStreamingOrderByBadRequestException( HttpConstants.StatusCodes.BADREQUEST, "Can not use a continuation token for a vector search query"); } - qryOptAccessor().getImpl(orderByCosmosQueryRequestOptions).setCustomItemSerializer(null); + documentQueryParams.setCosmosQueryRequestOptions(orderByCosmosQueryRequestOptions); return NonStreamingOrderByDocumentQueryExecutionContext.createAsync(diagnosticsClientContext, client, documentQueryParams, collection); } else { ModelBridgeInternal.setQueryRequestOptionsContinuationToken(orderByCosmosQueryRequestOptions, continuationToken); - qryOptAccessor().getImpl(orderByCosmosQueryRequestOptions).setCustomItemSerializer(null); documentQueryParams.setCosmosQueryRequestOptions(orderByCosmosQueryRequestOptions); return OrderByDocumentQueryExecutionContext.createAsync(diagnosticsClientContext, client, documentQueryParams, collection); } @@ -83,9 +82,7 @@ private static BiFunction, Flux { - CosmosQueryRequestOptions parallelCosmosQueryRequestOptions = - qryOptAccessor().clone(requestOptions); - qryOptAccessor().getImpl(parallelCosmosQueryRequestOptions).setCustomItemSerializer(null); + CosmosQueryRequestOptions parallelCosmosQueryRequestOptions = cloneOptionsForInternalPipeline(requestOptions); ModelBridgeInternal.setQueryRequestOptionsContinuationToken(parallelCosmosQueryRequestOptions, continuationToken); documentQueryParams.setCosmosQueryRequestOptions(parallelCosmosQueryRequestOptions); @@ -122,7 +119,7 @@ private static BiFunction, Flux { CosmosQueryRequestOptions orderByCosmosQueryRequestOptions = - qryOptAccessor().clone(requestOptions); + cloneOptionsForInternalPipeline(requestOptions); documentQueryParams.setCosmosQueryRequestOptions(orderByCosmosQueryRequestOptions); return HybridSearchDocumentQueryExecutionContext.createAsync(diagnosticsClientContext, client, documentQueryParams, collection); @@ -273,4 +270,24 @@ public Flux> executeAsync() { ) ); } + + /** + * Clones query request options and neutralizes the custom item serializer to + * DEFAULT_SERIALIZER. Internal pipeline stages must always use the default + * serializer; custom serialization is applied at the top-level pipeline boundary. + * + * All internal pipeline stages that work with Document/intermediate types MUST + * use this method rather than cloning options directly via + * {@code qryOptAccessor().clone(...)}. Bypassing this method would leave the + * custom serializer active in the inner pipeline, causing incorrect + * serialization of intermediate results. See PR #48811 for context. + */ + private static CosmosQueryRequestOptions cloneOptionsForInternalPipeline( + CosmosQueryRequestOptions source) { + + CosmosQueryRequestOptions cloned = qryOptAccessor().clone(source); + qryOptAccessor().getImpl(cloned) + .setCustomItemSerializer(CosmosItemSerializer.DEFAULT_SERIALIZER); + return cloned; + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryParams.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryParams.java index 4f8270688740..d5462eb6b0c2 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryParams.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedDocumentQueryParams.java @@ -21,7 +21,7 @@ public class PipelinedDocumentQueryParams { private final String collectionRid; private final ResourceType resourceTypeEnum; private final Class resourceType; - private final SqlQuerySpec query; + private SqlQuerySpec query; private final String resourceLink; private final UUID correlatedActivityId; private CosmosQueryRequestOptions cosmosQueryRequestOptions; @@ -101,6 +101,10 @@ public SqlQuerySpec getQuery() { return query; } + public void setQuery(SqlQuerySpec query) { + this.query = query; + } + public String getResourceLink() { return resourceLink; } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedQueryExecutionContextBase.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedQueryExecutionContextBase.java index 2bb1429bd9c8..23efe92e8d1e 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedQueryExecutionContextBase.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/PipelinedQueryExecutionContextBase.java @@ -5,12 +5,20 @@ import com.azure.cosmos.CosmosItemSerializer; import com.azure.cosmos.implementation.DiagnosticsClientContext; import com.azure.cosmos.implementation.DocumentCollection; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers.CosmosItemSerializerHelper.CosmosItemSerializerAccessor; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers.SqlParameterHelper.SqlParameterAccessor; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers.SqlQuerySpecHelper.SqlQuerySpecAccessor; import com.azure.cosmos.implementation.Utils; import com.azure.cosmos.implementation.query.hybridsearch.HybridSearchQueryInfo; import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.ModelBridgeInternal; +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; import reactor.core.publisher.Flux; +import java.util.ArrayList; +import java.util.List; import java.util.function.BiFunction; /** @@ -60,6 +68,28 @@ public static Flux> createAsync( CosmosItemSerializer candidateSerializer = client.getEffectiveItemSerializer(cosmosQueryRequestOptions); + // Apply the effective custom serializer to SqlParameter values so that + // query parameters are serialized consistently with stored document values. + // Clone the SqlQuerySpec first to avoid mutating the caller's original object, + // which could race if the same instance is reused across concurrent queries. + if (candidateSerializer != CosmosItemSerializer.DEFAULT_SERIALIZER + && cosmosItemSerializerAccessor().canSerialize(candidateSerializer)) { + SqlQuerySpec original = initParams.getQuery(); + if (original != null) { + List originalParams = original.getParameters(); + if (originalParams != null && !originalParams.isEmpty()) { + List clonedParams = new ArrayList<>(originalParams.size()); + SqlParameterAccessor paramAccessor = sqlParameterAccessor(); + for (SqlParameter p : originalParams) { + clonedParams.add(paramAccessor.createCopy(p)); + } + SqlQuerySpec clonedQuery = new SqlQuerySpec(original.getQueryText(), clonedParams); + sqlQuerySpecAccessor().applySerializerToParameters(clonedQuery, candidateSerializer); + initParams.setQuery(clonedQuery); + } + } + } + if (hybridSearchQueryInfo != null) { return PipelinedDocumentQueryExecutionContext.createHybridAsyncCore( diagnosticsClientContext, @@ -194,4 +224,16 @@ public QueryInfo getQueryInfo() { public HybridSearchQueryInfo getHybridSearchQueryInfo() { return this.hybridSearchQueryInfo; } + + private static SqlQuerySpecAccessor sqlQuerySpecAccessor() { + return ImplementationBridgeHelpers.SqlQuerySpecHelper.getSqlQuerySpecAccessor(); + } + + private static SqlParameterAccessor sqlParameterAccessor() { + return ImplementationBridgeHelpers.SqlParameterHelper.getSqlParameterAccessor(); + } + + private static CosmosItemSerializerAccessor cosmosItemSerializerAccessor() { + return ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor(); + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/QueryPlanRetriever.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/QueryPlanRetriever.java index 2b8a6216d6dc..591b601eb7c4 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/QueryPlanRetriever.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/QueryPlanRetriever.java @@ -15,6 +15,7 @@ import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.ModelBridgeInternal; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlParameter; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.implementation.BackoffRetryUtility; import com.azure.cosmos.implementation.DocumentClientRetryPolicy; @@ -26,6 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -105,7 +107,13 @@ static Mono getQueryPlanThroughGatewayAsync(Diagn resourceLink, requestHeaders); queryPlanRequest.useGatewayMode = true; - queryPlanRequest.setByteBuffer(ModelBridgeInternal.serializeJsonToByteBuffer(sqlQuerySpec)); + + // Create a defensive copy to prevent concurrent modification of the shared + // SqlQuerySpec's internal ObjectNode when multiple threads retrieve the query + // plan simultaneously. Each copy has its own property bag, avoiding the race + // condition on the non-thread-safe ObjectNode/LinkedHashMap backing store. + SqlQuerySpec querySpecCopy = copyQuerySpec(sqlQuerySpec); + queryPlanRequest.setByteBuffer(ModelBridgeInternal.serializeJsonToByteBuffer(querySpecCopy)); CosmosEndToEndOperationLatencyPolicyConfig end2EndConfig = queryOptionsAccessor() .getImpl(nonNullRequestOptions) @@ -162,4 +170,10 @@ static Mono getQueryPlanThroughGatewayAsync(Diagn return throwable; }); } + + private static SqlQuerySpec copyQuerySpec(SqlQuerySpec original) { + List params = original.getParameters(); + List copiedParams = params != null ? new ArrayList<>(params) : null; + return new SqlQuerySpec(original.getQueryText(), copiedParams); + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/ValueUnwrapCosmosItemSerializer.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/ValueUnwrapCosmosItemSerializer.java index b3a163dfab9e..7e3204e7ce61 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/ValueUnwrapCosmosItemSerializer.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/query/ValueUnwrapCosmosItemSerializer.java @@ -31,6 +31,10 @@ public static CosmosItemSerializer create(boolean shouldUnwrapValue) { private ValueUnwrapCosmosItemSerializer(boolean shouldUnwrapValue) { itemSerializerAccessor() .setShouldWrapSerializationExceptions(this, false); + ImplementationBridgeHelpers + .CosmosItemSerializerHelper + .getCosmosItemSerializerAccessor() + .setCanSerialize(this, false); this.shouldUnwrapValue = shouldUnwrapValue; } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ModelBridgeInternal.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ModelBridgeInternal.java index 152407a11e5d..c34df7c4e925 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ModelBridgeInternal.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ModelBridgeInternal.java @@ -766,5 +766,7 @@ public static void initializeAllAccessors() { CosmosClientTelemetryConfig.initialize(); CosmosContainerIdentity.initialize(); PriorityLevel.initialize(); + SqlQuerySpec.initialize(); + SqlParameter.initialize(); } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlParameter.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlParameter.java index 8ba3aa6e1c74..ceb817804cc3 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlParameter.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlParameter.java @@ -3,6 +3,9 @@ package com.azure.cosmos.models; +import com.azure.cosmos.CosmosItemSerializer; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers.CosmosItemSerializerHelper.CosmosItemSerializerAccessor; import com.azure.cosmos.implementation.JsonSerializable; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -13,6 +16,7 @@ */ public final class SqlParameter { private JsonSerializable jsonSerializable; + private Object rawValue; /** * Initializes a new instance of the SqlParameter class. @@ -29,6 +33,9 @@ public SqlParameter() { SqlParameter(ObjectNode objectNode) { this.jsonSerializable = new JsonSerializable(objectNode); + // Note: rawValue here may not preserve the original Java type (e.g. Instant) + // since it is read back from the JSON property bag after deserialization. + this.rawValue = this.jsonSerializable.getObject("value", Object.class); } /** @@ -84,6 +91,7 @@ public T getValue(Class classType) { * @return the SqlParameter. */ public SqlParameter setValue(Object value) { + this.rawValue = value; this.jsonSerializable.set( "value", value @@ -91,12 +99,63 @@ public SqlParameter setValue(Object value) { return this; } + /** + * Returns the raw value captured before serialization, when available. + * Package-private — used internally to clone parameters while preserving the + * original Java type (for example, {@code Instant}) when that value originated + * from direct Java assignment such as {@link #setValue(Object)}. + *

+ * Values populated from the JSON property bag may already have lost their + * original Java type fidelity during JSON serialization/deserialization. + */ + Object getRawValue() { + return this.rawValue; + } + + /** + * Re-serializes the parameter value using the given custom item serializer. + * This is called internally during query execution to ensure parameter values + * are serialized consistently with document values when a custom serializer is configured. + * + * @param serializer the custom item serializer to apply. + */ + void applySerializer(CosmosItemSerializer serializer) { + if (this.rawValue != null + && serializer != null + && cosmosItemSerializerAccessor().canSerialize(serializer)) { + + this.jsonSerializable.set("value", this.rawValue, serializer, true); + } + } + + /** + * Creates a copy of this SqlParameter, preserving the original raw value + * so that custom serializers can be applied to the clone independently. + * + * @return a new SqlParameter with the same name and raw value. + */ + private SqlParameter createCopy() { + return new SqlParameter(this.getName(), this.rawValue); + } + void populatePropertyBag() { this.jsonSerializable.populatePropertyBag(); } JsonSerializable getJsonSerializable() { return this.jsonSerializable; } + private static CosmosItemSerializerAccessor cosmosItemSerializerAccessor() { + return ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor(); + } + + static void initialize() { + ImplementationBridgeHelpers.SqlParameterHelper.setSqlParameterAccessor( + SqlParameter::createCopy + ); + } + + static { initialize(); } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlQuerySpec.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlQuerySpec.java index 9cec02f3237b..87d0176b9eaa 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlQuerySpec.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/SqlQuerySpec.java @@ -3,6 +3,8 @@ package com.azure.cosmos.models; +import com.azure.cosmos.CosmosItemSerializer; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; import com.azure.cosmos.implementation.JsonSerializable; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -134,5 +136,36 @@ void populatePropertyBag() { } } + /** + * Re-serializes all parameter values using the given custom item serializer. + * Called internally during query execution to ensure parameter values are serialized + * consistently with document values when a custom serializer is configured. + * + * @param serializer the custom item serializer to apply. + */ + void applySerializerToParameters(CosmosItemSerializer serializer) { + List params = this.getParameters(); + if (serializer != null && params != null) { + for (SqlParameter param : params) { + param.applySerializer(serializer); + } + } + } + JsonSerializable getJsonSerializable() { return this.jsonSerializable; } + + static void initialize() { + ImplementationBridgeHelpers.SqlQuerySpecHelper.setSqlQuerySpecAccessor( + new ImplementationBridgeHelpers.SqlQuerySpecHelper.SqlQuerySpecAccessor() { + @Override + public void applySerializerToParameters(SqlQuerySpec sqlQuerySpec, CosmosItemSerializer serializer) { + if (sqlQuerySpec != null) { + sqlQuerySpec.applySerializerToParameters(serializer); + } + } + } + ); + } + + static { initialize(); } }