http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/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 new file mode 100644 index 0000000..a78c877 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryModelVisitor.cs @@ -0,0 +1,508 @@ +/* + * 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 +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + using System.Text; + using Remotion.Linq; + using Remotion.Linq.Clauses; + using Remotion.Linq.Clauses.Expressions; + using Remotion.Linq.Clauses.ResultOperators; + + /// <summary> + /// Query visitor, transforms LINQ expression to SQL. + /// </summary> + internal sealed class CacheQueryModelVisitor : QueryModelVisitorBase + { + /** */ + private readonly StringBuilder _builder = new StringBuilder(); + + /** */ + private readonly List<object> _parameters = new List<object>(); + + /** */ + private readonly List<Expression> _parameterExpressions = new List<Expression>(); + + /** */ + private readonly AliasDictionary _aliases = new AliasDictionary(); + + /// <summary> + /// Generates the query. + /// </summary> + public QueryData GenerateQuery(QueryModel queryModel) + { + Debug.Assert(_builder.Length == 0); + Debug.Assert(_parameters.Count == 0); + + VisitQueryModel(queryModel); + + if (char.IsWhiteSpace(_builder[_builder.Length - 1])) + _builder.Remove(_builder.Length - 1, 1); // TrimEnd + + var qryText = _builder.ToString(); + + return new QueryData(qryText, _parameters, _parameterExpressions); + } + + /// <summary> + /// Gets the builder. + /// </summary> + public StringBuilder Builder + { + get { return _builder; } + } + + /// <summary> + /// Gets the parameters. + /// </summary> + public IList<object> Parameters + { + get { return _parameters; } + } + + /// <summary> + /// Gets the parameters. + /// </summary> + public IList<Expression> ParameterExpressions + { + get { return _parameterExpressions; } + } + + /// <summary> + /// Gets the aliases. + /// </summary> + public AliasDictionary Aliases + { + get { return _aliases; } + } + + /** <inheritdoc /> */ + public override void VisitQueryModel(QueryModel queryModel) + { + VisitQueryModel(queryModel, false); + } + + /// <summary> + /// Visits the query model. + /// </summary> + private void VisitQueryModel(QueryModel queryModel, bool forceStar) + { + _aliases.Push(); + + // SELECT + _builder.Append("select "); + + // TOP 1 FLD1, FLD2 + VisitSelectors(queryModel, forceStar); + + // FROM ... WHERE ... JOIN ... + base.VisitQueryModel(queryModel); + + // UNION ... + ProcessResultOperatorsEnd(queryModel); + + _aliases.Pop(); + } + + /// <summary> + /// Visits the selectors. + /// </summary> + public void VisitSelectors(QueryModel queryModel, bool forceStar) + { + var parenCount = ProcessResultOperatorsBegin(queryModel); + + if (parenCount >= 0) + { + // FIELD1, FIELD2 + BuildSqlExpression(queryModel.SelectClause.Selector, forceStar || parenCount > 0); + _builder.Append(')', parenCount).Append(" "); + } + } + + /// <summary> + /// Processes the result operators that come right after SELECT: min/max/count/sum/distinct + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + private int ProcessResultOperatorsBegin(QueryModel queryModel) + { + int parenCount = 0; + + foreach (var op in queryModel.ResultOperators.Reverse()) + { + if (op is CountResultOperator || op is AnyResultOperator) + { + _builder.Append("count ("); + parenCount++; + } + else if (op is SumResultOperator) + { + _builder.Append("sum ("); + parenCount++; + } + else if (op is MinResultOperator) + { + _builder.Append("min ("); + parenCount++; + } + else if (op is MaxResultOperator) + { + _builder.Append("max ("); + parenCount++; + } + else if (op is AverageResultOperator) + { + _builder.Append("avg ("); + parenCount++; + } + else if (op is DistinctResultOperator) + _builder.Append("distinct "); + else if (op is FirstResultOperator || op is SingleResultOperator) + _builder.Append("top 1 "); + else if (op is UnionResultOperator || op is IntersectResultOperator || op is ExceptResultOperator + || op is DefaultIfEmptyResultOperator || op is SkipResultOperator || op is TakeResultOperator) + // Will be processed later + break; + else + throw new NotSupportedException("Operator is not supported: " + op); + } + return parenCount; + } + + /// <summary> + /// Processes the result operators that go in the end of the query: limit/offset/union/intersect/except + /// </summary> + private void ProcessResultOperatorsEnd(QueryModel queryModel) + { + ProcessSkipTake(queryModel); + + foreach (var op in queryModel.ResultOperators.Reverse()) + { + string keyword = null; + Expression source = null; + + var union = op as UnionResultOperator; + if (union != null) + { + keyword = "union"; + source = union.Source2; + } + + var intersect = op as IntersectResultOperator; + if (intersect != null) + { + keyword = "intersect"; + source = intersect.Source2; + } + + var except = op as ExceptResultOperator; + if (except != null) + { + keyword = "except"; + source = except.Source2; + } + + if (keyword != null) + { + _builder.Append(keyword).Append(" ("); + + var subQuery = source as SubQueryExpression; + + if (subQuery != null) // Subquery union + VisitQueryModel(subQuery.QueryModel); + else + { + // Direct cache union, source is ICacheQueryable + var innerExpr = source as ConstantExpression; + + if (innerExpr == null) + throw new NotSupportedException("Unexpected UNION inner sequence: " + source); + + var queryable = innerExpr.Value as ICacheQueryableInternal; + + if (queryable == null) + throw new NotSupportedException("Unexpected UNION inner sequence " + + "(only results of cache.ToQueryable() are supported): " + + innerExpr.Value); + + VisitQueryModel(queryable.GetQueryModel()); + } + + _builder.Append(")"); + } + } + } + + /// <summary> + /// Processes the pagination (skip/take). + /// </summary> + private void ProcessSkipTake(QueryModel queryModel) + { + var limit = queryModel.ResultOperators.OfType<TakeResultOperator>().FirstOrDefault(); + var offset = queryModel.ResultOperators.OfType<SkipResultOperator>().FirstOrDefault(); + + if (limit == null && offset == null) + return; + + // "limit" is mandatory if there is "offset", but not vice versa + _builder.Append("limit "); + + if (limit == null) + { + // Workaround for unlimited offset (IGNITE-2602) + // H2 allows NULL & -1 for unlimited, but Ignite indexing does not + // Maximum limit that works is (int.MaxValue - offset) + var offsetInt = (int) ((ConstantExpression) offset.Count).Value; + _builder.Append((int.MaxValue - offsetInt).ToString()); + } + else + BuildSqlExpression(limit.Count); + + if (offset != null) + { + _builder.Append(" offset "); + BuildSqlExpression(offset.Count); + } + } + + /** <inheritdoc /> */ + protected override void VisitBodyClauses(ObservableCollection<IBodyClause> bodyClauses, QueryModel queryModel) + { + var i = 0; + foreach (var join in bodyClauses.OfType<JoinClause>()) + VisitJoinClause(join, queryModel, i++); + + var hasGroups = ProcessGroupings(queryModel); + + i = 0; + foreach (var where in bodyClauses.OfType<WhereClause>()) + VisitWhereClause(where, i++, hasGroups); + + i = 0; + foreach (var orderBy in bodyClauses.OfType<OrderByClause>()) + VisitOrderByClause(orderBy, queryModel, i++); + } + + /// <summary> + /// Processes the groupings. + /// </summary> + private bool ProcessGroupings(QueryModel queryModel) + { + var subQuery = queryModel.MainFromClause.FromExpression as SubQueryExpression; + + if (subQuery == null) + return false; + + var groupBy = subQuery.QueryModel.ResultOperators.OfType<GroupResultOperator>().FirstOrDefault(); + + if (groupBy == null) + return false; + + // Visit inner joins before grouping + var i = 0; + foreach (var join in subQuery.QueryModel.BodyClauses.OfType<JoinClause>()) + VisitJoinClause(join, queryModel, i++); + + // Append grouping + _builder.Append("group by ("); + + BuildSqlExpression(groupBy.KeySelector); + + _builder.Append(") "); + + return true; + } + + /** <inheritdoc /> */ + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + base.VisitMainFromClause(fromClause, queryModel); + + _builder.AppendFormat("from "); + _aliases.AppendAsClause(_builder, fromClause).Append(" "); + + foreach (var additionalFrom in queryModel.BodyClauses.OfType<AdditionalFromClause>()) + { + _builder.AppendFormat(", "); + _aliases.AppendAsClause(_builder, additionalFrom).Append(" "); + } + } + + /** <inheritdoc /> */ + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) + { + base.VisitWhereClause(whereClause, queryModel, index); + + VisitWhereClause(whereClause, index, false); + } + + /// <summary> + /// Visits the where clause. + /// </summary> + private void VisitWhereClause(WhereClause whereClause, int index, bool hasGroups) + { + _builder.Append(index > 0 + ? "and " + : hasGroups + ? "having" + : "where "); + + BuildSqlExpression(whereClause.Predicate); + + _builder.Append(" "); + } + + /** <inheritdoc /> */ + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] + public override void VisitOrderByClause(OrderByClause orderByClause, QueryModel queryModel, int index) + { + base.VisitOrderByClause(orderByClause, queryModel, index); + + _builder.Append("order by "); + + for (int i = 0; i < orderByClause.Orderings.Count; i++) + { + var ordering = orderByClause.Orderings[i]; + + if (i > 0) + _builder.Append(", "); + + _builder.Append("("); + + BuildSqlExpression(ordering.Expression); + + _builder.Append(")"); + + _builder.Append(ordering.OrderingDirection == OrderingDirection.Asc ? " asc" : " desc"); + } + + _builder.Append(" "); + } + + /** <inheritdoc /> */ + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] + public override void VisitJoinClause(JoinClause joinClause, QueryModel queryModel, int index) + { + base.VisitJoinClause(joinClause, queryModel, index); + + var subQuery = joinClause.InnerSequence as SubQueryExpression; + + if (subQuery != null) + { + var isOuter = subQuery.QueryModel.ResultOperators.OfType<DefaultIfEmptyResultOperator>().Any(); + + _builder.AppendFormat("{0} join (", isOuter ? "left outer" : "inner"); + + VisitQueryModel(subQuery.QueryModel, true); + + var queryable = ExpressionWalker.GetCacheQueryable(subQuery.QueryModel.MainFromClause); + var alias = _aliases.GetTableAlias(queryable); + _builder.AppendFormat(") as {0} on (", alias); + } + else + { + var innerExpr = joinClause.InnerSequence as ConstantExpression; + + if (innerExpr == null) + throw new NotSupportedException("Unexpected JOIN inner sequence (subqueries are not supported): " + + joinClause.InnerSequence); + + if (!(innerExpr.Value is ICacheQueryable)) + throw new NotSupportedException("Unexpected JOIN inner sequence " + + "(only results of cache.ToQueryable() are supported): " + + innerExpr.Value); + + var queryable = ExpressionWalker.GetCacheQueryable(joinClause); + var tableName = ExpressionWalker.GetTableNameWithSchema(queryable); + _builder.AppendFormat("inner join {0} as {1} on (", tableName, _aliases.GetTableAlias(tableName)); + } + + BuildJoinCondition(joinClause.InnerKeySelector, joinClause.OuterKeySelector); + + _builder.Append(") "); + } + + /// <summary> + /// Builds the join condition ('x=y AND foo=bar'). + /// </summary> + /// <param name="innerKey">The inner key selector.</param> + /// <param name="outerKey">The outer key selector.</param> + /// <exception cref="System.NotSupportedException"> + /// </exception> + private void BuildJoinCondition(Expression innerKey, Expression outerKey) + { + var innerNew = innerKey as NewExpression; + var outerNew = outerKey as NewExpression; + + if (innerNew == null && outerNew == null) + { + BuildJoinSubCondition(innerKey, outerKey); + return; + } + + if (innerNew != null && outerNew != null) + { + if (innerNew.Constructor != outerNew.Constructor) + throw new NotSupportedException( + string.Format("Unexpected JOIN condition. Multi-key joins should have " + + "the same initializers on both sides: '{0} = {1}'", innerKey, outerKey)); + + for (var i = 0; i < innerNew.Arguments.Count; i++) + { + if (i > 0) + _builder.Append(" and "); + + BuildJoinSubCondition(innerNew.Arguments[i], outerNew.Arguments[i]); + } + + return; + } + + throw new NotSupportedException( + string.Format("Unexpected JOIN condition. Multi-key joins should have " + + "anonymous type instances on both sides: '{0} = {1}'", innerKey, outerKey)); + } + + /// <summary> + /// Builds the join sub condition. + /// </summary> + /// <param name="innerKey">The inner key.</param> + /// <param name="outerKey">The outer key.</param> + private void BuildJoinSubCondition(Expression innerKey, Expression outerKey) + { + BuildSqlExpression(innerKey); + _builder.Append(" = "); + BuildSqlExpression(outerKey); + } + + /// <summary> + /// Builds the SQL expression. + /// </summary> + private void BuildSqlExpression(Expression expression, bool useStar = false) + { + new CacheQueryExpressionVisitor(this, useStar).Visit(expression); + } + } +}
http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/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 new file mode 100644 index 0000000..cee90f4 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryParser.cs @@ -0,0 +1,56 @@ +/* + * 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 +{ + using System.Threading; + using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; + using Remotion.Linq.Parsing.Structure; + using Remotion.Linq.Parsing.Structure.ExpressionTreeProcessors; + + /// <summary> + /// Cache query parser. + /// </summary> + internal static class CacheQueryParser + { + /** */ + private static readonly ThreadLocal<QueryParser> ThreadLocalInstance = + new ThreadLocal<QueryParser>(CreateParser); + + /// <summary> + /// Gets the default instance for current thread. + /// </summary> + public static QueryParser Instance + { + get { return ThreadLocalInstance.Value; } + } + + /// <summary> + /// Creates the parser. + /// </summary> + private static QueryParser CreateParser() + { + var transformerRegistry = ExpressionTransformerRegistry.CreateDefault(); + + var proc = new TransformingExpressionTreeProcessor(transformerRegistry); + + var parser = new ExpressionTreeParser(ExpressionTreeParser.CreateDefaultNodeTypeProvider(), proc); + + return new QueryParser(parser); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryable.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryable.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryable.cs new file mode 100644 index 0000000..959cc4b --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryable.cs @@ -0,0 +1,43 @@ +/* + * 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 +{ + using System.Linq; + using Apache.Ignite.Core.Cache; + using Apache.Ignite.Core.Impl.Cache; + + /// <summary> + /// <see cref="IQueryable{T}"/> implementation for <see cref="ICache{TK,TV}"/>. + /// </summary> + internal class CacheQueryable<TKey, TValue> : CacheQueryableBase<ICacheEntry<TKey, TValue>> + { + /// <summary> + /// Initializes a new instance of the <see cref="CacheQueryable{TKey, TValue}" /> class. + /// </summary> + /// <param name="cache">The cache.</param> + /// <param name="local">Local flag.</param> + /// <param name="tableName">Name of the table.</param> + public CacheQueryable(ICache<TKey, TValue> cache, bool local, string tableName) + : base(new CacheFieldsQueryProvider(CacheQueryParser.Instance, + new CacheFieldsQueryExecutor((ICacheInternal) cache, local), + cache.Ignite, cache.GetConfiguration(), tableName, typeof(TValue))) + { + // No-op. + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryableBase.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryableBase.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryableBase.cs new file mode 100644 index 0000000..d3115be --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryableBase.cs @@ -0,0 +1,122 @@ +/* + * 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 +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using Apache.Ignite.Core; + using Apache.Ignite.Core.Cache.Configuration; + using Apache.Ignite.Core.Cache.Query; + using Remotion.Linq; + + /// <summary> + /// Base class for cache queryables. + /// </summary> + internal class CacheQueryableBase<T> : QueryableBase<T>, ICacheQueryableInternal + { + /** <inheritdoc /> */ + public CacheQueryableBase(IQueryProvider provider) : base(provider) + { + // No-op. + } + + /** <inheritdoc /> */ + public CacheQueryableBase(IQueryProvider provider, Expression expression) : base(provider, expression) + { + // No-op. + } + + /** <inheritdoc /> */ + public CacheConfiguration CacheConfiguration + { + get { return CacheQueryProvider.CacheConfiguration; } + } + + /** <inheritdoc /> */ + public string CacheName + { + get { return CacheConfiguration.Name; } + } + + /** <inheritdoc /> */ + public IIgnite Ignite + { + get { return CacheQueryProvider.Ignite; } + } + + /** <inheritdoc /> */ + public SqlFieldsQuery GetFieldsQuery() + { + var data = GetQueryData(); + var executor = CacheQueryProvider.Executor; + + return new SqlFieldsQuery(data.QueryText, executor.Local, data.Parameters.ToArray()); + } + + /** <inheritdoc /> */ + public QueryModel GetQueryModel() + { + return CacheQueryProvider.GenerateQueryModel(Expression); + } + + /** <inheritdoc /> */ + public string TableName + { + get { return CacheQueryProvider.TableName; } + } + + /** <inheritdoc /> */ + public Func<object[], IQueryCursor<TQ>> CompileQuery<TQ>(Delegate queryCaller) + { + var executor = CacheQueryProvider.Executor; + + return executor.CompileQuery<TQ>(GetQueryModel(), queryCaller); + } + + /// <summary> + /// Gets the cache query provider. + /// </summary> + private CacheFieldsQueryProvider CacheQueryProvider + { + get { return (CacheFieldsQueryProvider)Provider; } + } + + /// <summary> + /// Gets the query data. + /// </summary> + /// <returns></returns> + private QueryData GetQueryData() + { + var model = GetQueryModel(); + + return CacheFieldsQueryExecutor.GetQueryData(model); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this instance. + /// </returns> + public override string ToString() + { + return GetQueryData().ToString(); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs new file mode 100644 index 0000000..96371cc --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs @@ -0,0 +1,172 @@ +/* + * 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 +{ + using System; + using System.Diagnostics; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Apache.Ignite.Core.Impl.Common; + using Remotion.Linq.Clauses; + using Remotion.Linq.Clauses.Expressions; + + /// <summary> + /// Walks expression trees to extract query and table name info. + /// </summary> + internal static class ExpressionWalker + { + /** Compiled member readers. */ + private static readonly CopyOnWriteConcurrentDictionary<MemberInfo, Func<object, object>> MemberReaders = + new CopyOnWriteConcurrentDictionary<MemberInfo, Func<object, object>>(); + + /// <summary> + /// Gets the cache queryable. + /// </summary> + public static ICacheQueryableInternal GetCacheQueryable(QuerySourceReferenceExpression expression) + { + Debug.Assert(expression != null); + + var fromSource = expression.ReferencedQuerySource as IFromClause; + + if (fromSource != null) + return GetCacheQueryable(fromSource); + + var joinSource = expression.ReferencedQuerySource as JoinClause; + + if (joinSource != null) + return GetCacheQueryable(joinSource); + + throw new NotSupportedException("Unexpected query source: " + expression.ReferencedQuerySource); + } + + /// <summary> + /// Gets the cache queryable. + /// </summary> + public static ICacheQueryableInternal GetCacheQueryable(IFromClause fromClause) + { + return GetCacheQueryable(fromClause.FromExpression); + } + + /// <summary> + /// Gets the cache queryable. + /// </summary> + public static ICacheQueryableInternal GetCacheQueryable(JoinClause joinClause) + { + return GetCacheQueryable(joinClause.InnerSequence); + } + + /// <summary> + /// Gets the cache queryable. + /// </summary> + public static ICacheQueryableInternal GetCacheQueryable(Expression expression, bool throwWhenNotFound = true) + { + var subQueryExp = expression as SubQueryExpression; + + if (subQueryExp != null) + return GetCacheQueryable(subQueryExp.QueryModel.MainFromClause); + + var srcRefExp = expression as QuerySourceReferenceExpression; + + if (srcRefExp != null) + return GetCacheQueryable(srcRefExp); + + var memberExpr = expression as MemberExpression; + + if (memberExpr != null) + { + if (memberExpr.Type.IsGenericType && + memberExpr.Type.GetGenericTypeDefinition() == typeof (IQueryable<>)) + return EvaluateExpression<ICacheQueryableInternal>(memberExpr); + + return GetCacheQueryable(memberExpr.Expression, throwWhenNotFound); + } + + var constExpr = expression as ConstantExpression; + + if (constExpr != null) + { + var queryable = constExpr.Value as ICacheQueryableInternal; + + if (queryable != null) + return queryable; + } + + if (throwWhenNotFound) + throw new NotSupportedException("Unexpected query source: " + expression); + + return null; + } + + /// <summary> + /// Evaluates the expression. + /// </summary> + public static T EvaluateExpression<T>(Expression expr) + { + var memberExpr = expr as MemberExpression; + + if (memberExpr != null) + { + var targetExpr = memberExpr.Expression as ConstantExpression; + + if (memberExpr.Expression == null || targetExpr != null) + { + // Instance or static member + var target = targetExpr == null ? null : targetExpr.Value; + + Func<object, object> reader; + if (MemberReaders.TryGetValue(memberExpr.Member, out reader)) + return (T) reader(target); + + return (T) MemberReaders.GetOrAdd(memberExpr.Member, x => CompileMemberReader(memberExpr))(target); + } + } + + throw new NotSupportedException("Expression not supported: " + expr); + } + + /// <summary> + /// Compiles the member reader. + /// </summary> + private static Func<object, object> CompileMemberReader(MemberExpression memberExpr) + { + // Field or property + var fld = memberExpr.Member as FieldInfo; + + if (fld != null) + return DelegateConverter.CompileFieldGetter(fld); + + var prop = memberExpr.Member as PropertyInfo; + + if (prop != null) + return DelegateConverter.CompilePropertyGetter(prop); + + throw new NotSupportedException("Expression not supported: " + memberExpr); + } + + /// <summary> + /// Gets the table name with schema. + /// </summary> + public static string GetTableNameWithSchema(ICacheQueryableInternal queryable) + { + Debug.Assert(queryable != null); + + return string.Format("\"{0}\".{1}", queryable.CacheConfiguration.Name, queryable.TableName); + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryProxy.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryProxy.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryProxy.cs new file mode 100644 index 0000000..b00937c --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryProxy.cs @@ -0,0 +1,40 @@ +/* + * 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 +{ + using System; + using Apache.Ignite.Core.Binary; + using Apache.Ignite.Core.Cache.Query; + + /// <summary> + /// Cache proxy interface that allows query execution without key/value generic arguments. + /// </summary> + internal interface ICacheQueryProxy + { + /// <summary> + /// Queries separate entry fields. + /// </summary> + /// <typeparam name="T">Type of the result.</typeparam> + /// <param name="qry">SQL fields query.</param> + /// <param name="readerFunc">Reader function, takes raw reader and field count, returns typed result.</param> + /// <returns> + /// Cursor. + /// </returns> + IQueryCursor<T> QueryFields<T>(SqlFieldsQuery qry, Func<IBinaryRawReader, int, T> readerFunc); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryableInternal.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryableInternal.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryableInternal.cs new file mode 100644 index 0000000..3e252c5 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ICacheQueryableInternal.cs @@ -0,0 +1,54 @@ +/* + * 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 +{ + using System; + using Apache.Ignite.Core.Cache.Configuration; + using Apache.Ignite.Core.Cache.Query; + using Remotion.Linq; + + /// <summary> + /// Internal queryable interface. + /// </summary> + internal interface ICacheQueryableInternal : ICacheQueryable + { + /// <summary> + /// Gets the configuration of the cache that is associated with this query. + /// </summary> + /// <value> + /// The configuration of the cache. + /// </value> + CacheConfiguration CacheConfiguration { get; } + + /// <summary> + /// Gets the name of the table. + /// </summary> + string TableName { get; } + + /// <summary> + /// Gets the query model. + /// </summary> + QueryModel GetQueryModel(); + + /// <summary> + /// Compiles the query. + /// </summary> + /// <param name="queryCaller">Caller expression to examine argument order.</param> + Func<object[], IQueryCursor<T>> CompileQuery<T>(Delegate queryCaller); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/MethodVisitor.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/MethodVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/MethodVisitor.cs new file mode 100644 index 0000000..00ba03f --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/MethodVisitor.cs @@ -0,0 +1,249 @@ +/* + * 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 +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using System.Text.RegularExpressions; + + /// <summary> + /// MethodCall expression visitor. + /// </summary> + internal static class MethodVisitor + { + /// <summary> The string length method. </summary> + public static readonly MemberInfo StringLength = typeof (string).GetProperty("Length"); + + /// <summary> Method visit delegate. </summary> + private delegate void VisitMethodDelegate(MethodCallExpression expression, CacheQueryExpressionVisitor visitor); + + /// <summary> + /// Delegates dictionary. + /// </summary> + private static readonly Dictionary<MethodInfo, VisitMethodDelegate> Delegates = new List + <KeyValuePair<MethodInfo, VisitMethodDelegate>> + { + GetStringMethod("ToLower", new Type[0], GetFunc("lower")), + GetStringMethod("ToUpper", new Type[0], GetFunc("upper")), + GetStringMethod("Contains", del: (e, v) => VisitSqlLike(e, v, "'%' || ? || '%'")), + GetStringMethod("StartsWith", new[] {typeof (string)}, (e, v) => VisitSqlLike(e, v, "? || '%'")), + GetStringMethod("EndsWith", new[] {typeof (string)}, (e, v) => VisitSqlLike(e, v, "'%' || ?")), + GetStringMethod("IndexOf", new[] {typeof (string)}, GetFunc("instr", -1)), + GetStringMethod("IndexOf", new[] {typeof (string), typeof (int)}, GetFunc("instr", -1)), + GetStringMethod("Substring", new[] {typeof (int)}, GetFunc("substring", 0, 1)), + GetStringMethod("Substring", new[] {typeof (int), typeof (int)}, GetFunc("substring", 0, 1)), + GetStringMethod("Trim", "trim"), + GetStringMethod("Trim", "trim", typeof(char[])), + GetStringMethod("TrimStart", "ltrim", typeof(char[])), + GetStringMethod("TrimEnd", "rtrim", typeof(char[])), + GetStringMethod("Replace", "replace", typeof(string), typeof(string)), + + GetMethod(typeof (Regex), "Replace", new[] {typeof (string), typeof (string), typeof (string)}, + GetFunc("regexp_replace")), + GetMethod(typeof (DateTime), "ToString", new[] {typeof (string)}, GetFunc("formatdatetime")), + + GetMathMethod("Abs", typeof (int)), + GetMathMethod("Abs", typeof (long)), + GetMathMethod("Abs", typeof (float)), + GetMathMethod("Abs", typeof (double)), + GetMathMethod("Abs", typeof (decimal)), + GetMathMethod("Abs", typeof (sbyte)), + GetMathMethod("Abs", typeof (short)), + GetMathMethod("Acos", typeof (double)), + GetMathMethod("Asin", typeof (double)), + GetMathMethod("Atan", typeof (double)), + GetMathMethod("Atan2", typeof (double), typeof (double)), + GetMathMethod("Ceiling", typeof (double)), + GetMathMethod("Ceiling", typeof (decimal)), + GetMathMethod("Cos", typeof (double)), + GetMathMethod("Cosh", typeof (double)), + GetMathMethod("Exp", typeof (double)), + GetMathMethod("Floor", typeof (double)), + GetMathMethod("Floor", typeof (decimal)), + GetMathMethod("Log", typeof (double)), + GetMathMethod("Log10", typeof (double)), + GetMathMethod("Pow", "Power", typeof (double), typeof (double)), + GetMathMethod("Round", typeof (double)), + GetMathMethod("Round", typeof (double), typeof (int)), + GetMathMethod("Round", typeof (decimal)), + GetMathMethod("Round", typeof (decimal), typeof (int)), + GetMathMethod("Sign", typeof (double)), + GetMathMethod("Sign", typeof (decimal)), + GetMathMethod("Sign", typeof (float)), + GetMathMethod("Sign", typeof (int)), + GetMathMethod("Sign", typeof (long)), + GetMathMethod("Sign", typeof (short)), + GetMathMethod("Sign", typeof (sbyte)), + GetMathMethod("Sin", typeof (double)), + GetMathMethod("Sinh", typeof (double)), + GetMathMethod("Sqrt", typeof (double)), + GetMathMethod("Tan", typeof (double)), + GetMathMethod("Tanh", typeof (double)), + GetMathMethod("Truncate", typeof (double)), + GetMathMethod("Truncate", typeof (decimal)), + }.ToDictionary(x => x.Key, x => x.Value); + + /// <summary> + /// Visits the method call expression. + /// </summary> + public static void VisitMethodCall(MethodCallExpression expression, CacheQueryExpressionVisitor visitor) + { + var mtd = expression.Method; + + VisitMethodDelegate del; + + if (!Delegates.TryGetValue(mtd, out del)) + throw new NotSupportedException(string.Format("Method not supported: {0}.({1})", + mtd.DeclaringType == null ? "static" : mtd.DeclaringType.FullName, mtd)); + + del(expression, visitor); + } + + /// <summary> + /// Gets the function. + /// </summary> + private static VisitMethodDelegate GetFunc(string func, params int[] adjust) + { + return (e, v) => VisitFunc(e, v, func, adjust); + } + + /// <summary> + /// Visits the instance function. + /// </summary> + private static void VisitFunc(MethodCallExpression expression, CacheQueryExpressionVisitor visitor, + string func, params int[] adjust) + { + visitor.ResultBuilder.Append(func).Append("("); + + var isInstanceMethod = expression.Object != null; + + if (isInstanceMethod) + visitor.Visit(expression.Object); + + for (int i= 0; i < expression.Arguments.Count; i++) + { + var arg = expression.Arguments[i]; + + if (isInstanceMethod || (i > 0)) + visitor.ResultBuilder.Append(", "); + + if (arg.NodeType == ExpressionType.NewArrayInit) + { + // Only trim methods use params[], only one param is supported + var args = ((NewArrayExpression) arg).Expressions; + + if (args.Count != 1) + throw new NotSupportedException("Method call only supports a single parameter: "+ expression); + + visitor.Visit(args[0]); + } + else + visitor.Visit(arg); + + AppendAdjustment(visitor, adjust, i + 1); + } + + visitor.ResultBuilder.Append(")"); + + AppendAdjustment(visitor, adjust, 0); + } + + /// <summary> + /// Appends the adjustment. + /// </summary> + private static void AppendAdjustment(CacheQueryExpressionVisitor visitor, int[] adjust, int idx) + { + if (idx < adjust.Length) + { + var delta = adjust[idx]; + + if (delta > 0) + visitor.ResultBuilder.AppendFormat(" + {0}", delta); + else if (delta < 0) + visitor.ResultBuilder.AppendFormat(" {0}", delta); + } + } + + /// <summary> + /// Visits the SQL like expression. + /// </summary> + private static void VisitSqlLike(MethodCallExpression expression, CacheQueryExpressionVisitor visitor, string likeFormat) + { + visitor.ResultBuilder.Append("("); + + visitor.Visit(expression.Object); + + visitor.ResultBuilder.AppendFormat(" like {0}) ", likeFormat); + + var arg = expression.Arguments[0] as ConstantExpression; + + var paramValue = arg != null ? arg.Value : visitor.RegisterEvaluatedParameter(expression.Arguments[0]); + + visitor.Parameters.Add(paramValue); + } + + /// <summary> + /// Gets the method. + /// </summary> + private static KeyValuePair<MethodInfo, VisitMethodDelegate> GetMethod(Type type, string name, + Type[] argTypes = null, VisitMethodDelegate del = null) + { + var method = argTypes == null ? type.GetMethod(name) : type.GetMethod(name, argTypes); + + return new KeyValuePair<MethodInfo, VisitMethodDelegate>(method, del ?? GetFunc(name)); + } + + /// <summary> + /// Gets the string method. + /// </summary> + private static KeyValuePair<MethodInfo, VisitMethodDelegate> GetStringMethod(string name, + Type[] argTypes = null, VisitMethodDelegate del = null) + { + return GetMethod(typeof(string), name, argTypes, del); + } + + /// <summary> + /// Gets the string method. + /// </summary> + private static KeyValuePair<MethodInfo, VisitMethodDelegate> GetStringMethod(string name, string sqlName, + params Type[] argTypes) + { + return GetMethod(typeof(string), name, argTypes, GetFunc(sqlName)); + } + + /// <summary> + /// Gets the math method. + /// </summary> + private static KeyValuePair<MethodInfo, VisitMethodDelegate> GetMathMethod(string name, string sqlName, + params Type[] argTypes) + { + return GetMethod(typeof(Math), name, argTypes, GetFunc(sqlName)); + } + + /// <summary> + /// Gets the math method. + /// </summary> + private static KeyValuePair<MethodInfo, VisitMethodDelegate> GetMathMethod(string name, params Type[] argTypes) + { + return GetMathMethod(name, name, argTypes); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/QueryData.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/QueryData.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/QueryData.cs new file mode 100644 index 0000000..5424692 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/QueryData.cs @@ -0,0 +1,92 @@ +/* + * 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 +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Linq.Expressions; + + /// <summary> + /// Query data holder. + /// </summary> + internal class QueryData + { + /** */ + private readonly ICollection<object> _parameters; + + /** */ + private readonly string _queryText; + + /** */ + private readonly ICollection<Expression> _parameterExpressions; + + /// <summary> + /// Initializes a new instance of the <see cref="QueryData"/> class. + /// </summary> + /// <param name="queryText">The query text.</param> + /// <param name="parameters">The parameters.</param> + /// <param name="parameterExpressions"></param> + public QueryData(string queryText, ICollection<object> parameters, ICollection<Expression> parameterExpressions) + { + Debug.Assert(queryText != null); + Debug.Assert(parameters != null); + Debug.Assert(parameterExpressions != null); + + _queryText = queryText; + _parameters = parameters; + _parameterExpressions = parameterExpressions; + } + + /// <summary> + /// Gets the parameters. + /// </summary> + public ICollection<object> Parameters + { + get { return _parameters; } + } + + /// <summary> + /// Gets the query text. + /// </summary> + public string QueryText + { + get { return _queryText; } + } + + /// <summary> + /// Gets the parameter expressions. + /// </summary> + public ICollection<Expression> ParameterExpressions + { + get { return _parameterExpressions; } + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this instance. + /// </returns> + public override string ToString() + { + return string.Format("SQL Query [Text={0}, Parameters={1}]", QueryText, + string.Join(", ", Parameters.Select(x => x == null ? "null" : x.ToString()))); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/SqlTypes.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/SqlTypes.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/SqlTypes.cs new file mode 100644 index 0000000..abaac21 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/SqlTypes.cs @@ -0,0 +1,63 @@ +/* + * 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 +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// SQL type mapping. + /// </summary> + internal static class SqlTypes + { + /** */ + private static readonly Dictionary<Type, string> NetToSql = new Dictionary<Type, string> + { + {typeof (bool), "boolean"}, + {typeof (byte), "smallint"}, + {typeof (sbyte), "tinyint"}, + {typeof (short), "smallint"}, + {typeof (ushort), "int"}, + {typeof (char), "nvarchar(1)"}, + {typeof (int), "int"}, + {typeof (uint), "bigint"}, + {typeof (long), "bigint"}, + {typeof (ulong), "bigint"}, + {typeof (float), "real"}, + {typeof (double), "double"}, + {typeof (string), "nvarchar"}, + {typeof (decimal), "decimal"}, + {typeof (Guid), "uuid"}, + {typeof (DateTime), "timestamp"}, + {typeof (DateTime?), "timestamp"}, + }; + + /// <summary> + /// Gets the corresponding Java type name. + /// </summary> + public static string GetSqlTypeName(Type type) + { + if (type == null) + return null; + + string res; + + return !NetToSql.TryGetValue(type, out res) ? null : res; + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/NuGet/LINQPad/QueryExample.linq ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/NuGet/LINQPad/QueryExample.linq b/modules/platforms/dotnet/Apache.Ignite.Linq/NuGet/LINQPad/QueryExample.linq new file mode 100644 index 0000000..eedbd7b --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/NuGet/LINQPad/QueryExample.linq @@ -0,0 +1,111 @@ +<Query Kind="Program"> + <NuGetReference>Apache.Ignite.Linq</NuGetReference> + <Namespace>Apache.Ignite.Core</Namespace> + <Namespace>Apache.Ignite.Core.Binary</Namespace> + <Namespace>Apache.Ignite.Core.Cache.Configuration</Namespace> + <Namespace>Apache.Ignite.Core.Cache.Query</Namespace> + <Namespace>Apache.Ignite.Linq</Namespace> +</Query> + +/* +* 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. +*/ + +/// <summary> +/// This example demonstrates put-get operations on Ignite cache +/// with binary values. Note that binary object can be retrieved in +/// fully-deserialized form or in binary object format using special +/// cache projection. +/// </summary> + +void Main() +{ + if (!Environment.Is64BitProcess) + throw new Exception("x64 LINQPad is required to run this sample (see AnyCPU build: http://www.linqpad.net/Download.aspx)"); + + // Force new LINQPad query process to reinit JVM + Util.NewProcess = true; + + // Configure cacheable types + var cfg = new IgniteConfiguration { BinaryConfiguration = new BinaryConfiguration(typeof(Organization), typeof(Person)) }; + + // Start instance + using (var ignite = Ignition.Start(cfg)) + { + // Create and populate organization cache + var orgs = ignite.GetOrCreateCache<int, Organization>(new CacheConfiguration("orgs", + new QueryEntity(typeof(int), typeof(Organization)))); + orgs[1] = new Organization { Name = "Apache", Type = "Private", Size = 5300 }; + orgs[2] = new Organization { Name = "Microsoft", Type = "Private", Size = 110000 }; + orgs[3] = new Organization { Name = "Red Cross", Type = "Non-Profit", Size = 35000 }; + + // Create and populate person cache + var persons = ignite.CreateCache<int, Person>(new CacheConfiguration("persons", typeof(Person))); + persons[1] = new Person { OrgId = 1, Name = "James Wilson" }; + persons[2] = new Person { OrgId = 1, Name = "Daniel Adams" }; + persons[3] = new Person { OrgId = 2, Name = "Christian Moss" }; + persons[4] = new Person { OrgId = 3, Name = "Allison Mathis" }; + persons[5] = new Person { OrgId = 3, Name = "Christopher Adams" }; + + // Create LINQ queryable + // NOTE: You can use LINQ-to-objects on ICache<K,V> instance directly, but this will cause local execution (slow) + var orgsQry = orgs.AsCacheQueryable(); + var personsQry = persons.AsCacheQueryable(); + + // SQL query + var sizeQry = orgsQry.Where(x => x.Value.Size < 100000); + sizeQry.Dump("Organizations with size less than 100K"); + + // Introspection + ((ICacheQueryable)sizeQry).GetFieldsQuery().Sql.Dump("Generated SQL"); + + // SQL query with join + const string orgName = "Apache"; + + var joinQry = + from p in personsQry + from o in orgsQry + where p.Value.OrgId == o.Key && o.Value.Name == orgName + select p; + + joinQry.Dump("Persons working for " + orgName); + ((ICacheQueryable)joinQry).GetFieldsQuery().Sql.Dump("Generated SQL"); + + // Fields query + var fieldsQry = orgsQry.Select(o => new { o.Value.Name, o.Value.Size }).OrderBy(x => x.Size); + fieldsQry.Dump("Fields query"); + ((ICacheQueryable)fieldsQry).GetFieldsQuery().Sql.Dump("Generated SQL"); + } +} + +public class Organization +{ + [QuerySqlField] + public string Name { get; set; } + + public string Type { get; set; } + + [QuerySqlField] + public int Size { get; set;} +} + +public class Person +{ + public string Name { get; set; } + + [QuerySqlField] + public int OrgId { get; set; } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/Properties/AssemblyInfo.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Properties/AssemblyInfo.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..945e107 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +/* + * 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. + */ + +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Apache.Ignite.Linq")] +[assembly: AssemblyDescription("Apache Ignite.NET LINQ Provider")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apache Software Foundation")] +[assembly: AssemblyProduct("Apache Ignite.NET")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5b571661-17f4-4f29-8c7d-0edb38ca9b55")] + +[assembly: AssemblyVersion("1.6.0.8653")] +[assembly: AssemblyFileVersion("1.6.0.8653")] +[assembly: AssemblyInformationalVersion("1.6.0")] + +[assembly: CLSCompliant(true)] http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.Linq/packages.config ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/packages.config b/modules/platforms/dotnet/Apache.Ignite.Linq/packages.config new file mode 100644 index 0000000..dce6508 --- /dev/null +++ b/modules/platforms/dotnet/Apache.Ignite.Linq/packages.config @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + 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. +--> + +<packages> + <package id="Remotion.Linq" version="2.0.1" targetFramework="net40" /> +</packages> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/Apache.Ignite.sln ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/Apache.Ignite.sln b/modules/platforms/dotnet/Apache.Ignite.sln index f323a28..407ea27 100644 --- a/modules/platforms/dotnet/Apache.Ignite.sln +++ b/modules/platforms/dotnet/Apache.Ignite.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Ignite.Core", "Apache.Ignite.Core\Apache.Ignite.Core.csproj", "{4CD2F726-7E2B-46C4-A5BA-057BB82EECB6}" @@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.txt = README.txt EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Ignite.Linq", "Apache.Ignite.Linq\Apache.Ignite.Linq.csproj", "{5B571661-17F4-4F29-8C7D-0EDB38CA9B55}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -133,6 +135,18 @@ Global {8F507DBE-56F9-437F-82D4-74C02EC44E41}.Release|x64.Build.0 = Release|Any CPU {8F507DBE-56F9-437F-82D4-74C02EC44E41}.Release|x86.ActiveCfg = Release|Any CPU {8F507DBE-56F9-437F-82D4-74C02EC44E41}.Release|x86.Build.0 = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|x64.Build.0 = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Debug|x86.Build.0 = Debug|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|Any CPU.Build.0 = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|x64.ActiveCfg = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|x64.Build.0 = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|x86.ActiveCfg = Release|Any CPU + {5B571661-17F4-4F29-8C7D-0EDB38CA9B55}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Apache.Ignite.Examples.csproj ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Apache.Ignite.Examples.csproj b/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Apache.Ignite.Examples.csproj index 14d0494..ab0a9f2 100644 --- a/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Apache.Ignite.Examples.csproj +++ b/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Apache.Ignite.Examples.csproj @@ -40,6 +40,10 @@ <HintPath Condition="Exists('..\..\Apache.Ignite')">..\..\Apache.Ignite\bin\$(Configuration)\Apache.Ignite.Core.dll</HintPath> <HintPath Condition="Exists('..\..\bin\Apache.Ignite.Core.dll')">..\..\bin\Apache.Ignite.Core.dll</HintPath> </Reference> + <Reference Include="Apache.Ignite.Linq"> + <HintPath Condition="Exists('..\..\Apache.Ignite.Linq')">..\..\Apache.Ignite.Linq\bin\$(Configuration)\Apache.Ignite.Linq.dll</HintPath> + <HintPath Condition="Exists('..\..\bin\Apache.Ignite.Linq.dll')">..\..\bin\Apache.Ignite.Linq.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> </ItemGroup> @@ -49,6 +53,7 @@ <Compile Include="Datagrid\ContinuousQueryExample.cs" /> <Compile Include="Datagrid\DataStreamerExample.cs" /> <Compile Include="Datagrid\PutGetExample.cs" /> + <Compile Include="Datagrid\LinqExample.cs" /> <Compile Include="Datagrid\QueryExample.cs" /> <Compile Include="Datagrid\StoreExample.cs" /> <Compile Include="Datagrid\TransactionExample.cs" /> http://git-wip-us.apache.org/repos/asf/ignite/blob/ef642e91/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Datagrid/LinqExample.cs ---------------------------------------------------------------------- diff --git a/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Datagrid/LinqExample.cs b/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Datagrid/LinqExample.cs new file mode 100644 index 0000000..a4dddcd --- /dev/null +++ b/modules/platforms/dotnet/examples/Apache.Ignite.Examples/Datagrid/LinqExample.cs @@ -0,0 +1,253 @@ +/* + * 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. + */ + +using System; +using System.Linq; +using System.Collections.Generic; + +using Apache.Ignite.Core; +using Apache.Ignite.Linq; +using Apache.Ignite.Core.Cache; +using Apache.Ignite.Core.Cache.Configuration; +using Apache.Ignite.Core.Cache.Query; +using Apache.Ignite.ExamplesDll.Binary; + +namespace Apache.Ignite.Examples.Datagrid +{ + /// <summary> + /// This example populates cache with sample data and runs several LINQ queries over this data. + /// <para /> + /// 1) Build the project Apache.Ignite.ExamplesDll (select it -> right-click -> Build). + /// Apache.Ignite.ExamplesDll.dll must appear in %IGNITE_HOME%/platforms/dotnet/examples/Apache.Ignite.ExamplesDll/bin/${Platform]/${Configuration} folder. + /// 2) Set this class as startup object (Apache.Ignite.Examples project -> right-click -> Properties -> + /// Application -> Startup object); + /// 3) Start example (F5 or Ctrl+F5). + /// <para /> + /// This example can be run with standalone Apache Ignite.NET node: + /// 1) Run %IGNITE_HOME%/platforms/dotnet/bin/Apache.Ignite.exe: + /// Apache.Ignite.exe -IgniteHome="%IGNITE_HOME%" -springConfigUrl=platforms\dotnet\examples\config\examples-config.xml -assembly=[path_to_Apache.Ignite.ExamplesDll.dll] + /// 2) Start example. + /// </summary> + public class LinqExample + { + /// <summary>Cache name.</summary> + private const string CacheName = "dotnet_cache_query"; + + [STAThread] + public static void Main() + { + var cfg = new IgniteConfiguration + { + SpringConfigUrl = @"platforms\dotnet\examples\config\examples-config.xml", + JvmOptions = new List<string> { "-Xms512m", "-Xmx512m" } + }; + + using (var ignite = Ignition.Start(cfg)) + { + Console.WriteLine(); + Console.WriteLine(">>> Cache LINQ example started."); + + var cache = ignite.GetOrCreateCache<object, object>(new CacheConfiguration + { + Name = CacheName, + QueryEntities = new[] + { + new QueryEntity(typeof(int), typeof(Organization)), + new QueryEntity(typeof(EmployeeKey), typeof(Employee)) + } + }); + + // Clean up caches on all nodes before run. + cache.Clear(); + + // Populate cache with sample data entries. + PopulateCache(cache); + + // Create cache that will work with specific types. + var employeeCache = ignite.GetCache<EmployeeKey, Employee>(CacheName); + var organizationCache = ignite.GetCache<int, Organization>(CacheName); + + // Run SQL query example. + QueryExample(employeeCache); + + // Run compiled SQL query example. + CompiledQueryExample(employeeCache); + + // Run SQL query with join example. + JoinQueryExample(employeeCache, organizationCache); + + // Run SQL fields query example. + FieldsQueryExample(employeeCache); + + Console.WriteLine(); + } + + Console.WriteLine(); + Console.WriteLine(">>> Example finished, press any key to exit ..."); + Console.ReadKey(); + } + + /// <summary> + /// Queries employees that have provided ZIP code in address. + /// </summary> + /// <param name="cache">Cache.</param> + private static void QueryExample(ICache<EmployeeKey, Employee> cache) + { + const int zip = 94109; + + IQueryable<ICacheEntry<EmployeeKey, Employee>> qry = + cache.AsCacheQueryable().Where(emp => emp.Value.Address.Zip == zip); + + Console.WriteLine(); + Console.WriteLine(">>> Employees with zipcode " + zip + ":"); + + foreach (ICacheEntry<EmployeeKey, Employee> entry in qry) + Console.WriteLine(">>> " + entry.Value); + } + + /// <summary> + /// Queries employees that have provided ZIP code in address with a compiled query. + /// </summary> + /// <param name="cache">Cache.</param> + private static void CompiledQueryExample(ICache<EmployeeKey, Employee> cache) + { + const int zip = 94109; + + // Compile cache query to eliminate LINQ overhead on multiple runs. + Func<int, IQueryCursor<ICacheEntry<EmployeeKey, Employee>>> qry = + CompiledQuery.Compile((int z) => cache.AsCacheQueryable().Where(emp => emp.Value.Address.Zip == z)); + + Console.WriteLine(); + Console.WriteLine(">>> Employees with zipcode using compiled query " + zip + ":"); + + foreach (ICacheEntry<EmployeeKey, Employee> entry in qry(zip)) + Console.WriteLine(">>> " + entry.Value); + } + + /// <summary> + /// Queries employees that work for organization with provided name. + /// </summary> + /// <param name="employeeCache">Employee cache.</param> + /// <param name="organizationCache">Organization cache.</param> + private static void JoinQueryExample(ICache<EmployeeKey, Employee> employeeCache, + ICache<int, Organization> organizationCache) + { + const string orgName = "Apache"; + + IQueryable<ICacheEntry<EmployeeKey, Employee>> employees = employeeCache.AsCacheQueryable(); + IQueryable<ICacheEntry<int, Organization>> organizations = organizationCache.AsCacheQueryable(); + + IQueryable<ICacheEntry<EmployeeKey, Employee>> qry = + from employee in employees + from organization in organizations + where employee.Key.OrganizationId == organization.Key && organization.Value.Name == orgName + select employee; + + + Console.WriteLine(); + Console.WriteLine(">>> Employees working for " + orgName + ":"); + + foreach (ICacheEntry<EmployeeKey, Employee> entry in qry) + Console.WriteLine(">>> " + entry.Value); + } + + /// <summary> + /// Queries names and salaries for all employees. + /// </summary> + /// <param name="cache">Cache.</param> + private static void FieldsQueryExample(ICache<EmployeeKey, Employee> cache) + { + var qry = cache.AsCacheQueryable().Select(entry => new {entry.Value.Name, entry.Value.Salary}); + + Console.WriteLine(); + Console.WriteLine(">>> Employee names and their salaries:"); + + foreach (var row in qry) + Console.WriteLine(">>> [Name=" + row.Name + ", salary=" + row.Salary + ']'); + } + + /// <summary> + /// Populate cache with data for this example. + /// </summary> + /// <param name="cache">Cache.</param> + private static void PopulateCache(ICache<object, object> cache) + { + cache.Put(1, new Organization( + "Apache", + new Address("1065 East Hillsdale Blvd, Foster City, CA", 94404), + OrganizationType.Private, + DateTime.Now + )); + + cache.Put(2, new Organization( + "Microsoft", + new Address("1096 Eddy Street, San Francisco, CA", 94109), + OrganizationType.Private, + DateTime.Now + )); + + cache.Put(new EmployeeKey(1, 1), new Employee( + "James Wilson", + 12500, + new Address("1096 Eddy Street, San Francisco, CA", 94109), + new List<string> { "Human Resources", "Customer Service" } + )); + + cache.Put(new EmployeeKey(2, 1), new Employee( + "Daniel Adams", + 11000, + new Address("184 Fidler Drive, San Antonio, TX", 78130), + new List<string> { "Development", "QA" } + )); + + cache.Put(new EmployeeKey(3, 1), new Employee( + "Cristian Moss", + 12500, + new Address("667 Jerry Dove Drive, Florence, SC", 29501), + new List<string> { "Logistics" } + )); + + cache.Put(new EmployeeKey(4, 2), new Employee( + "Allison Mathis", + 25300, + new Address("2702 Freedom Lane, San Francisco, CA", 94109), + new List<string> { "Development" } + )); + + cache.Put(new EmployeeKey(5, 2), new Employee( + "Breana Robbin", + 6500, + new Address("3960 Sundown Lane, Austin, TX", 78130), + new List<string> { "Sales" } + )); + + cache.Put(new EmployeeKey(6, 2), new Employee( + "Philip Horsley", + 19800, + new Address("2803 Elsie Drive, Sioux Falls, SD", 57104), + new List<string> { "Sales" } + )); + + cache.Put(new EmployeeKey(7, 2), new Employee( + "Brian Peters", + 10600, + new Address("1407 Pearlman Avenue, Boston, MA", 12110), + new List<string> { "Development", "QA" } + )); + } + } +}
