blob: da250e9a7162106c7a8425eaa6661037b8075b90 [file] [log] [blame]
/*
* 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.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Apache.Ignite.Linq.Impl.Dml;
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 AliasDictionary _aliases = new AliasDictionary();
/** */
private static readonly Type DefaultIfEmptyEnumeratorType = new object[0]
.DefaultIfEmpty()
.GetType()
.GetGenericTypeDefinition();
/// <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);
}
/// <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 aliases.
/// </summary>
public AliasDictionary Aliases
{
get { return _aliases; }
}
/** <inheritdoc /> */
public override void VisitQueryModel(QueryModel queryModel)
{
VisitQueryModel(queryModel, false);
}
/// <summary>
/// Visits the query model.
/// </summary>
internal void VisitQueryModel(QueryModel queryModel, bool includeAllFields, bool copyAliases = false)
{
_aliases.Push(copyAliases);
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);
// UNION ...
ProcessResultOperatorsEnd(queryModel);
}
_aliases.Pop();
}
/// <summary>
/// Visits the remove operator.
/// </summary>
private void VisitRemoveOperator(QueryModel queryModel)
{
var resultOps = queryModel.ResultOperators;
_builder.Append("delete ");
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);
_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)));
}
// 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(
"UpdateAll can not be combined with result operators (other than Take): " +
resultOps[0].GetType().Name);
_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)));
}
}
/// <summary>
/// Visits the selectors.
/// </summary>
public void VisitSelectors(QueryModel queryModel, bool includeAllFields)
{
var parenCount = ProcessResultOperatorsBegin(queryModel);
if (parenCount >= 0)
{
// FIELD1, FIELD2
BuildSqlExpression(queryModel.SelectClause.Selector, parenCount > 0, includeAllFields);
_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 if (op is ContainsResultOperator)
// Should be processed already
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)
if (offset.Count is ParameterExpression)
throw new NotSupportedException("Skip() without Take() is not supported in compiled queries.");
var offsetInt = (int) ((ConstantExpression) offset.Count).Value;
_builder.Append((int.MaxValue - offsetInt).ToString(CultureInfo.InvariantCulture));
}
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++);
i = 0;
foreach (var where in subQuery.QueryModel.BodyClauses.OfType<WhereClause>())
VisitWhereClause(where, i++, false);
// 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);
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.Append(", ");
ValidateFromClause(additionalFrom);
VisitAdditionalFromClause(additionalFrom, queryModel, i++);
}
if (isUpdateQuery)
{
BuildSetClauseForUpdateAll(queryModel);
}
}
/// <summary>
/// Validates from clause.
/// </summary>
// ReSharper disable once UnusedParameter.Local
private static void ValidateFromClause(IFromClause clause)
{
// Only IQueryable can be used in FROM clause. IEnumerable is not supported.
if (!typeof(IQueryable).IsAssignableFrom(clause.FromExpression.Type))
{
throw new NotSupportedException("FROM clause must be IQueryable: " + clause);
}
}
/** <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 queryable = ExpressionWalker.GetCacheQueryable(joinClause, false);
if (queryable != null)
{
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 alias = _aliases.GetTableAlias(subQuery.QueryModel.MainFromClause);
_builder.AppendFormat(") as {0} on (", alias);
}
else
{
var tableName = ExpressionWalker.GetTableNameWithSchema(queryable);
var alias = _aliases.GetTableAlias(joinClause);
_builder.AppendFormat("inner join {0} as {1} on (", tableName, alias);
}
}
else
{
VisitJoinWithLocalCollectionClause(joinClause);
}
BuildJoinCondition(joinClause.InnerKeySelector, joinClause.OuterKeySelector);
_builder.Append(") ");
}
/** <inheritdoc /> */
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel,
int index)
{
base.VisitAdditionalFromClause(fromClause, queryModel, index);
var subQuery = fromClause.FromExpression as SubQueryExpression;
if (subQuery != null)
{
_builder.Append("(");
VisitQueryModel(subQuery.QueryModel, true);
var alias = _aliases.GetTableAlias(subQuery.QueryModel.MainFromClause);
_builder.AppendFormat(") as {0} ", alias);
}
else
{
_aliases.AppendAsClause(_builder, fromClause).Append(" ");
}
}
/// <summary>
/// Visists Join clause in case of join with local collection
/// </summary>
private void VisitJoinWithLocalCollectionClause(JoinClause joinClause)
{
var type = joinClause.InnerSequence.Type;
var itemType = EnumerableHelper.GetIEnumerableItemType(type);
var sqlTypeName = SqlTypes.GetSqlTypeName(itemType);
if (string.IsNullOrWhiteSpace(sqlTypeName))
{
throw new NotSupportedException("Not supported item type for Join with local collection: " + type.Name);
}
var isOuter = false;
var sequenceExpression = joinClause.InnerSequence;
object values;
var subQuery = sequenceExpression as SubQueryExpression;
if (subQuery != null)
{
isOuter = subQuery.QueryModel.ResultOperators.OfType<DefaultIfEmptyResultOperator>().Any();
sequenceExpression = subQuery.QueryModel.MainFromClause.FromExpression;
}
switch (sequenceExpression.NodeType)
{
case ExpressionType.Constant:
var constantValueType = ((ConstantExpression)sequenceExpression).Value.GetType();
if (constantValueType.IsGenericType)
{
isOuter = DefaultIfEmptyEnumeratorType == constantValueType.GetGenericTypeDefinition();
}
values = ExpressionWalker.EvaluateEnumerableValues(sequenceExpression);
break;
case ExpressionType.Parameter:
values = ExpressionWalker.EvaluateExpression<object>(sequenceExpression);
break;
default:
throw new NotSupportedException("Expression not supported for Join with local collection: "
+ sequenceExpression);
}
var tableAlias = _aliases.GetTableAlias(joinClause);
var fieldAlias = _aliases.GetFieldAlias(joinClause.InnerKeySelector);
_builder.AppendFormat("{0} join table ({1} {2} = ?) {3} on (",
isOuter ? "left outer" : "inner",
fieldAlias,
sqlTypeName,
tableAlias);
Parameters.Add(values);
}
/// <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, 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)
{
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(" ");
}
}
}
}