diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 2ada0738da..fef136d8d2 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -15,12 +15,10 @@ namespace Microsoft.Azure.Cosmos.Linq using System.Reflection; using Microsoft.Azure.Cosmos.CosmosElements; using Microsoft.Azure.Cosmos.CosmosElements.Numbers; - using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Documents; using Newtonsoft.Json; - using Newtonsoft.Json.Linq; using static Microsoft.Azure.Cosmos.Linq.FromParameterBindings; // ReSharper disable UnusedParameter.Local @@ -77,10 +75,9 @@ public static class LinqMethods public const string Where = "Where"; } - private static string SqlRoot = "root"; - private static string DefaultParameterName = "v"; - private static bool usePropertyRef = false; - private static SqlIdentifier RootIdentifier = SqlIdentifier.Create(SqlRoot); + private static readonly string SqlRoot = "root"; + private static readonly string DefaultParameterName = "v"; + private static readonly bool usePropertyRef = false; /// /// Toplevel entry point. @@ -455,12 +452,12 @@ private static SqlScalarExpression VisitBinary(BinaryExpression inputExpression, { if (TryMatchStringCompareTo(methodCallExpression, constantExpression, inputExpression.NodeType)) { - return ExpressionToSql.VisitStringCompareTo(methodCallExpression, constantExpression, inputExpression.NodeType, reverseNodeType, context); + return ExpressionToSql.VisitStringCompareTo(methodCallExpression, inputExpression.NodeType, reverseNodeType, context); } if (TryMatchStringCompare(methodCallExpression, constantExpression, inputExpression.NodeType)) { - return ExpressionToSql.VisitStringCompare(methodCallExpression, constantExpression, inputExpression.NodeType, reverseNodeType, context); + return ExpressionToSql.VisitStringCompare(methodCallExpression, inputExpression.NodeType, reverseNodeType, context); } } @@ -613,7 +610,6 @@ private static bool TryMatchStringCompareTo(MethodCallExpression left, ConstantE private static SqlScalarExpression VisitStringCompareTo( MethodCallExpression left, - ConstantExpression right, ExpressionType compareOperator, bool reverseNodeType, TranslationContext context) @@ -690,7 +686,6 @@ private static bool TryMatchStringCompare(MethodCallExpression left, ConstantExp private static SqlScalarExpression VisitStringCompare( MethodCallExpression left, - ConstantExpression right, ExpressionType compareOperator, bool reverseNodeType, TranslationContext context) diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs index a694b78019..fd9ad03b53 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs @@ -16,8 +16,6 @@ namespace Microsoft.Azure.Cosmos using Microsoft.Azure.Cosmos.ChangeFeed; using Microsoft.Azure.Cosmos.ChangeFeed.FeedProcessing; using Microsoft.Azure.Cosmos.ChangeFeed.Pagination; - using Microsoft.Azure.Cosmos.ChangeFeed.Utils; - using Microsoft.Azure.Cosmos.Common; using Microsoft.Azure.Cosmos.CosmosElements; using Microsoft.Azure.Cosmos.Json; using Microsoft.Azure.Cosmos.Linq; @@ -29,11 +27,8 @@ namespace Microsoft.Azure.Cosmos using Microsoft.Azure.Cosmos.Query.Core.QueryPlan; using Microsoft.Azure.Cosmos.ReadFeed; using Microsoft.Azure.Cosmos.ReadFeed.Pagination; - using Microsoft.Azure.Cosmos.Routing; - using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.Tracing; using Microsoft.Azure.Documents; - using Microsoft.Azure.Documents.Routing; /// /// Used to perform operations on items. There are two different types of operations. diff --git a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs index b7523b2c6a..3b215a9cc8 100644 --- a/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs +++ b/Microsoft.Azure.Cosmos/src/Serializer/CosmosLinqSerializerOptions.cs @@ -4,10 +4,6 @@ namespace Microsoft.Azure.Cosmos { - using System; - using System.Collections.Generic; - using System.Text; - /// /// This class provides a way to configure Linq Serialization Properties /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDataMember.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDataMember.xml new file mode 100644 index 0000000000..6734a125fa --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDataMember.xml @@ -0,0 +1,236 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectDataMember() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectDataMember() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectDataMember() {NumericField = 1, StringField = "1"}, new DataObjectDataMember() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null} : {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectDataMember() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectDataMember() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectDataMember() {NumericField = 1, StringField = "1"}, new DataObjectDataMember() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null} : {"NumericFieldDataMember": 1, "StringFieldDataMember": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectDataMember() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml new file mode 100644 index 0000000000..b381e2b651 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerDotNet.xml @@ -0,0 +1,236 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectDotNet() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectDotNet() {NumericField = 1, StringField = "1"}, new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumericField": 1, "StringField": "1", "id": null, "Pk": null} : {"NumericField": 1, "StringField": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectDotNet() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectDotNet() {NumericField = 1, StringField = "1"}, new DataObjectDotNet() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumericField": 1, "StringField": "1", "id": null, "Pk": null} : {"NumericField": 1, "StringField": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectDotNet() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml new file mode 100644 index 0000000000..704eb39807 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerMultiSerializer.xml @@ -0,0 +1,236 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}, new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectMultiSerializer() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}, new DataObjectMultiSerializer() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectMultiSerializer() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoft.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoft.xml new file mode 100644 index 0000000000..b5b16752cc --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqTranslationWithCustomSerializerBaseline.TestMemberInitializerNewtonsoft.xml @@ -0,0 +1,236 @@ + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectNewtonsoft() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + + + + (doc.NumericField == 1))]]> + + + + + + + + + + + (doc == new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}))]]> + + + + + + + + + + + new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"})]]> + + + + + + + + + + + IIF((doc.NumericField > 1), new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}, new DataObjectNewtonsoft() {NumericField = 1, StringField = "1"}))]]> + + + 1) ? {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null} : {"NumberValueNewtonsoft": 1, "StringValueNewtonsoft": "1", "id": null, "Pk": null}) +FROM root]]> + + + + + + + + (doc == new DataObjectNewtonsoft() {NumericField = doc.NumericField, StringField = doc.StringField})).Select(b => "A")]]> + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CustomSerializationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CustomSerializationTests.cs index 5e6e2fe7bb..3334c201d1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CustomSerializationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CustomSerializationTests.cs @@ -14,7 +14,6 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; - using Microsoft.Azure.Cosmos.Linq; using Microsoft.Azure.Cosmos.Utils; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; @@ -35,7 +34,7 @@ public abstract class CustomSerializationTests private Uri databaseUri; private Uri collectionUri; private Uri partitionedCollectionUri; - private PartitionKeyDefinition defaultPartitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection(new[] { "/pk" }), Kind = PartitionKind.Hash }; + private readonly PartitionKeyDefinition defaultPartitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection(new[] { "/pk" }), Kind = PartitionKind.Hash }; internal abstract DocumentClient CreateDocumentClient( Uri hostUri, @@ -77,7 +76,7 @@ public void TestSetup() { if (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) { - // Emulator con sometimes fail under load, so we retry + // Emulator can sometimes fail under load, so we retry Task.Delay(1000); this.documentClient.CreateDocumentCollectionAsync(this.databaseUri, newCollection, new RequestOptions { OfferThroughput = 400 }).Wait(); } @@ -100,7 +99,7 @@ public void TestSetup() { if (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) { - // Emulator con sometimes fail under load, so we retry + // Emulator can sometimes fail under load, so we retry Task.Delay(1000); this.documentClient.CreateDocumentCollectionAsync(this.databaseUri, partitionedCollection, new RequestOptions { OfferThroughput = 10000 }).Wait(); } @@ -136,13 +135,12 @@ public void TestDateParseHandlingOnReadDocument() // Verify round-trip create and read document RequestOptions applyRequestOptions = this.ApplyRequestOptions(new RequestOptions(), serializerSettings); - this.AssertPropertyOnReadDocument(client, this.collectionUri, createdDocument, applyRequestOptions, originalDocument, jsonProperty); - this.AssertPropertyOnReadDocument(client, this.partitionedCollectionUri, partitionedDocument, applyRequestOptions, originalDocument, jsonProperty); + this.AssertPropertyOnReadDocument(client, createdDocument, applyRequestOptions, originalDocument, jsonProperty); + this.AssertPropertyOnReadDocument(client, partitionedDocument, applyRequestOptions, originalDocument, jsonProperty); } private void AssertPropertyOnReadDocument( DocumentClient client, - Uri targetCollectionUri, Document createdDocument, RequestOptions requestOptions, Document originalDocument, @@ -311,26 +309,6 @@ public async Task TestJsonSerializerSettings(bool useGateway) } this.AssertEqual(testDocument, allDocuments.First()); - - //Will add LINQ test once it is available with new V3 OM - // // LINQ Lambda - // var query1 = client.CreateDocumentQuery(partitionedCollectionUri, options) - // .Where(_ => _.Id.CompareTo(String.Empty) > 0) - // .Select(_ => _.Id); - // string query1Str = query1.ToString(); - // var result = query1.ToList(); - // Assert.AreEqual(1, result.Count); - // Assert.AreEqual(testDocument.Id, result[0]); - - // // LINQ Query - // var query2 = - // from f in client.CreateDocumentQuery(partitionedCollectionUri, options) - // where f.Id.CompareTo(String.Empty) > 0 - // select f.Id; - // string query2Str = query2.ToString(); - // var result2 = query2.ToList(); - // Assert.AreEqual(1, result2.Count); - // Assert.AreEqual(testDocument.Id, result2[0]); } [TestMethod] diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs index 87b33110e2..c8513e41b8 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs @@ -156,12 +156,11 @@ public static Boolean IsAnonymousType(Type type) } /// - /// Validate the results of CosmosDB query and the results of LinQ query on the original data - /// Using Assert, will fail the unit test if the two results list are not SequenceEqual + /// Gets the results of CosmosDB query and the results of LINQ query on the original data /// /// /// - public static void ValidateResults(IQueryable queryResults, IQueryable dataResults) + public static (List queryResults, List dataResults) GetResults(IQueryable queryResults, IQueryable dataResults) { // execution validation IEnumerator queryEnumerator = queryResults.GetEnumerator(); @@ -171,7 +170,19 @@ public static void ValidateResults(IQueryable queryResults, IQueryable dataResul queryResultsList.Add(queryEnumerator.Current); } - List dataResultsList = dataResults.Cast().ToList(); + List dataResultsList = dataResults?.Cast()?.ToList(); + + return (queryResultsList, dataResultsList); + } + + /// + /// Validates the results of CosmosDB query and the results of LINQ query on the original data + /// Using Assert, will fail the unit test if the two results list are not SequenceEqual + /// + /// + /// + private static void ValidateResults(List queryResultsList, List dataResultsList) + { bool resultMatched = true; string actualStr = null; string expectedStr = null; @@ -229,9 +240,8 @@ public static void ValidateResults(IQueryable queryResults, IQueryable dataResul string assertMsg = string.Empty; if (!resultMatched) { - if (actualStr == null) actualStr = JsonConvert.SerializeObject(queryResultsList); - - if (expectedStr == null) expectedStr = JsonConvert.SerializeObject(dataResultsList); + actualStr ??= JsonConvert.SerializeObject(queryResultsList); + expectedStr ??= JsonConvert.SerializeObject(dataResultsList); resultMatched |= actualStr.Equals(expectedStr); if (!resultMatched) @@ -271,7 +281,7 @@ public static DateTime RandomDateTime(Random random, DateTime midDateTime) } /// - /// Generate test data for most LinQ tests + /// Generate test data for most LINQ tests /// /// the object type /// the lamda to create an instance of test data @@ -315,6 +325,44 @@ public static Func> GenerateTestCosmosData(Func + /// Generate a non-random payload for serializer LINQ tests. + /// + /// the object type + /// the lamda to create an instance of test data + /// number of test data to be created + /// the target container + /// if theCosmosLinqSerializerOption of camelCaseSerialization should be applied + /// a lambda that takes a boolean which indicate where the query should run against CosmosDB or against original data, and return a query results as IQueryable. Also the serialized payload. + public static Func> GenerateSerializationTestCosmosData(Func func, int count, Container container, bool camelCaseSerialization = false) + { + List data = new List(); + for (int i = 0; i < count; i++) + { + data.Add(func(i, camelCaseSerialization)); + } + + foreach (T obj in data) + { + ItemResponse response = container.CreateItemAsync(obj, new Cosmos.PartitionKey("Test")).Result; + } + + FeedOptions feedOptions = new FeedOptions() { EnableScanInQuery = true, EnableCrossPartitionQuery = true }; + QueryRequestOptions requestOptions = new QueryRequestOptions() + { +#if PREVIEW + EnableOptimisticDirectExecution = false +#endif + }; + + CosmosLinqSerializerOptions linqSerializerOptions = new CosmosLinqSerializerOptions { PropertyNamingPolicy = camelCaseSerialization ? CosmosPropertyNamingPolicy.CamelCase : CosmosPropertyNamingPolicy.Default }; + IOrderedQueryable query = container.GetItemLinqQueryable(allowSynchronousQueryExecution: true, requestOptions: requestOptions, linqSerializerOptions: linqSerializerOptions); + + IQueryable getQuery(bool useQuery) => useQuery ? query : data.AsQueryable(); + + return getQuery; + } + public static Func> GenerateFamilyCosmosData( Cosmos.Database cosmosDatabase, out Container container) { @@ -434,7 +482,7 @@ Family createDataObj(Random random) for (int j = 0; j < random.Next(MaxThings) + 1; ++j) { obj.Children[i].Things.Add( - j == 0 ? "A" : $"{j}-{random.Next().ToString()}", + j == 0 ? "A" : $"{j}-{random.Next()}", LinqTestsCommon.RandomString(random, random.Next(MaxThingStringLength))); } } @@ -462,9 +510,7 @@ Family createDataObj(Random random) return getQuery; } - public static Func> GenerateSimpleCosmosData( - Cosmos.Database cosmosDatabase - ) + public static Func> GenerateSimpleCosmosData(Cosmos.Database cosmosDatabase) { const int DocumentCount = 10; PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection(new[] { "/Pk" }), Kind = PartitionKind.Hash }; @@ -480,7 +526,7 @@ Cosmos.Database cosmosDatabase { Id = Guid.NewGuid().ToString(), Number = random.Next(-10000, 10000), - Flag = index % 2 == 0 ? true : false, + Flag = index % 2 == 0, Multiples = new int[] { index, index * 2, index * 3, index * 4 }, Pk = "Test" }; @@ -508,28 +554,35 @@ Cosmos.Database cosmosDatabase return getQuery; } - public static LinqTestOutput ExecuteTest(LinqTestInput input) + public static LinqTestOutput ExecuteTest(LinqTestInput input, bool serializeResultsInBaseline = false) { string querySqlStr = string.Empty; try { Func compiledQuery = input.Expression.Compile(); - IQueryable queryResults = compiledQuery(true); - querySqlStr = JObject.Parse(queryResults.ToString()).GetValue("query", StringComparison.Ordinal).ToString(); + IQueryable query = compiledQuery(true); + querySqlStr = JObject.Parse(query.ToString()).GetValue("query", StringComparison.Ordinal).ToString(); - // we skip unordered query because the LinQ results vs actual query results are non-deterministic + IQueryable dataQuery = input.skipVerification ? null : compiledQuery(false); + + (List queryResults, List dataResults) = GetResults(query, dataQuery); + + // we skip unordered query because the LINQ results vs actual query results are non-deterministic if (!input.skipVerification) { - IQueryable dataResults = compiledQuery(false); LinqTestsCommon.ValidateResults(queryResults, dataResults); } - return new LinqTestOutput(querySqlStr); + string serializedResults = serializeResultsInBaseline ? + JsonConvert.SerializeObject(queryResults.Select(item => item is LinqTestObject ? item.ToString() : item), new JsonSerializerSettings { Formatting = Newtonsoft.Json.Formatting.Indented}) : + null; + + return new LinqTestOutput(querySqlStr, serializedResults, errorMsg: null, input.inputData); } catch (Exception e) { - return new LinqTestOutput(querySqlStr, LinqTestsCommon.BuildExceptionMessageForTest(e)); + return new LinqTestOutput(querySqlStr, serializedResults: null, errorMsg: LinqTestsCommon.BuildExceptionMessageForTest(e), inputData: input.inputData); } } @@ -570,13 +623,15 @@ public class LinqTestObject { private string json; + protected virtual string SerializeForTestBaseline() + { + return JsonConvert.SerializeObject(this); + } + public override string ToString() { // simple cached serialization - if (this.json == null) - { - this.json = JsonConvert.SerializeObject(this); - } + this.json ??= this.SerializeForTestBaseline(); return this.json; } @@ -608,18 +663,25 @@ public class LinqTestInput : BaselineTestInput internal int randomSeed = -1; internal Expression> Expression { get; } internal string expressionStr; + internal string inputData; // We skip the verification between Cosmos DB and actual query restuls in the following cases // - unordered query since the results are not deterministics for LinQ results and actual query results // - scenarios not supported in LINQ, e.g. sequence doesn't contain element. internal bool skipVerification; - internal LinqTestInput(string description, Expression> expr, bool skipVerification = false, string expressionStr = null) + internal LinqTestInput( + string description, + Expression> expr, + bool skipVerification = false, + string expressionStr = null, + string inputData = null) : base(description) { this.Expression = expr ?? throw new ArgumentNullException($"{nameof(expr)} must not be null."); this.skipVerification = skipVerification; this.expressionStr = expressionStr; + this.inputData = inputData; } public static string FilterInputExpression(string input) @@ -656,11 +718,7 @@ public override void SerializeAsXml(XmlWriter xmlWriter) throw new ArgumentNullException($"{nameof(xmlWriter)} cannot be null."); } - if (this.expressionStr == null) - { - this.expressionStr = LinqTestInput.FilterInputExpression(this.Expression.Body.ToString()); - } - + this.expressionStr ??= LinqTestInput.FilterInputExpression(this.Expression.Body.ToString()); xmlWriter.WriteStartElement("Description"); xmlWriter.WriteCData(this.Description); @@ -678,7 +736,9 @@ public class LinqTestOutput : BaselineTestOutput internal static Regex newLine = new Regex("(\r\n|\r|\n)"); internal string SqlQuery { get; } - internal string ErrorMessage { get; private set; } + internal string ErrorMessage { get; } + internal string Results { get; } + internal string InputData { get; } private static readonly Dictionary newlineKeywords = new Dictionary() { { "SELECT", "\nSELECT" }, @@ -704,10 +764,12 @@ public static string FormatErrorMessage(string msg) return msg; } - internal LinqTestOutput(string sqlQuery, string errorMsg = null) + internal LinqTestOutput(string sqlQuery, string serializedResults, string errorMsg, string inputData) { this.SqlQuery = FormatSql(sqlQuery); + this.Results = serializedResults; this.ErrorMessage = errorMsg; + this.InputData = inputData; } public static String FormatSql(string sqlQuery) @@ -740,7 +802,7 @@ public static String FormatSql(string sqlQuery) } else if (tokens[i].StartsWith(endCue, StringComparison.OrdinalIgnoreCase)) { - indentSb.Length = indentSb.Length - oneTab.Length; + indentSb.Length -= oneTab.Length; } sb.Append(indentSb).Append(tokens[i]).Append("\n"); @@ -754,6 +816,18 @@ public override void SerializeAsXml(XmlWriter xmlWriter) xmlWriter.WriteStartElement(nameof(this.SqlQuery)); xmlWriter.WriteCData(this.SqlQuery); xmlWriter.WriteEndElement(); + if (this.InputData != null) + { + xmlWriter.WriteStartElement("InputData"); + xmlWriter.WriteCData(this.InputData); + xmlWriter.WriteEndElement(); + } + if (this.Results != null) + { + xmlWriter.WriteStartElement("Results"); + xmlWriter.WriteCData(this.Results); + xmlWriter.WriteEndElement(); + } if (this.ErrorMessage != null) { xmlWriter.WriteStartElement("ErrorMessage"); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs new file mode 100644 index 0000000000..3266809926 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/LinqTranslationWithCustomSerializerBaseline.cs @@ -0,0 +1,407 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//----------------------------------------------------------------------- +namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Linq.Dynamic; + using System.Runtime.Serialization; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading.Tasks; + using BaselineTest; + using global::Azure.Core.Serialization; + using Microsoft.Azure.Cosmos.SDK.EmulatorTests; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + [SDK.EmulatorTests.TestClass] + public class LinqTranslationWithCustomSerializerBaseline : BaselineTests + { + private static CosmosClient CosmosClient; + private static Database TestDb; + private static Container TestContainer; + + private const int RecordCount = 3; + private const int MaxValue = 500; + private const int MaxStringLength = 100; + private const int PropertyCount = 4; + + [ClassInitialize] + public async static Task Initialize(TestContext textContext) + { + CosmosClient = TestCommon.CreateCosmosClient((cosmosClientBuilder) + => cosmosClientBuilder.WithCustomSerializer(new SystemTextJsonSerializer(new JsonSerializerOptions()))); + + string dbName = $"{nameof(LinqTranslationBaselineTests)}-{Guid.NewGuid():N}"; + TestDb = await CosmosClient.CreateDatabaseAsync(dbName); + } + + [ClassCleanup] + public async static Task Cleanup() + { + if (TestDb != null) + { + await TestDb.DeleteStreamAsync(); + } + } + + [TestInitialize] + public async Task TestInitialize() + { + TestContainer = await TestDb.CreateContainerAsync(new ContainerProperties(id: Guid.NewGuid().ToString(), partitionKeyPath: "/Pk")); + } + + [TestCleanup] + public async Task TestCleanup() + { + await TestContainer.DeleteContainerStreamAsync(); + } + + public override LinqTestOutput ExecuteTest(LinqTestInput input) + { + return LinqTestsCommon.ExecuteTest(input, serializeResultsInBaseline: true); + } + + [TestMethod] + public void TestMemberInitializerDotNet() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + + string insertedData = this.GetInsertedData().Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + // TODO (10/13/23): extend this and other tests cases as more LINQ features are added (GROUP BY, etc.) + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDotNet() { NumericField = 1, StringField = "1" } : new DataObjectDotNet() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + + // Negative test case: serializing only field name using custom serializer not currently supported + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDotNet() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } + + this.ExecuteTestSuite(inputs); + } + + [TestMethod] + public void TestMemberInitializerNewtonsoft() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + + string insertedData = this.GetInsertedData().Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" } : new DataObjectNewtonsoft() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + + // Negative test case: serializing only field name using custom serializer not currently supported + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectNewtonsoft() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } + + this.ExecuteTestSuite(inputs); + } + + [TestMethod] + public void TestMemberInitializerDataMember() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + + string insertedData = this.GetInsertedData().Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectDataMember() { NumericField = 1, StringField = "1" } : new DataObjectDataMember() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + + // Negative test case: serializing only field name using custom serializer not currently supported + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectDataMember() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } + + this.ExecuteTestSuite(inputs); + } + + [TestMethod] + public void TestMemberInitializerMultiSerializer() + { + Func> getQueryCamelCase; + Func> getQueryDefault; + (getQueryCamelCase, getQueryDefault) = this.InsertDataAndGetQueryables(); + + string insertedData = this.GetInsertedData().Result; + + List inputs = new List(); + foreach (bool useCamelCaseSerializer in new bool[] { true, false }) + { + Func> getQuery = useCamelCaseSerializer ? getQueryCamelCase : getQueryDefault; + + List camelCaseSettingInputs = new List + { + new LinqTestInput("Filter w/ constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc.NumericField == 1), skipVerification : true, inputData: insertedData), + new LinqTestInput("Filter w/ DataObject initializer with constant value, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Select w/ DataObject initializer, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + new LinqTestInput("Deeper than top level reference, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Select(doc => doc.NumericField > 1 ? new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" } : new DataObjectMultiSerializer() { NumericField = 1, StringField = "1" }), skipVerification : true, inputData: insertedData), + + // Negative test case: serializing only field name using custom serializer not currently supported + new LinqTestInput("Filter w/ DataObject initializer with member initialization, camelcase = " + useCamelCaseSerializer, b => getQuery(b).Where(doc => doc == new DataObjectMultiSerializer() { NumericField = doc.NumericField, StringField = doc.StringField }).Select(b => "A"), skipVerification : true, inputData: insertedData) + }; + + inputs.AddRange(camelCaseSettingInputs); + } + + this.ExecuteTestSuite(inputs); + } + + private (Func>, Func>) InsertDataAndGetQueryables() where T : LinqTestObject + { + static T createDataObj(int index, bool camelCase) + { + T obj = (T)Activator.CreateInstance(typeof(T), new object[] + { + index, index.ToString(), $"{index}-{camelCase}", "Test" + }); + return obj; + } + + Func> getQueryCamelCase = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, TestContainer, camelCaseSerialization: true); + Func> getQueryDefault = LinqTestsCommon.GenerateSerializationTestCosmosData(createDataObj, RecordCount, TestContainer, camelCaseSerialization: false); + + return (getQueryCamelCase, getQueryDefault); + } + + private async Task GetInsertedData() + { + List insertedDataList = new List(); + using (FeedIterator feedIterator = TestContainer.GetItemQueryStreamIterator("SELECT * FROM c")) + { + while (feedIterator.HasMoreResults) + { + using (ResponseMessage response = await feedIterator.ReadNextAsync()) + { + response.EnsureSuccessStatusCode(); + using (StreamReader streamReader = new StreamReader(response.Content)) + using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader)) + { + // manual parsing of response object to preserve property names + JObject queryResponseObject = await JObject.LoadAsync(jsonTextReader); + IEnumerable info = queryResponseObject["Documents"].AsEnumerable(); + + foreach (JToken docToken in info) + { + string documentString = "{"; + for (int index = 0; index < PropertyCount; index++) + { + documentString += index == 0 ? String.Empty : ", "; + documentString += docToken.ElementAt(index).ToString(); + } + documentString += "}"; + insertedDataList.Add(documentString); + } + } + } + } + } + + string insertedData = JsonConvert.SerializeObject(insertedDataList.Select(item => item), new JsonSerializerSettings { Formatting = Newtonsoft.Json.Formatting.Indented }); + return insertedData; + } + + private class SystemTextJsonSerializer : CosmosSerializer + { + private readonly JsonObjectSerializer systemTextJsonSerializer; + + public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions) + { + this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions); + } + + public override T FromStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (stream) + { + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default); + } + } + + public override Stream ToStream(T input) + { + MemoryStream streamPayload = new MemoryStream(); + this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default); + streamPayload.Position = 0; + return streamPayload; + } + } + + private class DataObjectDotNet : LinqTestObject + { + [JsonPropertyName("numberValueDotNet")] + public double NumericField { get; set; } + + [JsonPropertyName("stringValueDotNet")] + public string StringField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectDotNet() { } + + public DataObjectDotNet(double numericField, string stringField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.id = id; + this.Pk = pk; + } + + public override string ToString() + { + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } + + private class DataObjectNewtonsoft : LinqTestObject + { + [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] + public double NumericField { get; set; } + + [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] + public string StringField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectNewtonsoft() { } + + public DataObjectNewtonsoft(double numericField, string stringField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.id = id; + this.Pk = pk; + } + + public override string ToString() + { + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } + + [DataContract] + private class DataObjectDataMember : LinqTestObject + { + [DataMember(Name = "NumericFieldDataMember")] + public double NumericField { get; set; } + + [DataMember(Name = "StringFieldDataMember")] + public string StringField { get; set; } + + [DataMember(Name = "id")] + public string id { get; set; } + + [DataMember(Name = "Pk")] + public string Pk { get; set; } + + public DataObjectDataMember() { } + + public DataObjectDataMember(double numericField, string stringField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.id = id; + this.Pk = pk; + } + + public override string ToString() + { + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } + + private class DataObjectMultiSerializer : LinqTestObject + { + [Newtonsoft.Json.JsonProperty(PropertyName = "NumberValueNewtonsoft")] + [JsonPropertyName("numberValueDotNet")] + public double NumericField { get; set; } + + [Newtonsoft.Json.JsonProperty(PropertyName = "StringValueNewtonsoft")] + [JsonPropertyName("stringValueDotNet")] + public string StringField { get; set; } + + public string id { get; set; } + + public string Pk { get; set; } + + public DataObjectMultiSerializer() { } + + public DataObjectMultiSerializer(double numericField, string stringField, string id, string pk) + { + this.NumericField = numericField; + this.StringField = stringField; + this.id = id; + this.Pk = pk; + } + + public override string ToString() + { + return $"{{NumericField:{this.NumericField},StringField:{this.StringField},id:{this.id},Pk:{this.Pk}}}"; + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index 30c29ec5db..7ff30b3cee 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -1,4 +1,4 @@ - + true true @@ -38,6 +38,10 @@ + + + + @@ -251,7 +255,19 @@ PreserveNewest - PreserveNewest + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest PreserveNewest