//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- namespace System.Data.Mapping.Update.Internal { using System.Collections.Generic; using System.Data.Common; using System.Data.Common.Utils; using System.Data.EntityClient; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Data.Spatial; /// /// Aggregates information about a modification command delegated to a store function. /// internal sealed class FunctionUpdateCommand : UpdateCommand { #region Constructors /// /// Initialize a new function command. Initializes the command object. /// /// Function mapping metadata /// Translator /// State entries handled by this operation. /// 'Root' state entry being handled by this function. internal FunctionUpdateCommand(StorageModificationFunctionMapping functionMapping, UpdateTranslator translator, System.Collections.ObjectModel.ReadOnlyCollection stateEntries, ExtractedStateEntry stateEntry) : base(stateEntry.Original, stateEntry.Current) { EntityUtil.CheckArgumentNull(functionMapping, "functionMapping"); EntityUtil.CheckArgumentNull(translator, "translator"); EntityUtil.CheckArgumentNull(stateEntries, "stateEntries"); // populate the main state entry for error reporting m_stateEntries = stateEntries; // create a command DbCommandDefinition commandDefinition = translator.GenerateCommandDefinition(functionMapping); m_dbCommand = commandDefinition.CreateCommand(); } #endregion #region Fields private readonly System.Collections.ObjectModel.ReadOnlyCollection m_stateEntries; /// /// Gets the store command wrapped by this command. /// private readonly DbCommand m_dbCommand; /// /// Gets pairs for column names and propagator results (so that we can associate reader results with /// the source records for server generated values). /// private List> m_resultColumns; /// /// Gets map from identifiers (key component proxies) to parameters holding the actual /// key values. Supports propagation of identifier values (fixup for server-gen keys) /// private List> m_inputIdentifiers; /// /// Gets map from identifiers (key component proxies) to column names producing the actual /// key values. Supports propagation of identifier values (fixup for server-gen keys) /// private Dictionary m_outputIdentifiers; /// /// Gets a reference to the rows affected output parameter for the stored procedure. May be null. /// private DbParameter m_rowsAffectedParameter; #endregion #region Properties internal override IEnumerable InputIdentifiers { get { if (null == m_inputIdentifiers) { yield break; } else { foreach (KeyValuePair inputIdentifier in m_inputIdentifiers) { yield return inputIdentifier.Key; } } } } internal override IEnumerable OutputIdentifiers { get { if (null == m_outputIdentifiers) { return Enumerable.Empty(); } return m_outputIdentifiers.Keys; } } internal override UpdateCommandKind Kind { get { return UpdateCommandKind.Function; } } #endregion #region Methods /// /// Gets state entries contributing to this function. Supports error reporting. /// internal override IList GetStateEntries(UpdateTranslator translator) { return m_stateEntries; } // Adds and register a DbParameter to the current command. internal void SetParameterValue(PropagatorResult result, StorageModificationFunctionParameterBinding parameterBinding, UpdateTranslator translator) { // retrieve DbParameter DbParameter parameter = this.m_dbCommand.Parameters[parameterBinding.Parameter.Name]; TypeUsage parameterType = parameterBinding.Parameter.TypeUsage; object parameterValue = translator.KeyManager.GetPrincipalValue(result); translator.SetParameterValue(parameter, parameterType, parameterValue); // if the parameter corresponds to an identifier (key component), remember this fact in case // it's important for dependency ordering (e.g., output the identifier before creating it) int identifier = result.Identifier; if (PropagatorResult.NullIdentifier != identifier) { const int initialSize = 2; // expect on average less than two input identifiers per command if (null == m_inputIdentifiers) { m_inputIdentifiers = new List>(initialSize); } foreach (int principal in translator.KeyManager.GetPrincipals(identifier)) { m_inputIdentifiers.Add(new KeyValuePair(principal, parameter)); } } } // Adds and registers a DbParameter taking the number of rows affected internal void RegisterRowsAffectedParameter(FunctionParameter rowsAffectedParameter) { if (null != rowsAffectedParameter) { Debug.Assert(rowsAffectedParameter.Mode == ParameterMode.Out || rowsAffectedParameter.Mode == ParameterMode.InOut, "when loading mapping metadata, we check that the parameter is an out parameter"); m_rowsAffectedParameter = m_dbCommand.Parameters[rowsAffectedParameter.Name]; } } // Adds a result column binding from a column name (from the result set for the function) to // a propagator result (which contains the context necessary to back-propagate the result). // If the result is an identifier, binds the internal void AddResultColumn(UpdateTranslator translator, String columnName, PropagatorResult result) { const int initializeSize = 2; // expect on average less than two result columns per command if (null == m_resultColumns) { m_resultColumns = new List>(initializeSize); } m_resultColumns.Add(new KeyValuePair(columnName, result)); int identifier = result.Identifier; if (PropagatorResult.NullIdentifier != identifier) { if (translator.KeyManager.HasPrincipals(identifier)) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.Update_GeneratedDependent(columnName)); } // register output identifier to enable fix-up and dependency tracking AddOutputIdentifier(columnName, identifier); } } // Indicate that a column in the command result set (specified by 'columnName') produces the // value for a key component (specified by 'identifier') private void AddOutputIdentifier(String columnName, int identifier) { const int initialSize = 2; // expect on average less than two identifier output per command if (null == m_outputIdentifiers) { m_outputIdentifiers = new Dictionary(initialSize); } m_outputIdentifiers[identifier] = columnName; } // efects: Executes the current function command in the given transaction and connection context. // All server-generated values are added to the generatedValues list. If those values are identifiers, they are // also added to the identifierValues dictionary, which associates proxy identifiers for keys in the session // with their actual values, permitting fix-up of identifiers across relationships. internal override long Execute(UpdateTranslator translator, EntityConnection connection, Dictionary identifierValues, List> generatedValues) { // configure command to use the connection and transaction for this session m_dbCommand.Transaction = ((null != connection.CurrentTransaction) ? connection.CurrentTransaction.StoreTransaction : null); m_dbCommand.Connection = connection.StoreConnection; if (translator.CommandTimeout.HasValue) { m_dbCommand.CommandTimeout = translator.CommandTimeout.Value; } // set all identifier inputs (to support propagation of identifier values across relationship // boundaries) if (null != m_inputIdentifiers) { foreach (KeyValuePair inputIdentifier in m_inputIdentifiers) { object value; if (identifierValues.TryGetValue(inputIdentifier.Key, out value)) { // set the actual value for the identifier if it has been produced by some // other command inputIdentifier.Value.Value = value; } } } // Execute the query long rowsAffected; if (null != m_resultColumns) { // If there are result columns, read the server gen results rowsAffected = 0; IBaseList members = TypeHelpers.GetAllStructuralMembers(this.CurrentValues.StructuralType); using (DbDataReader reader = m_dbCommand.ExecuteReader(CommandBehavior.SequentialAccess)) { // Retrieve only the first row from the first result set if (reader.Read()) { rowsAffected++; foreach (var resultColumn in m_resultColumns .Select(r => new KeyValuePair(GetColumnOrdinal(translator, reader, r.Key), r.Value)) .OrderBy(r => r.Key)) // order by column ordinal to avoid breaking SequentialAccess readers { int columnOrdinal = resultColumn.Key; TypeUsage columnType = members[resultColumn.Value.RecordOrdinal].TypeUsage; object value; if (Helper.IsSpatialType(columnType) && !reader.IsDBNull(columnOrdinal)) { value = SpatialHelpers.GetSpatialValue(translator.MetadataWorkspace, reader, columnType, columnOrdinal); } else { value = reader.GetValue(columnOrdinal); } // register for back-propagation PropagatorResult result = resultColumn.Value; generatedValues.Add(new KeyValuePair(result, value)); // register identifier if it exists int identifier = result.Identifier; if (PropagatorResult.NullIdentifier != identifier) { identifierValues.Add(identifier, value); } } } // Consume the current reader (and subsequent result sets) so that any errors // executing the function can be intercepted CommandHelper.ConsumeReader(reader); } } else { rowsAffected = m_dbCommand.ExecuteNonQuery(); } // if an explicit rows affected parameter exists, use this value instead if (null != m_rowsAffectedParameter) { // by design, negative row counts indicate failure iff. an explicit rows // affected parameter is used if (DBNull.Value.Equals(m_rowsAffectedParameter.Value)) { rowsAffected = 0; } else { try { rowsAffected = Convert.ToInt64(m_rowsAffectedParameter.Value, CultureInfo.InvariantCulture); } catch (Exception e) { if (UpdateTranslator.RequiresContext(e)) { // wrap the exception throw EntityUtil.Update(System.Data.Entity.Strings.Update_UnableToConvertRowsAffectedParameterToInt32( m_rowsAffectedParameter.ParameterName, typeof(int).FullName), e, this.GetStateEntries(translator)); } throw; } } } return rowsAffected; } private int GetColumnOrdinal(UpdateTranslator translator, DbDataReader reader, string columnName) { int columnOrdinal; try { columnOrdinal = reader.GetOrdinal(columnName); } catch (IndexOutOfRangeException) { throw EntityUtil.Update(System.Data.Entity.Strings.Update_MissingResultColumn(columnName), null, this.GetStateEntries(translator)); } return columnOrdinal; } /// /// Gets modification operator corresponding to the given entity state. /// private static ModificationOperator GetModificationOperator(EntityState state) { switch (state) { case EntityState.Modified: case EntityState.Unchanged: // unchanged entities correspond to updates (consider the case where // the entity is not being modified but a collocated relationship is) return ModificationOperator.Update; case EntityState.Added: return ModificationOperator.Insert; case EntityState.Deleted: return ModificationOperator.Delete; default: Debug.Fail("unexpected entity state " + state); return default(ModificationOperator); } } internal override int CompareToType(UpdateCommand otherCommand) { Debug.Assert(!object.ReferenceEquals(this, otherCommand), "caller should ensure other command is different"); FunctionUpdateCommand other = (FunctionUpdateCommand)otherCommand; // first state entry is the 'main' state entry for the command (see ctor) IEntityStateEntry thisParent = this.m_stateEntries[0]; IEntityStateEntry otherParent = other.m_stateEntries[0]; // order by operator int result = (int)GetModificationOperator(thisParent.State) - (int)GetModificationOperator(otherParent.State); if (0 != result) { return result; } // order by entity set result = StringComparer.Ordinal.Compare(thisParent.EntitySet.Name, otherParent.EntitySet.Name); if (0 != result) { return result; } result = StringComparer.Ordinal.Compare(thisParent.EntitySet.EntityContainer.Name, otherParent.EntitySet.EntityContainer.Name); if (0 != result) { return result; } // order by key values int thisInputIdentifierCount = (null == this.m_inputIdentifiers ? 0 : this.m_inputIdentifiers.Count); int otherInputIdentifierCount = (null == other.m_inputIdentifiers ? 0 : other.m_inputIdentifiers.Count); result = thisInputIdentifierCount - otherInputIdentifierCount; if (0 != result) { return result; } for (int i = 0; i < thisInputIdentifierCount; i++) { DbParameter thisParameter = this.m_inputIdentifiers[i].Value; DbParameter otherParameter = other.m_inputIdentifiers[i].Value; result = ByValueComparer.Default.Compare(thisParameter.Value, otherParameter.Value); if (0 != result) { return result; } } // If the result is still zero, it means key values are all the same. Switch to synthetic identifiers // to differentiate. for (int i = 0; i < thisInputIdentifierCount; i++) { int thisIdentifier = this.m_inputIdentifiers[i].Key; int otherIdentifier = other.m_inputIdentifiers[i].Key; result = thisIdentifier - otherIdentifier; if (0 != result) { return result; } } return result; } #endregion } }