//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner Microsoft //--------------------------------------------------------------------- namespace System.Data.Objects.Internal { using System.Collections.Generic; using System.Data.Common; using System.Data.Common.Utils; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Text; /// /// Provides Entity-SQL query building services for . /// Knowledge of how to compose Entity-SQL fragments using query builder operators resides entirely in this class. /// internal static class EntitySqlQueryBuilder { /// /// Helper method to extract the Entity-SQL command text from an instance if that /// instance models an Entity-SQL-backed ObjectQuery, or to throw an exception indicating that query builder methods /// are not supported on this query. /// /// The instance from which the Entity-SQL command text should be retrieved /// The Entity-SQL command text, if the specified query state instance is based on Entity-SQL /// /// If the specified instance is not based on Entity-SQL command text, and so does not support Entity-SQL query builder methods /// private static string GetCommandText(ObjectQueryState query) { string commandText = null; if(!query.TryGetCommandText(out commandText)) { throw EntityUtil.NotSupported(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_NotSupportedLinqSource); } return commandText; } /// /// Merges s from a source ObjectQuery with ObjectParameters specified as an argument to a builder method. /// A new is returned that contains copies of parameters from both and . /// /// The to use when constructing the new parameter collection /// ObjectParameters from the ObjectQuery on which the query builder method was called /// ObjectParameters that were specified as an argument to the builder method /// A new ObjectParameterCollection containing copies of all parameters private static ObjectParameterCollection MergeParameters(ObjectContext context, ObjectParameterCollection sourceQueryParams, ObjectParameter[] builderMethodParams) { Debug.Assert(builderMethodParams != null, "params array argument should not be null"); if (sourceQueryParams == null && builderMethodParams.Length == 0) { return null; } ObjectParameterCollection mergedParams = ObjectParameterCollection.DeepCopy(sourceQueryParams); if (mergedParams == null) { mergedParams = new ObjectParameterCollection(context.Perspective); } foreach (ObjectParameter builderParam in builderMethodParams) { mergedParams.Add(builderParam); } return mergedParams; } /// /// Merges s from two ObjectQuery arguments to SetOp builder methods (Except, Intersect, Union, UnionAll). /// A new is returned that contains copies of parameters from both and . /// /// ObjectParameters from the first ObjectQuery argument (on which the query builder method was called) /// ObjectParameters from the second ObjectQuery argument (specified as an argument to the builder method) /// A new ObjectParameterCollection containing copies of all parameters private static ObjectParameterCollection MergeParameters(ObjectParameterCollection query1Params, ObjectParameterCollection query2Params) { if (query1Params == null && query2Params == null) { return null; } ObjectParameterCollection mergedParams; ObjectParameterCollection sourceParams; if (query1Params != null) { mergedParams = ObjectParameterCollection.DeepCopy(query1Params); sourceParams = query2Params; } else { mergedParams = ObjectParameterCollection.DeepCopy(query2Params); sourceParams = query1Params; } if (sourceParams != null) { foreach (ObjectParameter sourceParam in sourceParams) { mergedParams.Add(sourceParam.ShallowCopy()); } } return mergedParams; } private static ObjectQueryState NewBuilderQuery(ObjectQueryState sourceQuery, Type elementType, StringBuilder queryText, Span newSpan, IEnumerable enumerableParams) { return NewBuilderQuery(sourceQuery, elementType, queryText, false, newSpan, enumerableParams); } private static ObjectQueryState NewBuilderQuery(ObjectQueryState sourceQuery, Type elementType, StringBuilder queryText, bool allowsLimit, Span newSpan, IEnumerable enumerableParams) { ObjectParameterCollection queryParams = enumerableParams as ObjectParameterCollection; if (queryParams == null && enumerableParams != null) { queryParams = new ObjectParameterCollection(sourceQuery.ObjectContext.Perspective); foreach (ObjectParameter objectParam in enumerableParams) { queryParams.Add(objectParam); } } EntitySqlQueryState newState = new EntitySqlQueryState(elementType, queryText.ToString(), allowsLimit, sourceQuery.ObjectContext, queryParams, newSpan); sourceQuery.ApplySettingsTo(newState); return newState; } // Note that all query builder string constants contain embedded newlines to prevent manipulation of the // query text by single line comments (--) that might appear in user-supplied portions of the string such // as a filter predicate, projection list, etc. #region SetOp Helpers private const string _setOpEpilog = @" )"; private const string _setOpProlog = @"( "; // SetOp helper - note that this doesn't merge Spans, since Except uses the original query's Span // while Intersect/Union/UnionAll use the merged Span. private static ObjectQueryState BuildSetOp(ObjectQueryState leftQuery, ObjectQueryState rightQuery, Span newSpan, string setOp) { // Assert that the arguments aren't null (should have been verified by ObjectQuery) Debug.Assert(leftQuery != null, "Left query is null?"); Debug.Assert(rightQuery != null, "Right query is null?"); Debug.Assert(leftQuery.ElementType.Equals(rightQuery.ElementType), "Incompatible element types in arguments to Except/Intersect/Union/UnionAll?"); // Retrieve the left and right arguments to the set operation - // this will throw if either input query is not an Entity-SQL query. string left = GetCommandText(leftQuery); string right = GetCommandText(rightQuery); // ObjectQuery arguments must be associated with the same ObjectContext instance as the implemented query if (!object.ReferenceEquals(leftQuery.ObjectContext, rightQuery.ObjectContext)) { throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_QueryBuilder_InvalidQueryArgument, "query"); } // Create a string builder only large enough to contain the new query text int queryLength = _setOpProlog.Length + left.Length + setOp.Length + right.Length + _setOpEpilog.Length; StringBuilder builder = new StringBuilder(queryLength); // Build the new query builder.Append(_setOpProlog); builder.Append(left); builder.Append(setOp); builder.Append(right); builder.Append(_setOpEpilog); // Create a new query implementation and apply the state of this implementation to it. // The Span of the query argument will be merged into the new query's Span by the caller, iff the Set Op is NOT Except. // See the Except, Intersect, Union and UnionAll methods in this class for examples. return NewBuilderQuery(leftQuery, leftQuery.ElementType, builder, newSpan, MergeParameters(leftQuery.Parameters, rightQuery.Parameters)); } #endregion #region Select/SelectValue Helpers private const string _fromOp = @" FROM ( "; private const string _asOp = @" ) AS "; private static ObjectQueryState BuildSelectOrSelectValue(ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters, string projectOp, Type elementType) { Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(alias), "Invalid alias"); Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(projection), "Invalid projection"); string queryText = GetCommandText(query); // Build the new query string - " FROM () AS " int queryLength = projectOp.Length + projection.Length + _fromOp.Length + queryText.Length + _asOp.Length + alias.Length; StringBuilder builder = new StringBuilder(queryLength); builder.Append(projectOp); builder.Append(projection); builder.Append(_fromOp); builder.Append(queryText); builder.Append(_asOp); builder.Append(alias); // Create a new EntitySqlQueryImplementation that uses the new query as its command text. // Span should not be carried over from a Select or SelectValue operation. return NewBuilderQuery(query, elementType, builder, null, MergeParameters(query.ObjectContext, query.Parameters, parameters)); } #endregion #region OrderBy/Where Helper private static ObjectQueryState BuildOrderByOrWhere(ObjectQueryState query, string alias, string predicateOrKeys, ObjectParameter[] parameters, string op, string skipCount, bool allowsLimit) { Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(alias), "Invalid alias"); Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(predicateOrKeys), "Invalid predicate/keys"); Debug.Assert(null == skipCount || op == _orderByOp, "Skip clause used with WHERE operator?"); string queryText = GetCommandText(query); // Build the new query string: // Either: "SELECT VALUE FROM () AS WHERE " // (for Where) // Or: "SELECT VALUE FROM () AS ORDER BY >" // Depending on the value of 'op' int queryLength = _selectValueOp.Length + alias.Length + _fromOp.Length + queryText.Length + _asOp.Length + alias.Length + op.Length + predicateOrKeys.Length; if (skipCount != null) { queryLength += (_skipOp.Length + skipCount.Length); } StringBuilder builder = new StringBuilder(queryLength); builder.Append(_selectValueOp); builder.Append(alias); builder.Append(_fromOp); builder.Append(queryText); builder.Append(_asOp); builder.Append(alias); builder.Append(op); builder.Append(predicateOrKeys); if (skipCount != null) { builder.Append(_skipOp); builder.Append(skipCount); } // Create a new EntitySqlQueryImplementation that uses the new query as its command text. // Span is carried over, no adjustment is needed. return NewBuilderQuery(query, query.ElementType, builder, allowsLimit, query.Span, MergeParameters(query.ObjectContext, query.Parameters, parameters)); } #endregion #region Distinct private const string _distinctProlog = @"SET( "; private const string _distinctEpilog = @" )"; internal static ObjectQueryState Distinct(ObjectQueryState query) { // Build the new query string - "SET()" string queryText = GetCommandText(query); StringBuilder builder = new StringBuilder(_distinctProlog.Length + queryText.Length + _distinctEpilog.Length); builder.Append(_distinctProlog); builder.Append(queryText); builder.Append(_distinctEpilog); // Span is carried over, no adjustment is needed return NewBuilderQuery(query, query.ElementType, builder, query.Span, ObjectParameterCollection.DeepCopy(query.Parameters)); } #endregion #region Except private const string _exceptOp = @" ) EXCEPT ( "; internal static ObjectQueryState Except(ObjectQueryState leftQuery, ObjectQueryState rightQuery) { // Call the SetOp helper. // Span is taken from the leftmost query. return EntitySqlQueryBuilder.BuildSetOp(leftQuery, rightQuery, leftQuery.Span, _exceptOp); } #endregion #region GroupBy private const string _groupByOp = @" GROUP BY "; internal static ObjectQueryState GroupBy(ObjectQueryState query, string alias, string keys, string projection, ObjectParameter[] parameters) { Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(alias), "Invalid alias"); Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(alias), "Invalid keys"); Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(projection), "Invalid projection"); string queryText = GetCommandText(query); // Build the new query string: // "SELECT FROM () AS GROUP BY " int queryLength = _selectOp.Length + projection.Length + _fromOp.Length + queryText.Length + _asOp.Length + alias.Length + _groupByOp.Length + keys.Length; StringBuilder builder = new StringBuilder(queryLength); builder.Append(_selectOp); builder.Append(projection); builder.Append(_fromOp); builder.Append(queryText); builder.Append(_asOp); builder.Append(alias); builder.Append(_groupByOp); builder.Append(keys); // Create a new EntitySqlQueryImplementation that uses the new query as its command text. // Span should not be carried over from a GroupBy operation. return NewBuilderQuery(query, typeof(DbDataRecord), builder, null, MergeParameters(query.ObjectContext, query.Parameters, parameters)); } #endregion #region Intersect private const string _intersectOp = @" ) INTERSECT ( "; internal static ObjectQueryState Intersect(ObjectQueryState leftQuery, ObjectQueryState rightQuery) { // Ensure the Spans of the query arguments are merged into the new query's Span. Span newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span); // Call the SetOp helper. return BuildSetOp(leftQuery, rightQuery, newSpan, _intersectOp); } #endregion #region OfType private const string _ofTypeProlog = @"OFTYPE( ( "; private const string _ofTypeInfix = @" ), ["; private const string _ofTypeInfix2 = "].["; private const string _ofTypeEpilog = @"] )"; internal static ObjectQueryState OfType(ObjectQueryState query, EdmType newType, Type clrOfType) { Debug.Assert(newType != null, "OfType cannot be null"); Debug.Assert(Helper.IsEntityType(newType) || Helper.IsComplexType(newType), "OfType must be Entity or Complex type"); string queryText = GetCommandText(query); // Build the new query string - "OFTYPE((), [].[])" int queryLength = _ofTypeProlog.Length + queryText.Length + _ofTypeInfix.Length + newType.NamespaceName.Length + (newType.NamespaceName != string.Empty ? _ofTypeInfix2.Length : 0) + newType.Name.Length + _ofTypeEpilog.Length; StringBuilder builder = new StringBuilder(queryLength); builder.Append(_ofTypeProlog); builder.Append(queryText); builder.Append(_ofTypeInfix); if (newType.NamespaceName != string.Empty) { builder.Append(newType.NamespaceName); builder.Append(_ofTypeInfix2); } builder.Append(newType.Name); builder.Append(_ofTypeEpilog); // Create a new EntitySqlQueryImplementation that uses the new query as its command text. // Span is carried over, no adjustment is needed return NewBuilderQuery(query, clrOfType, builder, query.Span, ObjectParameterCollection.DeepCopy(query.Parameters)); } #endregion #region OrderBy private const string _orderByOp = @" ORDER BY "; internal static ObjectQueryState OrderBy(ObjectQueryState query, string alias, string keys, ObjectParameter[] parameters) { return BuildOrderByOrWhere(query, alias, keys, parameters, _orderByOp, null, true); } #endregion #region Select private const string _selectOp = "SELECT "; internal static ObjectQueryState Select(ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters) { return BuildSelectOrSelectValue(query, alias, projection, parameters, _selectOp, typeof(DbDataRecord)); } #endregion #region SelectValue private const string _selectValueOp = "SELECT VALUE "; internal static ObjectQueryState SelectValue(ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters, Type projectedType) { return BuildSelectOrSelectValue(query, alias, projection, parameters, _selectValueOp, projectedType); } #endregion #region Skip private const string _skipOp = @" SKIP "; internal static ObjectQueryState Skip(ObjectQueryState query, string alias, string keys, string count, ObjectParameter[] parameters) { Debug.Assert(!StringUtil.IsNullOrEmptyOrWhiteSpace(count), "Invalid skip count"); return BuildOrderByOrWhere(query, alias, keys, parameters, _orderByOp, count, true); } #endregion #region Top private const string _limitOp = @" LIMIT "; private const string _topOp = @"SELECT VALUE TOP( "; private const string _topInfix = @" ) "; internal static ObjectQueryState Top(ObjectQueryState query, string alias, string count, ObjectParameter[] parameters) { int queryLength = count.Length; string queryText = GetCommandText(query); bool limitAllowed = ((EntitySqlQueryState)query).AllowsLimitSubclause; if (limitAllowed) { // Build the new query string: // LIMIT queryLength += (queryText.Length + _limitOp.Length // + count.Length is added above ); } else { // Build the new query string: // "SELECT VALUE TOP() FROM () AS " queryLength += (_topOp.Length + // count.Length + is added above _topInfix.Length + alias.Length + _fromOp.Length + queryText.Length + _asOp.Length + alias.Length); } StringBuilder builder = new StringBuilder(queryLength); if (limitAllowed) { builder.Append(queryText); builder.Append(_limitOp); builder.Append(count); } else { builder.Append(_topOp); builder.Append(count); builder.Append(_topInfix); builder.Append(alias); builder.Append(_fromOp); builder.Append(queryText); builder.Append(_asOp); builder.Append(alias); } // Create a new EntitySqlQueryImplementation that uses the new query as its command text. // Span is carried over, no adjustment is needed. return NewBuilderQuery(query, query.ElementType, builder, query.Span, MergeParameters(query.ObjectContext, query.Parameters, parameters)); } #endregion #region Union private const string _unionOp = @" ) UNION ( "; internal static ObjectQueryState Union(ObjectQueryState leftQuery, ObjectQueryState rightQuery) { // Ensure the Spans of the query arguments are merged into the new query's Span. Span newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span); // Call the SetOp helper. return BuildSetOp(leftQuery, rightQuery, newSpan, _unionOp); } #endregion #region Union private const string _unionAllOp = @" ) UNION ALL ( "; internal static ObjectQueryState UnionAll(ObjectQueryState leftQuery, ObjectQueryState rightQuery) { // Ensure the Spans of the query arguments are merged into the new query's Span. Span newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span); // Call the SetOp helper. return BuildSetOp(leftQuery, rightQuery, newSpan, _unionAllOp); } #endregion #region Where private const string _whereOp = @" WHERE "; internal static ObjectQueryState Where(ObjectQueryState query, string alias, string predicate, ObjectParameter[] parameters) { return BuildOrderByOrWhere(query, alias, predicate, parameters, _whereOp, null, false); } #endregion } }