Repository: ignite Updated Branches: refs/heads/master 31087002b -> ed76af245
IGNITE-5298 .NET: DML update via LINQ This closes #3599 Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/ed76af24 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/ed76af24 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/ed76af24 Branch: refs/heads/master Commit: ed76af245e7947b0e701907bde76849971a6a75c Parents: 3108700 Author: Sergey Stronchinskiy <gurustronpub...@gmail.com> Authored: Wed Mar 7 11:44:51 2018 +0300 Committer: Pavel Tupitsyn <ptupit...@apache.org> Committed: Wed Mar 7 11:44:51 2018 +0300 ---------------------------------------------------------------------- .../Cache/Query/Linq/CacheLinqTest.Base.cs | 4 + .../Cache/Query/Linq/CacheLinqTest.Custom.cs | 195 +++++++++++++++++++ .../Apache.Ignite.Linq.csproj | 4 + .../Apache.Ignite.Linq/CacheLinqExtensions.cs | 28 +++ .../Apache.Ignite.Linq/IUpdateDescriptor.cs | 51 +++++ .../Apache.Ignite.Linq/Impl/AliasDictionary.cs | 8 +- .../Impl/CacheQueryExpressionVisitor.cs | 43 +++- .../Impl/CacheQueryModelVisitor.cs | 143 ++++++++++---- .../Apache.Ignite.Linq/Impl/CacheQueryParser.cs | 3 + .../Impl/Dml/MemberUpdateContainer.cs | 38 ++++ .../Impl/Dml/UpdateAllExpressionNode.cs | 138 +++++++++++++ .../Impl/Dml/UpdateAllResultOperator.cs | 75 +++++++ 12 files changed, 680 insertions(+), 50 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs index d70a7a4..5b56abd 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs @@ -346,6 +346,8 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq [QuerySqlField] public int AliasTest { get; set; } + [QuerySqlField] public bool Bool { get; set; } + public void WriteBinary(IBinaryWriter writer) { writer.WriteInt("age1", Age); @@ -354,6 +356,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq writer.WriteObject("Address", Address); writer.WriteTimestamp("Birthday", Birthday); writer.WriteInt("AliasTest", AliasTest); + writer.WriteBoolean("Bool", Bool); } public void ReadBinary(IBinaryReader reader) @@ -364,6 +367,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq Address = reader.ReadObject<Address>("Address"); Birthday = reader.ReadTimestamp("Birthday"); AliasTest = reader.ReadInt("AliasTest"); + Bool = reader.ReadBoolean("Bool"); } } http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs index f797b4c..c854f1f 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs @@ -28,6 +28,7 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq { using System; using System.Linq; + using Apache.Ignite.Core.Cache; using Apache.Ignite.Core.Cache.Configuration; using Apache.Ignite.Core.Common; using Apache.Ignite.Linq; @@ -103,5 +104,199 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq var ex = Assert.Throws<IgniteException>(() => qry.RemoveAll()); Assert.AreEqual("Failed to parse query", ex.Message.Substring(0, 21)); } + + /// <summary> + /// Tests the UpdateAll extension without condition. + /// </summary> + [Test] + public void TestUpdateAllUnconditional() + { + // Use new cache to avoid touching static data. + var personCount = 10; + var orgCount = 3; + var personQueryable = GetPersonCacheQueryable("updateAllTest_Unconditional_Persons", personCount, orgCount); + var orgQueryable = GetOrgCacheQueryable("updateAllTest_Unconditional_Org", orgCount); + + var allOrg = orgQueryable.ToArray(); + + // Constant value + var updated = personQueryable + .UpdateAll(d => d.Set(p => p.AliasTest, 7)); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, p => p.Value.AliasTest == 7); + + // Expression value - from self + updated = personQueryable + .UpdateAll(d => d.Set(p => p.AliasTest, e => e.Key)); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, p => p.Value.AliasTest == p.Key); + + // Multiple sets + var aliasValue = 3; + updated = personQueryable + .UpdateAll(d => d.Set(p => p.AliasTest, aliasValue).Set(p => p.Name, aliasValue.ToString())); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, p => p.Value.AliasTest == aliasValue && p.Value.Name == aliasValue.ToString()); + + // Expression value - subquery with same cache + updated = personQueryable + .UpdateAll(d => d.Set(p => p.AliasTest, + e => personQueryable.Where(ie => ie.Key == e.Key).Select(ie => ie.Key).First())); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, p => p.Value.AliasTest == p.Key); + + // Expression value - subquery with other cache + updated = personQueryable + .UpdateAll(d => d.Set(p => p.AliasTest, p => orgQueryable.Count(o => o.Value.Id > p.Key))); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, p => p.Value.AliasTest == allOrg.Count(o => o.Value.Id > p.Key)); + + updated = personQueryable + .UpdateAll(d => d.Set(p => p.Name, + e => orgQueryable.Where(o => o.Key == e.Value.OrganizationId).Select(o => o.Value.Name).First())); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, + p => p.Value.Name == allOrg.Where(o => o.Key == p.Value.OrganizationId).Select(o => o.Value.Name) + .FirstOrDefault()); + + // Expression value - Contains subquery with other cache + updated = personQueryable + .UpdateAll(d => d.Set(p => p.Bool, + p => orgQueryable.Select(o => o.Key).Contains(p.Value.OrganizationId))); + Assert.AreEqual(personCount, updated); + AssertAll(personQueryable, + p => p.Value.Bool == allOrg.Select(o => o.Key).Contains(p.Value.OrganizationId)); + + // Row number limit. + var name = "rowLimit" + 2; + updated = personQueryable + .Take(2) + .UpdateAll(d => d.Set(p => p.Name, name)); + Assert.AreEqual(2, updated); + Assert.AreEqual(2, personQueryable.Count(p => p.Value.Name == name)); + } + + /// <summary> + /// Tests the UpdateAll extension with condition. + /// </summary> + [Test] + public void TestUpdateAllWithCondition() + { + // ReSharper disable AccessToModifiedClosure + // Use new cache to avoid touching static data. + var personQueryable = GetPersonCacheQueryable("updateAllTest_WithCondition_Persons", 10); + + // Simple conditional + var aliasValue = 777; + var updated = personQueryable + .Where(p => p.Key > 8) + .UpdateAll(d => d.Set(p => p.AliasTest, aliasValue)); + Assert.AreEqual(2, updated); + AssertAll(personQueryable, + p => p.Key <= 8 && p.Value.AliasTest != aliasValue || p.Value.AliasTest == aliasValue); + + // Conditional with limit + aliasValue = 8888; + updated = personQueryable + .Where(p => p.Key > 1) + .Take(3) + .UpdateAll(d => d.Set(p => p.AliasTest, aliasValue)); + Assert.AreEqual(3, updated); + Assert.AreEqual(3, personQueryable.ToArray().Count(p => p.Value.AliasTest == aliasValue)); + // ReSharper restore AccessToModifiedClosure + } + + /// <summary> + /// Tests not supported queries for the UpdateAll extension . + /// </summary> + [Test] + public void TestUpdateAllUnsupported() + { + // Use new cache to avoid touching static data. + var personQueryable = GetPersonCacheQueryable("updateAllTest_Unsupported_Persons", 10); + + // Skip is not supported with DELETE. + var nex = Assert.Throws<NotSupportedException>( + () => personQueryable.Skip(1).UpdateAll(d => d.Set(p => p.Age, 15))); + Assert.AreEqual("UpdateAll can not be combined with result operators (other than Take): SkipResultOperator", + nex.Message); + + // Multiple result operators are not supported with DELETE. + nex = Assert.Throws<NotSupportedException>(() => + personQueryable.Skip(1).Take(1).UpdateAll(d => d.Set(p => p.Age, 15))); + Assert.AreEqual( + "UpdateAll can not be combined with result operators (other than Take): SkipResultOperator, " + + "TakeResultOperator, UpdateAllResultOperator", nex.Message); + + // Joins are not supported in H2. + var qry = personQueryable + .Where(x => x.Key == 7) + .Join(GetPersonCache().AsCacheQueryable(), p => p.Key, p => p.Key, (p1, p2) => p1); + + var ex = Assert.Throws<IgniteException>(() => qry.UpdateAll(d => d.Set(p => p.Age, 15))); + Assert.AreEqual("Failed to parse query", ex.Message.Substring(0, 21)); + } + + /// <summary> + /// Gets filled persons cache queryable + /// </summary> + private IQueryable<ICacheEntry<int, Person>> GetPersonCacheQueryable(string cacheName, int personCount, + int? orgCount = null) + { + var cache = GetCache<Person>(cacheName); + + cache.PutAll(Enumerable.Range(1, personCount).ToDictionary(x => x, + x => new Person(x, x.ToString()) + { + Birthday = DateTime.UtcNow.AddDays(personCount - x), + OrganizationId = x % orgCount ?? 0 + })); + + return cache.AsCacheQueryable(); + } + + /// <summary> + /// Asserts that all values in cache correspond to predicate + /// </summary> + private static void AssertAll( + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + IQueryable<ICacheEntry<int, Person>> personQueryable, + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + Func<ICacheEntry<int, Person>, bool> predicate) + { + Assert.IsTrue(personQueryable.ToArray().All(predicate)); + } + + /// <summary> + /// Gets filled organization cache queryable + /// </summary> + private IQueryable<ICacheEntry<int, Organization>> GetOrgCacheQueryable(string cacheName, int orgCount) + { + var cache = GetCache<Organization>(cacheName); + var allOrg = Enumerable.Range(1, orgCount) + .Select(x => new Organization + { + Id = x, + Name = x.ToString() + }) + .ToList(); + + allOrg.ForEach(x => cache.Put(x.Id, x)); + + return cache.AsCacheQueryable(); + } + + /// <summary> + /// Gets cache of <see cref="T"/> + /// </summary> + private ICache<int, T> GetCache<T>(string cacheName) + { + return Ignition.GetIgnite().GetOrCreateCache<int, T>(new CacheConfiguration(cacheName, + new QueryEntity(typeof(int), typeof(T))) + { + SqlEscapeAll = GetSqlEscapeAll(), + CacheMode = CacheMode.Replicated + }); + } } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Apache.Ignite.Linq.csproj ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Apache.Ignite.Linq.csproj b/modules/platforms/dotnet/Apache.Ignite.Linq/Apache.Ignite.Linq.csproj index fc13914..d5a2d83 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Apache.Ignite.Linq.csproj +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Apache.Ignite.Linq.csproj @@ -64,7 +64,10 @@ <Compile Include="Impl\CacheQueryExpressionVisitor.cs" /> <Compile Include="Impl\CacheQueryModelVisitor.cs" /> <Compile Include="Impl\CacheQueryParser.cs" /> + <Compile Include="Impl\Dml\MemberUpdateContainer.cs" /> + <Compile Include="Impl\Dml\UpdateAllExpressionNode.cs" /> <Compile Include="Impl\Dml\RemoveAllExpressionNode.cs" /> + <Compile Include="Impl\Dml\UpdateAllResultOperator.cs" /> <Compile Include="Impl\Dml\RemoveAllResultOperator.cs" /> <Compile Include="Impl\EnumerableHelper.cs" /> <Compile Include="Impl\ICacheQueryableInternal.cs" /> @@ -73,6 +76,7 @@ <Compile Include="Impl\SqlTypes.cs" /> <Compile Include="Impl\ExpressionWalker.cs" /> <Compile Include="Impl\JoinInnerSequenceParameterNotNullExpressionVisitor.cs" /> + <Compile Include="IUpdateDescriptor.cs" /> <Compile Include="Package-Info.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="QueryOptions.cs" /> http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/CacheLinqExtensions.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/CacheLinqExtensions.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/CacheLinqExtensions.cs index 935666f..2b65d85 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/CacheLinqExtensions.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/CacheLinqExtensions.cs @@ -193,5 +193,33 @@ namespace Apache.Ignite.Linq return query.Provider.Execute<int>(Expression.Call(null, method, query.Expression, Expression.Quote(predicate))); } + + /// <summary> + /// Updates all rows that are matched by the specified query. + /// <para /> + /// This method results in "UPDATE" distributed SQL query, performing bulk update + /// (as opposed to fetching all rows locally). + /// </summary> + /// <typeparam name="TKey">Key type.</typeparam> + /// <typeparam name="TValue">Value type.</typeparam> + /// <param name="query">The query.</param> + /// <param name="updateDescription">The update description.</param> + /// <returns>Affected row count.</returns> + public static int UpdateAll<TKey, TValue>(this IQueryable<ICacheEntry<TKey, TValue>> query, + Expression<Func<IUpdateDescriptor<TKey,TValue>, IUpdateDescriptor<TKey,TValue>>> updateDescription) + { + IgniteArgumentCheck.NotNull(query, "query"); + IgniteArgumentCheck.NotNull(updateDescription, "updateDescription"); + + var method = UpdateAllExpressionNode.UpdateAllMethodInfo + .MakeGenericMethod(typeof(TKey), typeof(TValue)); // TODO: cache? + + return query.Provider.Execute<int>(Expression.Call(null, method, new[] + { + query.Expression, + Expression.Quote(updateDescription) + })); + } + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/IUpdateDescriptor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/IUpdateDescriptor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/IUpdateDescriptor.cs new file mode 100644 index 0000000..2327b99 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/IUpdateDescriptor.cs @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Apache.Ignite.Linq +{ + using System; + using Apache.Ignite.Core.Cache; + + /// <summary> + /// Dummy interface to provide update description for + /// <see + /// cref="CacheLinqExtensions.UpdateAll{TKey,TValue}" /> + /// </summary> + /// <typeparam name="TKey">Key type.</typeparam> + /// <typeparam name="TValue">Value type.</typeparam> + public interface IUpdateDescriptor<out TKey, out TValue> + { + /// <summary> + /// Specifies member update with constant + /// </summary> + /// <typeparam name="TProp">Member type</typeparam> + /// <param name="selector">Member selector</param> + /// <param name="value">New value</param> + /// <returns></returns> + IUpdateDescriptor<TKey, TValue> Set<TProp>(Func<TValue, TProp> selector, TProp value); + + /// <summary> + /// Specifies member update with expression + /// </summary> + /// <typeparam name="TProp">Member type</typeparam> + /// <param name="selector">Member selector</param> + /// <param name="valueBuilder">New value generator</param> + /// <returns></returns> + IUpdateDescriptor<TKey, TValue> Set<TProp>(Func<TValue, TProp> selector, + Func<ICacheEntry<TKey, TValue>, TProp> valueBuilder); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs index 8902e7c..15710db 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs @@ -20,6 +20,7 @@ namespace Apache.Ignite.Linq.Impl using System; using System.Collections.Generic; using System.Diagnostics; + using System.Linq; using System.Linq.Expressions; using System.Text; using Remotion.Linq.Clauses; @@ -49,11 +50,14 @@ namespace Apache.Ignite.Linq.Impl /// <summary> /// Pushes current aliases to stack. /// </summary> - public void Push() + /// <param name="copyAliases">Flag indicating that current aliases should be copied</param> + public void Push(bool copyAliases) { _stack.Push(_tableAliases); - _tableAliases = new Dictionary<IQuerySource, string>(); + _tableAliases = copyAliases + ? _tableAliases.ToDictionary(p => p.Key, p => p.Value) + : new Dictionary<IQuerySource, string>(); } /// <summary> http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs index 4caefe1..c5c887f 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs @@ -54,21 +54,34 @@ namespace Apache.Ignite.Linq.Impl /** */ private readonly bool _includeAllFields; + /** */ + private readonly bool _visitEntireSubQueryModel; + /// <summary> /// Initializes a new instance of the <see cref="CacheQueryExpressionVisitor" /> class. /// </summary> /// <param name="modelVisitor">The _model visitor.</param> - /// <param name="useStar">Flag indicating that star '*' qualifier should be used - /// for the whole-table select instead of _key, _val.</param> - /// <param name="includeAllFields">Flag indicating that star '*' qualifier should be used - /// for the whole-table select as well as _key, _val.</param> - public CacheQueryExpressionVisitor(CacheQueryModelVisitor modelVisitor, bool useStar, bool includeAllFields) + /// <param name="useStar"> + /// Flag indicating that star '*' qualifier should be used + /// for the whole-table select instead of _key, _val. + /// </param> + /// <param name="includeAllFields"> + /// Flag indicating that star '*' qualifier should be used + /// for the whole-table select as well as _key, _val. + /// </param> + /// <param name="visitEntireSubQueryModel"> + /// Flag indicating that subquery + /// should be visited as full query + /// </param> + public CacheQueryExpressionVisitor(CacheQueryModelVisitor modelVisitor, bool useStar, bool includeAllFields, + bool visitEntireSubQueryModel) { Debug.Assert(modelVisitor != null); _modelVisitor = modelVisitor; _useStar = useStar; _includeAllFields = includeAllFields; + _visitEntireSubQueryModel = visitEntireSubQueryModel; } /// <summary> @@ -548,12 +561,18 @@ namespace Apache.Ignite.Linq.Impl var subQueryModel = expression.QueryModel; var contains = subQueryModel.ResultOperators.FirstOrDefault() as ContainsResultOperator; - + // Check if IEnumerable.Contains is used. - if (subQueryModel.ResultOperators.Count == 1 && contains != null) + if (subQueryModel.ResultOperators.Count == 1 && contains != null) { VisitContains(subQueryModel, contains); } + else if (_visitEntireSubQueryModel) + { + ResultBuilder.Append("("); + _modelVisitor.VisitQueryModel(subQueryModel, false, true); + ResultBuilder.Append(")"); + } else { // This happens when New expression uses a subquery, in a GroupBy. @@ -579,7 +598,15 @@ namespace Apache.Ignite.Linq.Impl Visit(contains.Item); ResultBuilder.Append(" IN ("); - _modelVisitor.VisitQueryModel(subQueryModel); + if (_visitEntireSubQueryModel) + { + _modelVisitor.VisitQueryModel(subQueryModel, false, true); + } + else + { + _modelVisitor.VisitQueryModel(subQueryModel); + } + ResultBuilder.Append(")"); } else http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs index 223eea6..da250e9 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs @@ -103,26 +103,30 @@ namespace Apache.Ignite.Linq.Impl /// <summary> /// Visits the query model. /// </summary> - private void VisitQueryModel(QueryModel queryModel, bool includeAllFields) + internal void VisitQueryModel(QueryModel queryModel, bool includeAllFields, bool copyAliases = false) { - _aliases.Push(); + _aliases.Push(copyAliases); - var hasDelete = VisitRemoveOperator(queryModel); - - if (!hasDelete) + var lastResultOp = queryModel.ResultOperators.LastOrDefault(); + if (lastResultOp is RemoveAllResultOperator) + { + VisitRemoveOperator(queryModel); + } + else if(lastResultOp is UpdateAllResultOperator) + { + VisitUpdateAllOperator(queryModel); + } + else { // SELECT _builder.Append("select "); // TOP 1 FLD1, FLD2 VisitSelectors(queryModel, includeAllFields); - } - // FROM ... WHERE ... JOIN ... - base.VisitQueryModel(queryModel); + // FROM ... WHERE ... JOIN ... + base.VisitQueryModel(queryModel); - if (!hasDelete) - { // UNION ... ProcessResultOperatorsEnd(queryModel); } @@ -131,42 +135,68 @@ namespace Apache.Ignite.Linq.Impl } /// <summary> - /// Visits the remove operator. Returns true if it is present. + /// Visits the remove operator. /// </summary> - private bool VisitRemoveOperator(QueryModel queryModel) + private void VisitRemoveOperator(QueryModel queryModel) { var resultOps = queryModel.ResultOperators; - if (resultOps.LastOrDefault() is RemoveAllResultOperator) + _builder.Append("delete "); + + if (resultOps.Count == 2) { - _builder.Append("delete "); + var resOp = resultOps[0] as TakeResultOperator; - if (resultOps.Count == 2) - { - var resOp = resultOps[0] as TakeResultOperator; + if (resOp == null) + throw new NotSupportedException( + "RemoveAll can not be combined with result operators (other than Take): " + + resultOps[0].GetType().Name); - if (resOp == null) - { - throw new NotSupportedException( - "RemoveAll can not be combined with result operators (other than Take): " + - resultOps[0].GetType().Name); - } + _builder.Append("top "); + BuildSqlExpression(resOp.Count); + _builder.Append(" "); + } + else if (resultOps.Count > 2) + { + throw new NotSupportedException( + "RemoveAll can not be combined with result operators (other than Take): " + + string.Join(", ", resultOps.Select(x => x.GetType().Name))); + } - _builder.Append("top "); - BuildSqlExpression(resOp.Count); - _builder.Append(" "); - } - else if (resultOps.Count > 2) - { + // FROM ... WHERE ... JOIN ... + base.VisitQueryModel(queryModel); + } + + /// <summary> + /// Visits the UpdateAll operator. + /// </summary> + private void VisitUpdateAllOperator(QueryModel queryModel) + { + var resultOps = queryModel.ResultOperators; + + _builder.Append("update "); + + // FROM ... WHERE ... + base.VisitQueryModel(queryModel); + + if (resultOps.Count == 2) + { + var resOp = resultOps[0] as TakeResultOperator; + + if (resOp == null) throw new NotSupportedException( - "RemoveAll can not be combined with result operators (other than Take): " + - string.Join(", ", resultOps.Select(x => x.GetType().Name))); - } + "UpdateAll can not be combined with result operators (other than Take): " + + resultOps[0].GetType().Name); - return true; + _builder.Append("limit "); + BuildSqlExpression(resOp.Count); + } + else if (resultOps.Count > 2) + { + throw new NotSupportedException( + "UpdateAll can not be combined with result operators (other than Take): " + + string.Join(", ", resultOps.Select(x => x.GetType().Name))); } - - return false; } /// <summary> @@ -394,18 +424,28 @@ namespace Apache.Ignite.Linq.Impl { base.VisitMainFromClause(fromClause, queryModel); - _builder.AppendFormat("from "); + var isUpdateQuery = queryModel.ResultOperators.LastOrDefault() is UpdateAllResultOperator; + if (!isUpdateQuery) + { + _builder.Append("from "); + } + ValidateFromClause(fromClause); _aliases.AppendAsClause(_builder, fromClause).Append(" "); var i = 0; foreach (var additionalFrom in queryModel.BodyClauses.OfType<AdditionalFromClause>()) { - _builder.AppendFormat(", "); + _builder.Append(", "); ValidateFromClause(additionalFrom); VisitAdditionalFromClause(additionalFrom, queryModel, i++); } + + if (isUpdateQuery) + { + BuildSetClauseForUpdateAll(queryModel); + } } /// <summary> @@ -535,7 +575,6 @@ namespace Apache.Ignite.Linq.Impl } } - /// <summary> /// Visists Join clause in case of join with local collection /// </summary> @@ -651,9 +690,33 @@ namespace Apache.Ignite.Linq.Impl /// <summary> /// Builds the SQL expression. /// </summary> - private void BuildSqlExpression(Expression expression, bool useStar = false, bool includeAllFields = false) + private void BuildSqlExpression(Expression expression, bool useStar = false, bool includeAllFields = false, + bool visitSubqueryModel = false) + { + new CacheQueryExpressionVisitor(this, useStar, includeAllFields, visitSubqueryModel).Visit(expression); + } + + /// <summary> + /// Builds SET clause of UPDATE statement + /// </summary> + private void BuildSetClauseForUpdateAll(QueryModel queryModel) { - new CacheQueryExpressionVisitor(this, useStar, includeAllFields).Visit(expression); + var updateAllResultOperator = queryModel.ResultOperators.LastOrDefault() as UpdateAllResultOperator; + if (updateAllResultOperator != null) + { + _builder.Append("set "); + var first = true; + foreach (var update in updateAllResultOperator.Updates) + { + if (!first) _builder.Append(", "); + first = false; + BuildSqlExpression(update.Selector); + _builder.Append(" = "); + BuildSqlExpression(update.Value, visitSubqueryModel: true); + } + + _builder.Append(" "); + } } } } http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs index 17ec0a3..3f69cce 100644 --- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs @@ -66,6 +66,9 @@ namespace Apache.Ignite.Linq.Impl methodInfoRegistry.Register(RemoveAllExpressionNode.GetSupportedMethods(), typeof(RemoveAllExpressionNode)); + methodInfoRegistry.Register(UpdateAllExpressionNode.GetSupportedMethods(), + typeof(UpdateAllExpressionNode)); + return new CompoundNodeTypeProvider(new INodeTypeProvider[] { methodInfoRegistry, http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/MemberUpdateContainer.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/MemberUpdateContainer.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/MemberUpdateContainer.cs new file mode 100644 index 0000000..bf8a5fd --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/MemberUpdateContainer.cs @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +namespace Apache.Ignite.Linq.Impl.Dml +{ + using System.Linq.Expressions; + + /// <summary> + /// Contains information about member update + /// </summary> + internal struct MemberUpdateContainer + { + /// <summary> + /// Gets or sets member selector + /// </summary> + public Expression Selector { get; set; } + + /// <summary> + /// Gets or sets member new value + /// </summary> + public Expression Value { get; set; } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllExpressionNode.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllExpressionNode.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllExpressionNode.cs new file mode 100644 index 0000000..8cfb552 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllExpressionNode.cs @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Apache.Ignite.Linq.Impl.Dml +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Remotion.Linq.Clauses; + using Remotion.Linq.Clauses.Expressions; + using Remotion.Linq.Parsing.ExpressionVisitors; + using Remotion.Linq.Parsing.Structure.IntermediateModel; + + /// <summary> + /// Represents a <see cref="MethodCallExpression" /> for + /// <see cref="CacheLinqExtensions.UpdateAll{TKey,TValue}" />. + /// When user calls UpdateAll, this node is generated. + /// </summary> + internal sealed class UpdateAllExpressionNode : ResultOperatorExpressionNodeBase + { + /// <summary> + /// The UpdateAll method. + /// </summary> + public static readonly MethodInfo UpdateAllMethodInfo = typeof(CacheLinqExtensions) + .GetMethods() + .Single(x => x.Name == "UpdateAll"); + + //** */ + private static readonly MethodInfo[] SupportedMethods = {UpdateAllMethodInfo}; + + //** */ + private readonly LambdaExpression _updateDescription; + + /// <summary> + /// Initializes a new instance of the <see cref="UpdateAllExpressionNode" /> class. + /// </summary> + /// <param name="parseInfo">The parse information.</param> + /// <param name="updateDescription">Expression with update description info</param> + public UpdateAllExpressionNode(MethodCallExpressionParseInfo parseInfo, + LambdaExpression updateDescription) + : base(parseInfo, null, null) + { + _updateDescription = updateDescription; + } + + /** <inheritdoc /> */ + [ExcludeFromCodeCoverage] + public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, + ClauseGenerationContext clauseGenerationContext) + { + throw CreateResolveNotSupportedException(); + } + + /** <inheritdoc /> */ + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + if (_updateDescription.Parameters.Count != 1) + throw new NotSupportedException("Expression is not supported for UpdateAll: " + + _updateDescription); + + var querySourceRefExpression = (QuerySourceReferenceExpression) Source.Resolve( + _updateDescription.Parameters[0], + _updateDescription.Parameters[0], + clauseGenerationContext); + + var cacheEntryType = querySourceRefExpression.Type; + var querySourceAccessValue = + Expression.MakeMemberAccess(querySourceRefExpression, cacheEntryType.GetMember("Value").First()); + + if (!(_updateDescription.Body is MethodCallExpression)) + throw new NotSupportedException("Expression is not supported for UpdateAll: " + + _updateDescription.Body); + + var updates = new List<MemberUpdateContainer>(); + + var methodCall = (MethodCallExpression) _updateDescription.Body; + while (methodCall != null) + { + if (methodCall.Arguments.Count != 2) + throw new NotSupportedException("Method is not supported for UpdateAll: " + methodCall); + + var selectorLambda = (LambdaExpression) methodCall.Arguments[0]; + var selector = ReplacingExpressionVisitor.Replace(selectorLambda.Parameters[0], querySourceAccessValue, + selectorLambda.Body); + + var newValue = methodCall.Arguments[1]; + switch (newValue.NodeType) + { + case ExpressionType.Constant: + break; + case ExpressionType.Lambda: + var newValueLambda = (LambdaExpression) newValue; + newValue = ReplacingExpressionVisitor.Replace(newValueLambda.Parameters[0], + querySourceRefExpression, newValueLambda.Body); + break; + default: + throw new NotSupportedException("Value expression is not supported for UpdateAll: " + + newValue); + } + + updates.Add(new MemberUpdateContainer + { + Selector = selector, + Value = newValue + }); + + methodCall = methodCall.Object as MethodCallExpression; + } + + return new UpdateAllResultOperator(updates); + } + + /// <summary> + /// Gets the supported methods. + /// </summary> + public static IEnumerable<MethodInfo> GetSupportedMethods() + { + return SupportedMethods; + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ed76af24/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllResultOperator.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllResultOperator.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllResultOperator.cs new file mode 100644 index 0000000..be682b0 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/Dml/UpdateAllResultOperator.cs @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Apache.Ignite.Linq.Impl.Dml +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + using Remotion.Linq.Clauses; + using Remotion.Linq.Clauses.ResultOperators; + using Remotion.Linq.Clauses.StreamedData; + + /// <summary> + /// Represents an operator for <see cref="CacheLinqExtensions.UpdateAll{TKey,TValue}"/>. + /// </summary> + internal sealed class UpdateAllResultOperator : ValueFromSequenceResultOperatorBase + { + /// <summary> + /// Gets the members updates + /// </summary> + public MemberUpdateContainer[] Updates { get; private set; } + + /// <summary> + /// Initializes a new instance of <see cref="UpdateAllResultOperator" /> + /// </summary> + /// <param name="updates">members updates</param> + public UpdateAllResultOperator(IEnumerable<MemberUpdateContainer> updates) + { + Updates = updates.ToArray(); + } + + /** <inheritdoc /> */ + public override IStreamedDataInfo GetOutputDataInfo(IStreamedDataInfo inputInfo) + { + return new StreamedScalarValueInfo(typeof(int)); + } + + /** <inheritdoc /> */ + [ExcludeFromCodeCoverage] + public override ResultOperatorBase Clone(CloneContext cloneContext) + { + return new UpdateAllResultOperator(Updates); + } + + /** <inheritdoc /> */ + [ExcludeFromCodeCoverage] + public override void TransformExpressions(Func<Expression, Expression> transformation) + { + // No-op. + } + + /** <inheritdoc /> */ + [ExcludeFromCodeCoverage] + public override StreamedValue ExecuteInMemory<T>(StreamedSequence sequence) + { + throw new NotSupportedException("UpdateAll is not supported for in-memory sequences."); + } + } +} \ No newline at end of file