diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/PartitioningQueryTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/PartitioningQueryTests.cs index c925c95d67..ed61b8fc8e 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/PartitioningQueryTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/PartitioningQueryTests.cs @@ -489,5 +489,90 @@ async Task ImplementationAsync( Assert.IsTrue(actualPartitionKeyValues.SetEquals(args.ExpectedPartitionKeyValues)); } } + + [TestMethod] + public async Task TestGeospatial() + { + string[] inputDocs = new[] + { + @"{""id"":""documentId1"",""key"":""A"",""prop"":3,""shortArray"":[{""a"":5}]}", + @"{""id"":""documentId2"",""key"":""A"",""prop"":2,""shortArray"":[{""a"":6}]}", + @"{""id"":""documentId3"",""key"":""A"",""prop"":1,""shortArray"":[{""a"":7}]}", + @"{""id"":""documentId4"",""key"":5,""prop"":3,""shortArray"":[{""a"":5}]}", + @"{""id"":""documentId5"",""key"":5,""prop"":2,""shortArray"":[{""a"":6}]}", + @"{""id"":""documentId6"",""key"":5,""prop"":1,""shortArray"":[{""a"":7}]}", + @"{""id"":""documentId10"",""prop"":3,""shortArray"":[{""a"":5}]}", + @"{""id"":""documentId11"",""prop"":2,""shortArray"":[{""a"":6}]}", + @"{""id"":""documentId12"",""prop"":1,""shortArray"":[{""a"":7}]}", + }; + + string All = "documentId4,documentId5,documentId6,documentId10,documentId11,documentId12,documentId1,documentId2,documentId3"; + string None = string.Empty; + var testVariations = new[] + { + new + { + Query = "SELECT c.id FROM c WHERE ST_DISTANCE({'type': 'Polygon', 'coordinates': [[[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]], [[20, 30], [35, 35], [30, 20], [20, 30]]]}, {'type': 'Point', 'coordinates': [30, 10]}) > 66408.034483", + Expected = new Dictionary + { + { Cosmos.GeospatialType.Geography, All }, + { Cosmos.GeospatialType.Geometry, None }, + } + }, + new + { + Query = "SELECT c.id FROM c WHERE ST_ISVALID({'type': 'Polygon', 'coordinates': [[[-1000, 1000], [1000, 1000], [1000, 4000], [-1000, 4000], [-1000, 1000]]]})", + Expected = new Dictionary + { + { Cosmos.GeospatialType.Geography, None }, + { Cosmos.GeospatialType.Geometry, All }, + } + }, + new + { + Query = "SELECT * FROM c WHERE NOT ST_WITHIN({'type': 'Point', 'coordinates': [0, 40]}, {'type':'Polygon','coordinates':[[[-60,20], [70,20], [70,70], [-60,70], [-60,20]]]})", + Expected = new Dictionary + { + { Cosmos.GeospatialType.Geography, All }, + { Cosmos.GeospatialType.Geometry, None }, + } + }, + }; + + foreach (Cosmos.GeospatialType geospatialType in new[] { Cosmos.GeospatialType.Geography, Cosmos.GeospatialType.Geometry }) + { + await this.CreateIngestQueryDeleteAsync( + ConnectionModes.Direct, + CollectionTypes.MultiPartition, + inputDocs, + ImplementationAsync, + "/key", + geospatialType: geospatialType); + + async Task ImplementationAsync(Container container, IReadOnlyList documents) + { + foreach (var testVariation in testVariations) + { + string expectedResult = string.Join(",", testVariation.Expected[geospatialType]); + + FeedIterator resultSetIterator = container.GetItemQueryIterator( + queryText: testVariation.Query, + requestOptions: new QueryRequestOptions()); + + List result = new List(); + while (resultSetIterator.HasMoreResults) + { + result.AddRange(await resultSetIterator.ReadNextAsync()); + } + + string resultDocIds = string.Join(",", result.Select(doc => doc.Id)); + Assert.AreEqual( + expectedResult, + resultDocIds, + $"{Environment.NewLine}Query failed for geospatial type '{geospatialType}'{Environment.NewLine}{testVariation.Query}"); + } + } + } + } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/QueryTestsBase.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/QueryTestsBase.cs index f40015473a..269e8448bd 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/QueryTestsBase.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Query/QueryTestsBase.cs @@ -113,12 +113,14 @@ private async Task> GetPartitionKeyRanges(Conta private async Task CreateMultiPartitionContainer( string partitionKey = "/id", - Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null) + Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { ContainerResponse containerResponse = await this.CreatePartitionedContainer( throughput: 25000, partitionKey: partitionKey, - indexingPolicy: indexingPolicy); + indexingPolicy: indexingPolicy, + geospatialType); IReadOnlyList ranges = await this.GetPartitionKeyRanges(containerResponse); Assert.IsTrue( @@ -130,12 +132,14 @@ private async Task CreateMultiPartitionContainer( private async Task CreateSinglePartitionContainer( string partitionKey = "/id", - Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null) + Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { ContainerResponse containerResponse = await this.CreatePartitionedContainer( throughput: 4000, partitionKey: partitionKey, - indexingPolicy: indexingPolicy); + indexingPolicy: indexingPolicy, + geospatialType: geospatialType); Assert.IsNotNull(containerResponse); Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); @@ -149,13 +153,15 @@ private async Task CreateSinglePartitionContainer( } private async Task CreateNonPartitionedContainerAsync( - Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null) + Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { string containerName = Guid.NewGuid().ToString() + "container"; await NonPartitionedContainerHelper.CreateNonPartitionedContainer( this.database, containerName, - indexingPolicy == null ? null : JsonConvert.SerializeObject(indexingPolicy)); + indexingPolicy == null ? null : JsonConvert.SerializeObject(indexingPolicy), + geospatialType); return this.database.GetContainer(containerName); } @@ -163,7 +169,8 @@ await NonPartitionedContainerHelper.CreateNonPartitionedContainer( private async Task CreatePartitionedContainer( int throughput, string partitionKey = "/id", - Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null) + Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { // Assert that database exists (race deletes are possible when used concurrently) ResponseMessage responseMessage = await this.database.ReadStreamAsync(); @@ -192,7 +199,8 @@ private async Task CreatePartitionedContainer( { Paths = new Collection { partitionKey }, Kind = PartitionKind.Hash - } + }, + GeospatialConfig = new Cosmos.GeospatialConfig(geospatialType) }, // This throughput needs to be about half the max with multi master // otherwise it will create about twice as many partitions. @@ -208,50 +216,57 @@ private async Task CreatePartitionedContainer( private Task<(Container, IReadOnlyList)> CreateNonPartitionedContainerAndIngestDocumentsAsync( IEnumerable documents, - Cosmos.IndexingPolicy indexingPolicy = null) + Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { return this.CreateContainerAndIngestDocumentsAsync( CollectionTypes.NonPartitioned, documents, partitionKey: null, - indexingPolicy: indexingPolicy); + indexingPolicy: indexingPolicy, + geospatialType: geospatialType); } private Task<(Container, IReadOnlyList)> CreateSinglePartitionContainerAndIngestDocumentsAsync( IEnumerable documents, string partitionKey = "/id", - Cosmos.IndexingPolicy indexingPolicy = null) + Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { return this.CreateContainerAndIngestDocumentsAsync( CollectionTypes.SinglePartition, documents, partitionKey, - indexingPolicy); + indexingPolicy, + geospatialType); } private Task<(Container, IReadOnlyList)> CreateMultiPartitionContainerAndIngestDocumentsAsync( IEnumerable documents, string partitionKey = "/id", - Cosmos.IndexingPolicy indexingPolicy = null) + Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { return this.CreateContainerAndIngestDocumentsAsync( CollectionTypes.MultiPartition, documents, partitionKey, - indexingPolicy); + indexingPolicy, + geospatialType); } private async Task<(Container, IReadOnlyList)> CreateContainerAndIngestDocumentsAsync( CollectionTypes collectionType, IEnumerable documents, string partitionKey = "/id", - Cosmos.IndexingPolicy indexingPolicy = null) + Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { Container container = collectionType switch { - CollectionTypes.NonPartitioned => await this.CreateNonPartitionedContainerAsync(indexingPolicy), - CollectionTypes.SinglePartition => await this.CreateSinglePartitionContainer(partitionKey, indexingPolicy), - CollectionTypes.MultiPartition => await this.CreateMultiPartitionContainer(partitionKey, indexingPolicy), + CollectionTypes.NonPartitioned => await this.CreateNonPartitionedContainerAsync(indexingPolicy, geospatialType), + CollectionTypes.SinglePartition => await this.CreateSinglePartitionContainer(partitionKey, indexingPolicy, geospatialType), + CollectionTypes.MultiPartition => await this.CreateMultiPartitionContainer(partitionKey, indexingPolicy, geospatialType), _ => throw new ArgumentException($"Unknown {nameof(CollectionTypes)} : {collectionType}"), }; List insertedDocuments = new List(); @@ -369,7 +384,8 @@ internal Task CreateIngestQueryDeleteAsync( Query query, string partitionKey = "/id", Cosmos.IndexingPolicy indexingPolicy = null, - CosmosClientFactory cosmosClientFactory = null) + CosmosClientFactory cosmosClientFactory = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { Task queryWrapper(Container container, IReadOnlyList inputDocuments, object throwaway) { @@ -384,7 +400,8 @@ Task queryWrapper(Container container, IReadOnlyList inputDocument null, partitionKey, indexingPolicy, - cosmosClientFactory); + cosmosClientFactory, + geospatialType); } internal Task CreateIngestQueryDeleteAsync( @@ -395,7 +412,8 @@ internal Task CreateIngestQueryDeleteAsync( T testArgs, string partitionKey = "/id", Cosmos.IndexingPolicy indexingPolicy = null, - CosmosClientFactory cosmosClientFactory = null) + CosmosClientFactory cosmosClientFactory = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { return this.CreateIngestQueryDeleteAsync( connectionModes, @@ -405,7 +423,8 @@ internal Task CreateIngestQueryDeleteAsync( cosmosClientFactory ?? this.CreateDefaultCosmosClient, testArgs, partitionKey, - indexingPolicy); + indexingPolicy, + geospatialType); } /// @@ -434,7 +453,8 @@ internal async Task CreateIngestQueryDeleteAsync( CosmosClientFactory cosmosClientFactory, T testArgs, string partitionKey = "/id", - Cosmos.IndexingPolicy indexingPolicy = null) + Cosmos.IndexingPolicy indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { try { @@ -450,15 +470,18 @@ internal async Task CreateIngestQueryDeleteAsync( { CollectionTypes.NonPartitioned => this.CreateNonPartitionedContainerAndIngestDocumentsAsync( documents, - indexingPolicy), + indexingPolicy, + geospatialType), CollectionTypes.SinglePartition => this.CreateSinglePartitionContainerAndIngestDocumentsAsync( documents, partitionKey, - indexingPolicy), + indexingPolicy, + geospatialType), CollectionTypes.MultiPartition => this.CreateMultiPartitionContainerAndIngestDocumentsAsync( documents, partitionKey, - indexingPolicy), + indexingPolicy, + geospatialType), _ => throw new ArgumentException($"Unknown {nameof(CollectionTypes)} : {collectionType}"), }; collectionsAndDocuments.Add(await createContainerTask); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs index 8748360c9a..0482695ec2 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs @@ -18,7 +18,8 @@ internal static class NonPartitionedContainerHelper internal static async Task CreateNonPartitionedContainer( Cosmos.Database database, string containerId, - string indexingPolicy = null) + string indexingPolicy = null, + Cosmos.GeospatialType geospatialType = Cosmos.GeospatialType.Geography) { DocumentCollection documentCollection = new DocumentCollection() { @@ -30,6 +31,13 @@ internal static async Task CreateNonPartitionedContainer( documentCollection.IndexingPolicy = JsonConvert.DeserializeObject(indexingPolicy); } + documentCollection.GeospatialConfig = geospatialType switch + { + Cosmos.GeospatialType.Geography => new GeospatialConfig(GeospatialType.Geography), + Cosmos.GeospatialType.Geometry => new GeospatialConfig(GeospatialType.Geometry), + _ => throw new InvalidOperationException($"Unsupported geospatialType {geospatialType}") + }; + await NonPartitionedContainerHelper.CreateNonPartitionedContainer( database, documentCollection); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPlanBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPlanBaselineTests.cs index c0ce4c3966..79d674d7b9 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPlanBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPlanBaselineTests.cs @@ -1313,8 +1313,7 @@ public void Spatial() // ST_WITHIN new { Description = @"ST_WITHIN Constant Foldable", Query = @"SELECT * FROM c WHERE NOT ST_WITHIN({'type': 'Point', 'coordinates': [0, 40]}, {'type':'Polygon','coordinates':[[[-60,20], [70,20], [70,70], [-60,70], [-60,20]]]})" }, - new { Description = @"ST_WITHIN", Query = @"SELECT * FROM c WHERE NOT ST_WITHIN(c.geojson, - {'type':'Polygon','coordinates':[[[-60,20], [70,20], [70,70], [-60,70], [-60,20]]]})" }, + new { Description = @"ST_WITHIN", Query = @"SELECT * FROM c WHERE NOT ST_WITHIN(c.geojson, {'type':'Polygon','coordinates':[[[-60,20], [70,20], [70,70], [-60,70], [-60,20]]]})" }, }; List testVariations = variations