//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //------------------------------------------------------------------------------ namespace System.Data.EntityClient { using System.Collections.Generic; using System.Data.Common; using System.Data.Common.CommandTrees; using System.Data.Common.Utils; using System.Data.Mapping; using System.Data.Metadata.Edm; using System.Data.Query.InternalTrees; using System.Data.Query.PlanCompiler; using System.Data.Query.ResultAssembly; using System.Diagnostics; using System.Linq; using System.Text; /// /// An aggregate Command Definition used by the EntityClient layers. This is an aggregator /// object that represent information from multiple underlying provider commands. /// sealed internal class EntityCommandDefinition : DbCommandDefinition { #region internal state /// /// nested store command definitions /// private readonly List _mappedCommandDefinitions; /// /// generates column map for the store result reader /// private readonly IColumnMapGenerator[] _columnMapGenerators; /// /// list of the parameters that the resulting command should have /// private readonly System.Collections.ObjectModel.ReadOnlyCollection _parameters; /// /// Set of entity sets exposed in the command. /// private readonly Set _entitySets; #endregion #region constructors /// /// don't let this be constructed publicly; /// /// Cannot prepare the command definition for execution; consult the InnerException for more information. /// The ADO.NET Data Provider you are using does not support CommandTrees. internal EntityCommandDefinition(DbProviderFactory storeProviderFactory, DbCommandTree commandTree) { EntityUtil.CheckArgumentNull(storeProviderFactory, "storeProviderFactory"); EntityUtil.CheckArgumentNull(commandTree, "commandTree"); DbProviderServices storeProviderServices = DbProviderServices.GetProviderServices(storeProviderFactory); try { if (DbCommandTreeKind.Query == commandTree.CommandTreeKind) { // Next compile the plan for the command tree List mappedCommandList = new List(); ColumnMap columnMap; int columnCount; PlanCompiler.Compile(commandTree, out mappedCommandList, out columnMap, out columnCount, out _entitySets); _columnMapGenerators = new IColumnMapGenerator[] {new ConstantColumnMapGenerator(columnMap, columnCount)}; // Note: we presume that the first item in the ProviderCommandInfo is the root node; Debug.Assert(mappedCommandList.Count > 0, "empty providerCommandInfo collection and no exception?"); // this shouldn't ever happen. // Then, generate the store commands from the resulting command tree(s) _mappedCommandDefinitions = new List(mappedCommandList.Count); foreach (ProviderCommandInfo providerCommandInfo in mappedCommandList) { DbCommandDefinition providerCommandDefinition = storeProviderServices.CreateCommandDefinition(providerCommandInfo.CommandTree); if (null == providerCommandDefinition) { throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.ProviderReturnedNullForCreateCommandDefinition); } _mappedCommandDefinitions.Add(providerCommandDefinition); } } else { Debug.Assert(DbCommandTreeKind.Function == commandTree.CommandTreeKind, "only query and function command trees are supported"); DbFunctionCommandTree entityCommandTree = (DbFunctionCommandTree)commandTree; // Retrieve mapping and metadata information for the function import. FunctionImportMappingNonComposable mapping = GetTargetFunctionMapping(entityCommandTree); IList returnParameters = entityCommandTree.EdmFunction.ReturnParameters; int resultSetCount = returnParameters.Count > 1 ? returnParameters.Count : 1; _columnMapGenerators = new IColumnMapGenerator[resultSetCount]; TypeUsage storeResultType = DetermineStoreResultType(entityCommandTree.MetadataWorkspace, mapping, 0, out _columnMapGenerators[0]); for (int i = 1; i < resultSetCount; i++) { DetermineStoreResultType(entityCommandTree.MetadataWorkspace, mapping, i, out _columnMapGenerators[i]); } // Copy over parameters (this happens through a more indirect route in the plan compiler, but // it happens nonetheless) List> providerParameters = new List>(); foreach (KeyValuePair parameter in entityCommandTree.Parameters) { providerParameters.Add(parameter); } // Construct store command tree usage. DbFunctionCommandTree providerCommandTree = new DbFunctionCommandTree(entityCommandTree.MetadataWorkspace, DataSpace.SSpace, mapping.TargetFunction, storeResultType, providerParameters); DbCommandDefinition storeCommandDefinition = storeProviderServices.CreateCommandDefinition(providerCommandTree); _mappedCommandDefinitions = new List(1) { storeCommandDefinition }; EntitySet firstResultEntitySet = mapping.FunctionImport.EntitySets.FirstOrDefault(); if (firstResultEntitySet != null) { _entitySets = new Set(); _entitySets.Add(mapping.FunctionImport.EntitySets.FirstOrDefault()); _entitySets.MakeReadOnly(); } } // Finally, build a list of the parameters that the resulting command should have; List parameterList = new List(); foreach (KeyValuePair queryParameter in commandTree.Parameters) { EntityParameter parameter = CreateEntityParameterFromQueryParameter(queryParameter); parameterList.Add(parameter); } _parameters = new System.Collections.ObjectModel.ReadOnlyCollection(parameterList); } catch (EntityCommandCompilationException) { // No need to re-wrap EntityCommandCompilationException throw; } catch (Exception e) { // we should not be wrapping all exceptions if (EntityUtil.IsCatchableExceptionType(e)) { // we don't wan't folks to have to know all the various types of exceptions that can // occur, so we just rethrow a CommandDefinitionException and make whatever we caught // the inner exception of it. throw EntityUtil.CommandCompilation(System.Data.Entity.Strings.EntityClient_CommandDefinitionPreparationFailed, e); } throw; } } /// /// Determines the store type for a function import. /// private TypeUsage DetermineStoreResultType(MetadataWorkspace workspace, FunctionImportMappingNonComposable mapping, int resultSetIndex, out IColumnMapGenerator columnMapGenerator) { // Determine column maps and infer result types for the mapped function. There are four varieties: // Collection(Entity) // Collection(PrimitiveType) // Collection(ComplexType) // No result type TypeUsage storeResultType; { StructuralType baseStructuralType; EdmFunction functionImport = mapping.FunctionImport; // Collection(Entity) or Collection(ComplexType) if (MetadataHelper.TryGetFunctionImportReturnType(functionImport, resultSetIndex, out baseStructuralType)) { ValidateEdmResultType(baseStructuralType, functionImport); //Note: Defensive check for historic reasons, we expect functionImport.EntitySets.Count > resultSetIndex EntitySet entitySet = functionImport.EntitySets.Count > resultSetIndex ? functionImport.EntitySets[resultSetIndex] : null; columnMapGenerator = new FunctionColumnMapGenerator(mapping, resultSetIndex, entitySet, baseStructuralType); // We don't actually know the return type for the stored procedure, but we can infer // one based on the mapping (i.e.: a column for every property of the mapped types // and for all discriminator columns) storeResultType = mapping.GetExpectedTargetResultType(workspace, resultSetIndex); } // Collection(PrimitiveType) else { FunctionParameter returnParameter = MetadataHelper.GetReturnParameter(functionImport, resultSetIndex); if (returnParameter != null && returnParameter.TypeUsage != null) { // Get metadata description of the return type storeResultType = returnParameter.TypeUsage; Debug.Assert(storeResultType.EdmType.BuiltInTypeKind == BuiltInTypeKind.CollectionType, "FunctionImport currently supports only collection result type"); TypeUsage elementType = ((CollectionType)storeResultType.EdmType).TypeUsage; Debug.Assert(Helper.IsScalarType(elementType.EdmType) , "FunctionImport supports only Collection(Entity), Collection(Enum) and Collection(Primitive)"); // Build collection column map where the first column of the store result is assumed // to contain the primitive type values. ScalarColumnMap scalarColumnMap = new ScalarColumnMap(elementType, string.Empty, 0, 0); SimpleCollectionColumnMap collectionColumnMap = new SimpleCollectionColumnMap(storeResultType, string.Empty, scalarColumnMap, null, null); columnMapGenerator = new ConstantColumnMapGenerator(collectionColumnMap, 1); } // No result type else { storeResultType = null; columnMapGenerator = new ConstantColumnMapGenerator(null, 0); } } } return storeResultType; } /// /// Handles the following negative scenarios /// Nested ComplexType Property in ComplexType /// /// private void ValidateEdmResultType(EdmType resultType, EdmFunction functionImport) { if (Helper.IsComplexType(resultType)) { ComplexType complexType = resultType as ComplexType; Debug.Assert(null != complexType, "we should have a complex type here"); foreach (var property in complexType.Properties) { if (property.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType) { throw new NotSupportedException(System.Data.Entity.Strings.ComplexTypeAsReturnTypeAndNestedComplexProperty(property.Name, complexType.Name, functionImport.FullName)); } } } } /// /// Retrieves mapping for the given C-Space functionCommandTree /// private static FunctionImportMappingNonComposable GetTargetFunctionMapping(DbFunctionCommandTree functionCommandTree) { Debug.Assert(functionCommandTree.DataSpace == DataSpace.CSpace, "map from CSpace->SSpace function"); Debug.Assert(functionCommandTree != null, "null functionCommandTree"); Debug.Assert(!functionCommandTree.EdmFunction.IsComposableAttribute, "functionCommandTree.EdmFunction must be non-composable."); // Find mapped store function. FunctionImportMapping targetFunctionMapping; if (!functionCommandTree.MetadataWorkspace.TryGetFunctionImportMapping(functionCommandTree.EdmFunction, out targetFunctionMapping)) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_UnmappedFunctionImport(functionCommandTree.EdmFunction.FullName)); } return (FunctionImportMappingNonComposable)targetFunctionMapping; } #endregion #region public API /// /// Create a DbCommand object from the definition, that can be executed /// /// public override DbCommand CreateCommand() { return new EntityCommand(this); } #endregion #region internal methods /// /// Get a list of commands to be executed by the provider /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal IEnumerable MappedCommands { get { // Build up the list of command texts, if we haven't done so yet List mappedCommandTexts = new List(); foreach (DbCommandDefinition commandDefinition in _mappedCommandDefinitions) { DbCommand mappedCommand = commandDefinition.CreateCommand(); mappedCommandTexts.Add(mappedCommand.CommandText); } return mappedCommandTexts; } } /// /// Creates ColumnMap for result assembly using the given reader. /// internal ColumnMap CreateColumnMap(DbDataReader storeDataReader) { return CreateColumnMap(storeDataReader, 0); } /// /// Creates ColumnMap for result assembly using the given reader's resultSetIndexth result set. /// internal ColumnMap CreateColumnMap(DbDataReader storeDataReader, int resultSetIndex) { return _columnMapGenerators[resultSetIndex].CreateColumnMap(storeDataReader); } /// /// Property to expose the known parameters for the query, so the Command objects /// constructor can poplulate it's parameter collection from. /// internal IEnumerable Parameters { get { return _parameters; } } /// /// Set of entity sets exposed in the command. /// internal Set EntitySets { get { return _entitySets; } } /// /// Constructs a EntityParameter from a CQT parameter. /// /// /// private static EntityParameter CreateEntityParameterFromQueryParameter(KeyValuePair queryParameter) { // We really can't have a parameter here that isn't a scalar type... Debug.Assert(TypeSemantics.IsScalarType(queryParameter.Value), "Non-scalar type used as query parameter type"); EntityParameter result = new EntityParameter(); result.ParameterName = queryParameter.Key; EntityCommandDefinition.PopulateParameterFromTypeUsage(result, queryParameter.Value, isOutParam: false); return result; } internal static void PopulateParameterFromTypeUsage(EntityParameter parameter, TypeUsage type, bool isOutParam) { // type can be null here if the type provided by the user is not a known model type if (type != null) { PrimitiveTypeKind primitiveTypeKind; if (Helper.IsEnumType(type.EdmType)) { type = TypeUsage.Create(Helper.GetUnderlyingEdmTypeForEnumType(type.EdmType)); } else if (Helper.IsSpatialType(type, out primitiveTypeKind)) { parameter.EdmType = EdmProviderManifest.Instance.GetPrimitiveType(primitiveTypeKind); } } DbCommandDefinition.PopulateParameterFromTypeUsage(parameter, type, isOutParam); } /// /// Internal execute method -- copies command information from the map command /// to the command objects, executes them, and builds the result assembly /// structures needed to return the data reader /// /// /// /// /// behavior must specify CommandBehavior.SequentialAccess /// input parameters in the entityCommand.Parameters collection must have non-null values. internal DbDataReader Execute(EntityCommand entityCommand, CommandBehavior behavior) { if (CommandBehavior.SequentialAccess != (behavior & CommandBehavior.SequentialAccess)) { throw EntityUtil.MustUseSequentialAccess(); } DbDataReader storeDataReader = ExecuteStoreCommands(entityCommand, behavior); DbDataReader result = null; // If we actually executed something, then go ahead and construct a bridge // data reader for it. if (null != storeDataReader) { try { ColumnMap columnMap = this.CreateColumnMap(storeDataReader, 0); if (null == columnMap) { // For a query with no result type (and therefore no column map), consume the reader. // When the user requests Metadata for this reader, we return nothing. CommandHelper.ConsumeReader(storeDataReader); result = storeDataReader; } else { result = BridgeDataReader.Create(storeDataReader, columnMap, entityCommand.Connection.GetMetadataWorkspace(), GetNextResultColumnMaps(storeDataReader)); } } catch { // dispose of store reader if there is an error creating the BridgeDataReader storeDataReader.Dispose(); throw; } } return result; } private IEnumerable GetNextResultColumnMaps(DbDataReader storeDataReader) { for (int i = 1; i < _columnMapGenerators.Length; ++i) { yield return this.CreateColumnMap(storeDataReader, i); } } /// /// Execute the store commands, and return IteratorSources for each one /// /// /// internal DbDataReader ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior) { // SQLPT #120007433 is the work item to implement MARS support, which we // need to do here, but since the PlanCompiler doesn't // have it yet, neither do we... if (1 != _mappedCommandDefinitions.Count) { throw EntityUtil.NotSupported("MARS"); } EntityTransaction entityTransaction = CommandHelper.GetEntityTransaction(entityCommand); DbCommandDefinition definition = _mappedCommandDefinitions[0]; DbCommand storeProviderCommand = definition.CreateCommand(); CommandHelper.SetStoreProviderCommandState(entityCommand, entityTransaction, storeProviderCommand); // Copy over the values from the map command to the store command; we // assume that they were not renamed by either the plan compiler or SQL // Generation. // // Note that this pretty much presumes that named parameters are supported // by the store provider, but it might work if we don't reorder/reuse // parameters. // // Note also that the store provider may choose to add parameters to thier // command object for some things; we'll only copy over the values for // parameters that we find in the EntityCommands parameters collection, so // we won't damage anything the store provider did. bool hasOutputParameters = false; if (storeProviderCommand.Parameters != null) // SQLBUDT 519066 { DbProviderServices storeProviderServices = DbProviderServices.GetProviderServices(entityCommand.Connection.StoreProviderFactory); foreach (DbParameter storeParameter in storeProviderCommand.Parameters) { // I could just use the string indexer, but then if I didn't find it the // consumer would get some ParameterNotFound exeception message and that // wouldn't be very meaningful. Instead, I use the IndexOf method and // if I don't find it, it's not a big deal (The store provider must // have added it). int parameterOrdinal = entityCommand.Parameters.IndexOf(storeParameter.ParameterName); if (-1 != parameterOrdinal) { EntityParameter entityParameter = entityCommand.Parameters[parameterOrdinal]; SyncParameterProperties(entityParameter, storeParameter, storeProviderServices); if (storeParameter.Direction != ParameterDirection.Input) { hasOutputParameters = true; } } } } // If the EntityCommand has output parameters, we must synchronize parameter values when // the reader is closed. Tell the EntityCommand about the store command so that it knows // where to pull those values from. if (hasOutputParameters) { entityCommand.SetStoreProviderCommand(storeProviderCommand); } DbDataReader reader = null; try { reader = storeProviderCommand.ExecuteReader(behavior & ~CommandBehavior.SequentialAccess); } catch (Exception e) { // we should not be wrapping all exceptions if (EntityUtil.IsCatchableExceptionType(e)) { // we don't wan't folks to have to know all the various types of exceptions that can // occur, so we just rethrow a CommandDefinitionException and make whatever we caught // the inner exception of it. throw EntityUtil.CommandExecution(System.Data.Entity.Strings.EntityClient_CommandDefinitionExecutionFailed, e); } throw; } return reader; } /// /// Updates storeParameter size, precision and scale properties from user provided parameter properties. /// /// /// private static void SyncParameterProperties(EntityParameter entityParameter, DbParameter storeParameter, DbProviderServices storeProviderServices) { IDbDataParameter dbDataParameter = (IDbDataParameter)storeParameter; // DBType is not currently syncable; it's part of the cache key anyway; this is because we can't guarantee // that the store provider will honor it -- (SqlClient doesn't...) //if (entityParameter.IsDbTypeSpecified) //{ // storeParameter.DbType = entityParameter.DbType; //} // Give the store provider the opportunity to set the value before any parameter state has been copied from // the EntityParameter. TypeUsage parameterTypeUsage = TypeHelpers.GetPrimitiveTypeUsageForScalar(entityParameter.GetTypeUsage()); storeProviderServices.SetParameterValue(storeParameter, parameterTypeUsage, entityParameter.Value); // Override the store provider parameter state with any explicitly specified values from the EntityParameter. if (entityParameter.IsDirectionSpecified) { storeParameter.Direction = entityParameter.Direction; } if (entityParameter.IsIsNullableSpecified) { storeParameter.IsNullable = entityParameter.IsNullable; } if (entityParameter.IsSizeSpecified) { storeParameter.Size = entityParameter.Size; } if (entityParameter.IsPrecisionSpecified) { dbDataParameter.Precision = entityParameter.Precision; } if (entityParameter.IsScaleSpecified) { dbDataParameter.Scale = entityParameter.Scale; } } /// /// Return the string used by EntityCommand and ObjectQuery ToTraceString"/> /// /// internal string ToTraceString() { if (_mappedCommandDefinitions != null) { if (_mappedCommandDefinitions.Count == 1) { // Gosh it sure would be nice if I could just get the inner commandText, but // that would require more public surface area on DbCommandDefinition, or // me to know about the inner object... return _mappedCommandDefinitions[0].CreateCommand().CommandText; } else { StringBuilder sb = new StringBuilder(); foreach (DbCommandDefinition commandDefinition in _mappedCommandDefinitions) { DbCommand mappedCommand = commandDefinition.CreateCommand(); sb.Append(mappedCommand.CommandText); } return sb.ToString(); } } return string.Empty; } #endregion #region nested types /// /// Generates a column map given a data reader. /// private interface IColumnMapGenerator { /// /// Given a data reader, returns column map. /// /// Data reader. /// Column map. ColumnMap CreateColumnMap(DbDataReader reader); } /// /// IColumnMapGenerator wrapping a constant instance of a column map (invariant with respect /// to the given DbDataReader) /// private sealed class ConstantColumnMapGenerator : IColumnMapGenerator { private readonly ColumnMap _columnMap; private readonly int _fieldsRequired; internal ConstantColumnMapGenerator(ColumnMap columnMap, int fieldsRequired) { _columnMap = columnMap; _fieldsRequired = fieldsRequired; } ColumnMap IColumnMapGenerator.CreateColumnMap(DbDataReader reader) { if (null != reader && reader.FieldCount < _fieldsRequired) { throw EntityUtil.CommandExecution(System.Data.Entity.Strings.EntityClient_TooFewColumns); } return _columnMap; } } /// /// Generates column maps for a non-composable function mapping. /// private sealed class FunctionColumnMapGenerator : IColumnMapGenerator { private readonly FunctionImportMappingNonComposable _mapping; private readonly EntitySet _entitySet; private readonly StructuralType _baseStructuralType; private readonly int _resultSetIndex; internal FunctionColumnMapGenerator(FunctionImportMappingNonComposable mapping, int resultSetIndex, EntitySet entitySet, StructuralType baseStructuralType) { _mapping = mapping; _entitySet = entitySet; _baseStructuralType = baseStructuralType; _resultSetIndex = resultSetIndex; } ColumnMap IColumnMapGenerator.CreateColumnMap(DbDataReader reader) { return ColumnMapFactory.CreateFunctionImportStructuralTypeColumnMap(reader, _mapping, _resultSetIndex, _entitySet, _baseStructuralType); } } #endregion } }