536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
271 lines
12 KiB
C#
271 lines
12 KiB
C#
//---------------------------------------------------------------------
|
|
// <copyright file="EntitySqlQueryState.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//
|
|
// @owner Microsoft
|
|
//---------------------------------------------------------------------
|
|
|
|
namespace System.Data.Objects
|
|
{
|
|
using System.Collections.Generic;
|
|
using System.Data.Common.CommandTrees;
|
|
using System.Data.Common.CommandTrees.ExpressionBuilder;
|
|
using System.Data.Common.EntitySql;
|
|
using System.Data.Common.QueryCache;
|
|
using System.Data.Metadata.Edm;
|
|
using System.Data.Objects.Internal;
|
|
using System.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// ObjectQueryState based on Entity-SQL query text.
|
|
/// </summary>
|
|
internal sealed class EntitySqlQueryState : ObjectQueryState
|
|
{
|
|
/// <summary>
|
|
/// The Entity-SQL text that defines the query.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is important that this field is readonly for consistency reasons wrt <see cref="_queryExpression"/>.
|
|
/// If this field becomes read-write, then write should be allowed only when <see cref="_queryExpression"/> is null,
|
|
/// or there should be a mechanism keeping both fields consistent.
|
|
/// </remarks>
|
|
private readonly string _queryText;
|
|
|
|
/// <summary>
|
|
/// Optional <see cref="DbExpression"/> that defines the query. Must be semantically equal to the <see cref="_queryText"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is important that this field is readonly for consistency reasons wrt <see cref="_queryText"/>.
|
|
/// If this field becomes read-write, then there should be a mechanism keeping both fields consistent.
|
|
/// </remarks>
|
|
private readonly DbExpression _queryExpression;
|
|
|
|
/// <summary>
|
|
/// Can a Limit subclause be appended to the text of this query?
|
|
/// </summary>
|
|
private readonly bool _allowsLimit;
|
|
|
|
/// <summary>
|
|
/// Initializes a new query EntitySqlQueryState instance.
|
|
/// </summary>
|
|
/// <param name="context">
|
|
/// The ObjectContext containing the metadata workspace the query was
|
|
/// built against, the connection on which to execute the query, and the
|
|
/// cache to store the results in. Must not be null.
|
|
/// </param>
|
|
/// <param name="commandText">
|
|
/// The Entity-SQL text of the query
|
|
/// </param>
|
|
/// <param name="mergeOption">
|
|
/// The merge option to use when retrieving results if an explicit merge option is not specified
|
|
/// </param>
|
|
internal EntitySqlQueryState(Type elementType, string commandText, bool allowsLimit, ObjectContext context, ObjectParameterCollection parameters, Span span)
|
|
: this(elementType, commandText, /*expression*/ null, allowsLimit, context, parameters, span)
|
|
{ }
|
|
|
|
/// <summary>
|
|
/// Initializes a new query EntitySqlQueryState instance.
|
|
/// </summary>
|
|
/// <param name="context">
|
|
/// The ObjectContext containing the metadata workspace the query was
|
|
/// built against, the connection on which to execute the query, and the
|
|
/// cache to store the results in. Must not be null.
|
|
/// </param>
|
|
/// <param name="commandText">
|
|
/// The Entity-SQL text of the query
|
|
/// </param>
|
|
/// <param name="expression">
|
|
/// Optional <see cref="DbExpression"/> that defines the query. Must be semantically equal to the <paramref name="commandText"/>.
|
|
/// </param>
|
|
/// <param name="mergeOption">
|
|
/// The merge option to use when retrieving results if an explicit merge option is not specified
|
|
/// </param>
|
|
internal EntitySqlQueryState(Type elementType, string commandText, DbExpression expression, bool allowsLimit, ObjectContext context, ObjectParameterCollection parameters, Span span)
|
|
: base(elementType, context, parameters, span)
|
|
{
|
|
EntityUtil.CheckArgumentNull(commandText, "commandText");
|
|
if (string.IsNullOrEmpty(commandText))
|
|
{
|
|
throw EntityUtil.Argument(System.Data.Entity.Strings.ObjectQuery_InvalidEmptyQuery, "commandText");
|
|
}
|
|
|
|
_queryText = commandText;
|
|
_queryExpression = expression;
|
|
_allowsLimit = allowsLimit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether or not the current query is a 'Skip' or 'Sort' operation
|
|
/// and so would allow a 'Limit' clause to be appended to the current query text.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// <c>True</c> if the current query is a Skip or Sort expression, or a
|
|
/// Project expression with a Skip or Sort expression input.
|
|
/// </returns>
|
|
internal bool AllowsLimitSubclause { get { return _allowsLimit; } }
|
|
|
|
/// <summary>
|
|
/// Always returns the Entity-SQL text of the implemented ObjectQuery.
|
|
/// </summary>
|
|
/// <param name="commandText">Always set to the Entity-SQL text of this ObjectQuery.</param>
|
|
/// <returns>Always returns <c>true</c>.</returns>
|
|
internal override bool TryGetCommandText(out string commandText)
|
|
{
|
|
commandText = this._queryText;
|
|
return true;
|
|
}
|
|
|
|
internal override bool TryGetExpression(out System.Linq.Expressions.Expression expression)
|
|
{
|
|
expression = null;
|
|
return false;
|
|
}
|
|
|
|
protected override TypeUsage GetResultType()
|
|
{
|
|
DbExpression query = this.Parse();
|
|
return query.ResultType;
|
|
}
|
|
|
|
internal override ObjectQueryState Include<TElementType>(ObjectQuery<TElementType> sourceQuery, string includePath)
|
|
{
|
|
ObjectQueryState retState = new EntitySqlQueryState(this.ElementType, _queryText, _queryExpression, _allowsLimit, this.ObjectContext, ObjectParameterCollection.DeepCopy(this.Parameters), Span.IncludeIn(this.Span, includePath));
|
|
this.ApplySettingsTo(retState);
|
|
return retState;
|
|
}
|
|
|
|
internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMergeOption)
|
|
{
|
|
// Metadata is required to generate the execution plan or to retrieve it from the cache.
|
|
this.ObjectContext.EnsureMetadata();
|
|
|
|
// Determine the required merge option, with the following precedence:
|
|
// 1. The merge option specified to Execute(MergeOption) as forMergeOption.
|
|
// 2. The merge option set via ObjectQuery.MergeOption.
|
|
// 3. The global default merge option.
|
|
MergeOption mergeOption = EnsureMergeOption(forMergeOption, this.UserSpecifiedMergeOption);
|
|
|
|
// If a cached plan is present, then it can be reused if it has the required merge option
|
|
// (since span and parameters cannot change between executions). However, if the cached
|
|
// plan does not have the required merge option we proceed as if it were not present.
|
|
ObjectQueryExecutionPlan plan = this._cachedPlan;
|
|
if (plan != null)
|
|
{
|
|
if (plan.MergeOption == mergeOption)
|
|
{
|
|
return plan;
|
|
}
|
|
else
|
|
{
|
|
plan = null;
|
|
}
|
|
}
|
|
|
|
// There is no cached plan (or it was cleared), so the execution plan must be retrieved from
|
|
// the global query cache (if plan caching is enabled) or rebuilt for the required merge option.
|
|
QueryCacheManager cacheManager = null;
|
|
EntitySqlQueryCacheKey cacheKey = null;
|
|
if (this.PlanCachingEnabled)
|
|
{
|
|
// Create a new cache key that reflects the current state of the Parameters collection
|
|
// and the Span object (if any), and uses the specified merge option.
|
|
cacheKey = new EntitySqlQueryCacheKey(
|
|
this.ObjectContext.DefaultContainerName,
|
|
_queryText,
|
|
(null == this.Parameters ? 0 : this.Parameters.Count),
|
|
(null == this.Parameters ? null : this.Parameters.GetCacheKey()),
|
|
(null == this.Span ? null : this.Span.GetCacheKey()),
|
|
mergeOption,
|
|
this.ElementType);
|
|
|
|
cacheManager = this.ObjectContext.MetadataWorkspace.GetQueryCacheManager();
|
|
ObjectQueryExecutionPlan executionPlan = null;
|
|
if (cacheManager.TryCacheLookup(cacheKey, out executionPlan))
|
|
{
|
|
plan = executionPlan;
|
|
}
|
|
}
|
|
|
|
if (plan == null)
|
|
{
|
|
// Either caching is not enabled or the execution plan was not found in the cache
|
|
DbExpression queryExpression = this.Parse();
|
|
Debug.Assert(queryExpression != null, "EntitySqlQueryState.Parse returned null expression?");
|
|
DbQueryCommandTree tree = DbQueryCommandTree.FromValidExpression(this.ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression);
|
|
plan = ObjectQueryExecutionPlan.Prepare(this.ObjectContext, tree, this.ElementType, mergeOption, this.Span, null, DbExpressionBuilder.AliasGenerator);
|
|
|
|
// If caching is enabled then update the cache now.
|
|
// Note: the logic is the same as in ELinqQueryState.
|
|
if (cacheKey != null)
|
|
{
|
|
var newEntry = new QueryCacheEntry(cacheKey, plan);
|
|
QueryCacheEntry foundEntry = null;
|
|
if (cacheManager.TryLookupAndAdd(newEntry, out foundEntry))
|
|
{
|
|
// If TryLookupAndAdd returns 'true' then the entry was already present in the cache when the attempt to add was made.
|
|
// In this case the existing execution plan should be used.
|
|
plan = (ObjectQueryExecutionPlan)foundEntry.GetTarget();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.Parameters != null)
|
|
{
|
|
this.Parameters.SetReadOnly(true);
|
|
}
|
|
|
|
// Update the cached plan with the newly retrieved/prepared plan
|
|
this._cachedPlan = plan;
|
|
|
|
// Return the execution plan
|
|
return plan;
|
|
}
|
|
|
|
internal DbExpression Parse()
|
|
{
|
|
if (_queryExpression != null)
|
|
{
|
|
return _queryExpression;
|
|
}
|
|
|
|
List<DbParameterReferenceExpression> parameters = null;
|
|
if (this.Parameters != null)
|
|
{
|
|
parameters = new List<DbParameterReferenceExpression>(this.Parameters.Count);
|
|
foreach (ObjectParameter parameter in this.Parameters)
|
|
{
|
|
TypeUsage typeUsage = parameter.TypeUsage;
|
|
if (null == typeUsage)
|
|
{
|
|
// Since ObjectParameters do not allow users to specify 'facets', make
|
|
// sure that the parameter TypeUsage is not populated with the provider
|
|
// default facet values.
|
|
this.ObjectContext.Perspective.TryGetTypeByName(
|
|
parameter.MappableType.FullName,
|
|
false /* bIgnoreCase */,
|
|
out typeUsage);
|
|
}
|
|
|
|
Debug.Assert(typeUsage != null, "typeUsage != null");
|
|
|
|
parameters.Add(typeUsage.Parameter(parameter.Name));
|
|
}
|
|
}
|
|
|
|
DbLambda lambda =
|
|
CqlQuery.CompileQueryCommandLambda(
|
|
_queryText, // Command Text
|
|
this.ObjectContext.Perspective, // Perspective
|
|
null, // Parser options - null indicates 'use default'
|
|
parameters, // Parameters
|
|
null // Variables
|
|
);
|
|
|
|
Debug.Assert(lambda.Variables == null || lambda.Variables.Count == 0, "lambda.Variables must be empty");
|
|
|
|
return lambda.Body;
|
|
}
|
|
}
|
|
}
|