File: System\Data\Merger.cs
Web Access
Project: src\src\libraries\System.Data.Common\src\System.Data.Common.csproj (System.Data.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace System.Data
{
    internal sealed class Merger
    {
        private readonly DataSet? _dataSet;
        private readonly DataTable? _dataTable;
        private readonly bool _preserveChanges;
        private readonly MissingSchemaAction _missingSchemaAction;
        private readonly bool _isStandAlonetable;
        private bool _IgnoreNSforTableLookup; // Everett Behavior : SQL BU DT 370850
 
        internal Merger(DataSet dataSet, bool preserveChanges, MissingSchemaAction missingSchemaAction)
        {
            _dataSet = dataSet;
            _preserveChanges = preserveChanges;
 
            // map AddWithKey -> Add
            _missingSchemaAction = missingSchemaAction == MissingSchemaAction.AddWithKey ?
                MissingSchemaAction.Add :
                missingSchemaAction;
        }
 
        internal Merger(DataTable dataTable, bool preserveChanges, MissingSchemaAction missingSchemaAction)
        {
            _isStandAlonetable = true;
            _dataTable = dataTable;
            _preserveChanges = preserveChanges;
 
            // map AddWithKey -> Add
            _missingSchemaAction = missingSchemaAction == MissingSchemaAction.AddWithKey ?
                MissingSchemaAction.Add :
                missingSchemaAction;
        }
 
        internal void MergeDataSet(DataSet source)
        {
            Debug.Assert(_dataSet != null);
 
            if (source == _dataSet) return;  //somebody is doing an 'automerge'
            bool fEnforce = _dataSet.EnforceConstraints;
            _dataSet.EnforceConstraints = false;
            _IgnoreNSforTableLookup = (_dataSet._namespaceURI != source._namespaceURI); // if two DataSets have different
            // Namespaces, ignore NS for table lookups as we won't be able to find the right tables which inherits its NS
 
            List<DataColumn>? existingColumns = null; // need to cache existing columns
 
            if (MissingSchemaAction.Add == _missingSchemaAction)
            {
                existingColumns = new List<DataColumn>(); // need to cache existing columns
                foreach (DataTable dt in _dataSet.Tables)
                {
                    foreach (DataColumn dc in dt.Columns)
                    {
                        existingColumns.Add(dc);
                    }
                }
            }
 
            for (int i = 0; i < source.Tables.Count; i++)
            {
                MergeTableData(source.Tables[i]); // since column expression might have dependency on relation, we do not set
                //column expression at this point. We need to set it after adding relations
            }
 
            if (MissingSchemaAction.Ignore != _missingSchemaAction)
            {
                // Add all independent constraints
                MergeConstraints(source);
 
                // Add all relationships
                for (int i = 0; i < source.Relations.Count; i++)
                {
                    MergeRelation(source.Relations[i]);
                }
            }
 
            if (MissingSchemaAction.Add == _missingSchemaAction)
            {
                // for which other options we should add expressions also?
                foreach (DataTable sourceTable in source.Tables)
                {
                    DataTable targetTable;
                    if (_IgnoreNSforTableLookup)
                    {
                        targetTable = _dataSet.Tables[sourceTable.TableName]!;
                    }
                    else
                    {
                        targetTable = _dataSet.Tables[sourceTable.TableName, sourceTable.Namespace]!; // we know that target table won't be null since MissingSchemaAction is Add , we have already added it!
                    }
 
                    foreach (DataColumn dc in sourceTable.Columns)
                    {
                        // Should we overwrite the previous expression column? No, refer to spec, if it is new column we need to add the schema
                        if (dc.Computed)
                        {
                            DataColumn targetColumn = targetTable.Columns[dc.ColumnName]!;
                            if (!existingColumns!.Contains(targetColumn))
                            {
                                targetColumn.CopyExpressionFrom(dc);
                            }
                        }
                    }
                }
            }
 
            MergeExtendedProperties(source.ExtendedProperties, _dataSet.ExtendedProperties);
            foreach (DataTable dt in _dataSet.Tables)
            {
                dt.EvaluateExpressions();
            }
            _dataSet.EnforceConstraints = fEnforce;
        }
 
        internal void MergeTable(DataTable src)
        {
            bool fEnforce = false;
            if (!_isStandAlonetable)
            {
                if (src.DataSet == _dataSet) return; //somebody is doing an 'automerge'
                fEnforce = _dataSet!.EnforceConstraints;
                _dataSet.EnforceConstraints = false;
            }
            else
            {
                if (src == _dataTable) return; //somebody is doing an 'automerge'
                _dataTable!.SuspendEnforceConstraints = true;
            }
 
            if (_dataSet != null)
            {
                // this is ds.Merge
                // if source does not have a DS, or if NS of both DS does not match, ignore the NS
                if (src.DataSet == null || src.DataSet._namespaceURI != _dataSet._namespaceURI)
                {
                    _IgnoreNSforTableLookup = true;
                }
            }
            else
            {
                // this is dt.Merge
                if (_dataTable!.DataSet == null || src.DataSet == null || src.DataSet._namespaceURI != _dataTable.DataSet._namespaceURI)
                {
                    _IgnoreNSforTableLookup = true;
                }
            }
 
            MergeTableData(src);
 
            DataTable? dt = _dataTable;
            if (dt == null && _dataSet != null)
            {
                dt = _IgnoreNSforTableLookup ?
                    _dataSet.Tables[src.TableName] :
                    _dataSet.Tables[src.TableName, src.Namespace];
            }
 
            dt?.EvaluateExpressions();
 
            if (!_isStandAlonetable)
            {
                _dataSet!.EnforceConstraints = fEnforce;
            }
            else
            {
                _dataTable!.SuspendEnforceConstraints = false;
                try
                {
                    if (_dataTable.EnforceConstraints)
                    {
                        _dataTable.EnableConstraints();
                    }
                }
                catch (ConstraintException)
                {
                    if (_dataTable.DataSet != null)
                    {
                        _dataTable.DataSet.EnforceConstraints = false;
                    }
                    throw;
                }
            }
        }
 
        private void MergeTable(DataTable src, DataTable dst)
        {
            int rowsCount = src.Rows.Count;
            bool wasEmpty = dst.Rows.Count == 0;
            if (0 < rowsCount)
            {
                Index? ndxSearch = null;
                DataKey key = default(DataKey);
                dst.SuspendIndexEvents();
                try
                {
                    if (!wasEmpty && dst._primaryKey != null)
                    {
                        key = GetSrcKey(src, dst);
                        if (key.HasValue)
                        {
                            ndxSearch = dst._primaryKey.Key.GetSortIndex(DataViewRowState.OriginalRows | DataViewRowState.Added);
                        }
                    }
 
                    // this improves performance by iterating over the rows instead of computing their position
                    foreach (DataRow sourceRow in src.Rows)
                    {
                        DataRow? targetRow = null;
                        if (ndxSearch != null)
                        {
                            targetRow = dst.FindMergeTarget(sourceRow, key, ndxSearch);
                        }
                        dst.MergeRow(sourceRow, targetRow, _preserveChanges, ndxSearch);
                    }
                }
                finally
                {
                    dst.RestoreIndexEvents(true);
                }
            }
            MergeExtendedProperties(src.ExtendedProperties, dst.ExtendedProperties);
        }
 
        internal void MergeRows(DataRow[] rows)
        {
            Debug.Assert(_dataSet != null);
 
            DataTable? src = null;
            DataTable? dst = null;
            DataKey key = default(DataKey);
            Index? ndxSearch = null;
 
            bool fEnforce = _dataSet.EnforceConstraints;
            _dataSet.EnforceConstraints = false;
 
            for (int i = 0; i < rows.Length; i++)
            {
                DataRow row = rows[i];
 
                if (row == null)
                {
                    throw ExceptionBuilder.ArgumentNull($"{nameof(rows)}[{i}]");
                }
                if (row.Table == null)
                {
                    throw ExceptionBuilder.ArgumentNull($"{nameof(rows)}[{i}].{nameof(DataRow.Table)}");
                }
 
                //somebody is doing an 'automerge'
                if (row.Table.DataSet == _dataSet)
                {
                    continue;
                }
 
                if (src != row.Table)
                {                     // row.Table changed from prev. row.
                    src = row.Table;
                    dst = MergeSchema(row.Table);
                    if (dst == null)
                    {
                        Debug.Assert(MissingSchemaAction.Ignore == _missingSchemaAction, "MergeSchema failed");
                        _dataSet.EnforceConstraints = fEnforce;
                        return;
                    }
                    if (dst._primaryKey != null)
                    {
                        key = GetSrcKey(src, dst);
                    }
                    if (key.HasValue)
                    {
                        // Getting our own copy instead. ndxSearch = dst.primaryKey.Key.GetSortIndex();
                        // IMO, Better would be to reuse index
                        // ndxSearch = dst.primaryKey.Key.GetSortIndex(DataViewRowState.OriginalRows | DataViewRowState.Added );
                        ndxSearch?.RemoveRef();
                        ndxSearch = new Index(dst, dst._primaryKey!.Key.GetIndexDesc(), DataViewRowState.OriginalRows | DataViewRowState.Added, null);
                        ndxSearch.AddRef(); // need to addref twice, otherwise it will be collected
                        ndxSearch.AddRef(); // in past first adref was done in const
                    }
                }
 
                if (row._newRecord == -1 && row._oldRecord == -1)
                {
                    continue;
                }
 
                DataRow? targetRow = null;
                if (0 < dst!.Rows.Count && ndxSearch != null)
                {
                    targetRow = dst.FindMergeTarget(row, key, ndxSearch);
                }
 
                targetRow = dst.MergeRow(row, targetRow, _preserveChanges, ndxSearch);
 
                if (targetRow.Table._dependentColumns != null && targetRow.Table._dependentColumns.Count > 0)
                {
                    targetRow.Table.EvaluateExpressions(targetRow, DataRowAction.Change, null);
                }
            }
            ndxSearch?.RemoveRef();
 
            _dataSet.EnforceConstraints = fEnforce;
        }
 
        private DataTable? MergeSchema(DataTable table)
        {
            DataTable? targetTable = null;
            if (!_isStandAlonetable)
            {
                if (_dataSet!.Tables.Contains(table.TableName, true))
                {
                    if (_IgnoreNSforTableLookup)
                    {
                        targetTable = _dataSet.Tables[table.TableName];
                    }
                    else
                    {
                        targetTable = _dataSet.Tables[table.TableName, table.Namespace];
                    }
                }
            }
            else
            {
                targetTable = _dataTable;
            }
 
            if (targetTable == null)
            {
                // in case of standalone table, we make sure that targetTable is not null, so if this check passes, it will be when it is called via detaset
                if (MissingSchemaAction.Add == _missingSchemaAction)
                {
                    targetTable = table.Clone(table.DataSet); // if we are here mainly we are called from DataSet.Merge at this point we don't set
                    //expression columns, since it might have refer to other columns via relation, so it won't find the table and we get exception;
                    // do it after adding relations.
                    _dataSet!.Tables.Add(targetTable);
                }
                else if (MissingSchemaAction.Error == _missingSchemaAction)
                {
                    throw ExceptionBuilder.MergeMissingDefinition(table.TableName);
                }
            }
            else
            {
                if (MissingSchemaAction.Ignore != _missingSchemaAction)
                {
                    // Do the columns
                    int oldCount = targetTable.Columns.Count;
                    for (int i = 0; i < table.Columns.Count; i++)
                    {
                        DataColumn src = table.Columns[i];
                        DataColumn? dest = (targetTable.Columns.Contains(src.ColumnName, true)) ? targetTable.Columns[src.ColumnName] : null;
                        if (dest == null)
                        {
                            if (MissingSchemaAction.Add == _missingSchemaAction)
                            {
                                dest = src.Clone();
                                targetTable.Columns.Add(dest);
                            }
                            else
                            {
                                if (!_isStandAlonetable)
                                {
                                    _dataSet!.RaiseMergeFailed(targetTable, SR.Format(SR.DataMerge_MissingColumnDefinition, table.TableName, src.ColumnName), _missingSchemaAction);
                                }
                                else
                                {
                                    throw ExceptionBuilder.MergeFailed(SR.Format(SR.DataMerge_MissingColumnDefinition, table.TableName, src.ColumnName));
                                }
                            }
                        }
                        else
                        {
                            if (dest.DataType != src.DataType ||
                                ((dest.DataType == typeof(DateTime)) && (dest.DateTimeMode != src.DateTimeMode) && ((dest.DateTimeMode & src.DateTimeMode) != DataSetDateTime.Unspecified)))
                            {
                                if (!_isStandAlonetable)
                                    _dataSet!.RaiseMergeFailed(targetTable, SR.Format(SR.DataMerge_DataTypeMismatch, src.ColumnName), MissingSchemaAction.Error);
                                else
                                    throw ExceptionBuilder.MergeFailed(SR.Format(SR.DataMerge_DataTypeMismatch, src.ColumnName));
                            }
 
                            MergeExtendedProperties(src.ExtendedProperties, dest.ExtendedProperties);
                        }
                    }
 
                    // Set DataExpression
                    if (_isStandAlonetable)
                    {
                        for (int i = oldCount; i < targetTable.Columns.Count; i++)
                        {
                            targetTable.Columns[i].CopyExpressionFrom(table.Columns[targetTable.Columns[i].ColumnName]!);
                        }
                    }
 
                    // check the PrimaryKey
                    DataColumn[] targetPKey = targetTable.PrimaryKey;
                    DataColumn[] tablePKey = table.PrimaryKey;
                    if (targetPKey.Length != tablePKey.Length)
                    {
                        // special case when the target table does not have the PrimaryKey
 
                        if (targetPKey.Length == 0)
                        {
                            DataColumn[] key = new DataColumn[tablePKey.Length];
                            for (int i = 0; i < tablePKey.Length; i++)
                            {
                                key[i] = targetTable.Columns[tablePKey[i].ColumnName]!;
                            }
                            targetTable.PrimaryKey = key;
                        }
                        else if (tablePKey.Length != 0)
                        {
                            _dataSet!.RaiseMergeFailed(targetTable, SR.DataMerge_PrimaryKeyMismatch, _missingSchemaAction);
                        }
                    }
                    else
                    {
                        for (int i = 0; i < targetPKey.Length; i++)
                        {
                            if (string.Compare(targetPKey[i].ColumnName, tablePKey[i].ColumnName, false, targetTable.Locale) != 0)
                            {
                                _dataSet!.RaiseMergeFailed(table,
                                    SR.Format(SR.DataMerge_PrimaryKeyColumnsMismatch, targetPKey[i].ColumnName, tablePKey[i].ColumnName),
                                    _missingSchemaAction);
                            }
                        }
                    }
                }
 
                MergeExtendedProperties(table.ExtendedProperties, targetTable.ExtendedProperties);
            }
 
            return targetTable;
        }
 
        private void MergeTableData(DataTable src)
        {
            DataTable? dest = MergeSchema(src);
            if (dest == null) return;
 
            dest.MergingData = true;
            try
            {
                MergeTable(src, dest);
            }
            finally
            {
                dest.MergingData = false;
            }
        }
 
        private void MergeConstraints(DataSet source)
        {
            for (int i = 0; i < source.Tables.Count; i++)
            {
                MergeConstraints(source.Tables[i]);
            }
        }
 
        private void MergeConstraints(DataTable table)
        {
            Debug.Assert(_dataSet != null);
 
            // Merge constraints
            for (int i = 0; i < table.Constraints.Count; i++)
            {
                Constraint src = table.Constraints[i];
                Constraint? dest = src.Clone(_dataSet, _IgnoreNSforTableLookup);
 
                if (dest == null)
                {
                    _dataSet.RaiseMergeFailed(table,
                        SR.Format(SR.DataMerge_MissingConstraint, src.GetType().FullName, src.ConstraintName),
                        _missingSchemaAction
                    );
                }
                else
                {
                    Constraint? cons = dest.Table!.Constraints.FindConstraint(dest);
                    if (cons == null)
                    {
                        if (MissingSchemaAction.Add == _missingSchemaAction)
                        {
                            try
                            {
                                // try to keep the original name
                                dest.Table.Constraints.Add(dest);
                            }
                            catch (DuplicateNameException)
                            {
                                // if fail, assume default name
                                dest.ConstraintName = string.Empty;
                                dest.Table.Constraints.Add(dest);
                            }
                        }
                        else if (MissingSchemaAction.Error == _missingSchemaAction)
                        {
                            _dataSet.RaiseMergeFailed(table,
                                SR.Format(SR.DataMerge_MissingConstraint, src.GetType().FullName, src.ConstraintName),
                                _missingSchemaAction
                            );
                        }
                    }
                    else
                    {
                        MergeExtendedProperties(src.ExtendedProperties, cons.ExtendedProperties);
                    }
                }
            }
        }
 
        private void MergeRelation(DataRelation relation)
        {
            Debug.Assert(_dataSet != null);
            Debug.Assert(MissingSchemaAction.Error == _missingSchemaAction ||
                         MissingSchemaAction.Add == _missingSchemaAction,
                         "Unexpected value of MissingSchemaAction parameter : " + _missingSchemaAction.ToString());
            DataRelation? destRelation = null;
 
            // try to find given relation in this dataSet
 
            int iDest = _dataSet.Relations.InternalIndexOf(relation.RelationName);
 
            if (iDest >= 0)
            {
                // check the columns and Relation properties..
                destRelation = _dataSet.Relations[iDest];
 
                if (relation.ParentKey.ColumnsReference.Length != destRelation.ParentKey.ColumnsReference.Length)
                {
                    _dataSet.RaiseMergeFailed(null,
                        SR.Format(SR.DataMerge_MissingDefinition, relation.RelationName),
                        _missingSchemaAction);
                }
                for (int i = 0; i < relation.ParentKey.ColumnsReference.Length; i++)
                {
                    DataColumn dest = destRelation.ParentKey.ColumnsReference[i];
                    DataColumn src = relation.ParentKey.ColumnsReference[i];
 
                    if (0 != string.Compare(dest.ColumnName, src.ColumnName, false, dest.Table!.Locale))
                    {
                        _dataSet.RaiseMergeFailed(null,
                            SR.Format(SR.DataMerge_ReltionKeyColumnsMismatch, relation.RelationName),
                            _missingSchemaAction);
                    }
 
                    dest = destRelation.ChildKey.ColumnsReference[i];
                    src = relation.ChildKey.ColumnsReference[i];
 
                    if (0 != string.Compare(dest.ColumnName, src.ColumnName, false, dest.Table!.Locale))
                    {
                        _dataSet.RaiseMergeFailed(null,
                            SR.Format(SR.DataMerge_ReltionKeyColumnsMismatch, relation.RelationName),
                            _missingSchemaAction);
                    }
                }
            }
            else
            {
                if (MissingSchemaAction.Add == _missingSchemaAction)
                {
                    // create identical realtion in the current dataset
                    DataTable parent = _IgnoreNSforTableLookup ?
                        _dataSet.Tables[relation.ParentTable.TableName]! :
                        _dataSet.Tables[relation.ParentTable.TableName, relation.ParentTable.Namespace]!;
 
                    DataTable child = _IgnoreNSforTableLookup ?
                        _dataSet.Tables[relation.ChildTable.TableName]! :
                        _dataSet.Tables[relation.ChildTable.TableName, relation.ChildTable.Namespace]!;
 
                    DataColumn[] parentColumns = new DataColumn[relation.ParentKey.ColumnsReference.Length];
                    DataColumn[] childColumns = new DataColumn[relation.ParentKey.ColumnsReference.Length];
                    for (int i = 0; i < relation.ParentKey.ColumnsReference.Length; i++)
                    {
                        parentColumns[i] = parent.Columns[relation.ParentKey.ColumnsReference[i].ColumnName]!;
                        childColumns[i] = child.Columns[relation.ChildKey.ColumnsReference[i].ColumnName]!;
                    }
                    try
                    {
                        destRelation = new DataRelation(relation.RelationName, parentColumns, childColumns, relation._createConstraints);
                        destRelation.Nested = relation.Nested;
                        _dataSet.Relations.Add(destRelation);
                    }
                    catch (Exception e) when (Common.ADP.IsCatchableExceptionType(e))
                    {
                        ExceptionBuilder.TraceExceptionForCapture(e);
                        _dataSet.RaiseMergeFailed(null, e.Message, _missingSchemaAction);
                        // TODO: destRelation may be null, causing an NRE below
                    }
                }
                else
                {
                    Debug.Assert(MissingSchemaAction.Error == _missingSchemaAction, $"Unexpected value of MissingSchemaAction parameter : {_missingSchemaAction}");
                    throw ExceptionBuilder.MergeMissingDefinition(relation.RelationName);
                }
            }
 
            // TODO: destRelation may be null, see comment above
            MergeExtendedProperties(relation.ExtendedProperties, destRelation!.ExtendedProperties);
        }
 
        private void MergeExtendedProperties(PropertyCollection src, PropertyCollection dst)
        {
            if (MissingSchemaAction.Ignore == _missingSchemaAction)
            {
                return;
            }
 
            IDictionaryEnumerator srcDE = src.GetEnumerator();
            while (srcDE.MoveNext())
            {
                if (!_preserveChanges || dst[srcDE.Key] == null)
                {
                    dst[srcDE.Key] = srcDE.Value;
                }
            }
        }
 
        private static DataKey GetSrcKey(DataTable src, DataTable dst)
        {
            if (src._primaryKey != null)
            {
                return src._primaryKey.Key;
            }
 
            DataKey key = default(DataKey);
            if (dst._primaryKey != null)
            {
                DataColumn[] dstColumns = dst._primaryKey.Key.ColumnsReference;
                DataColumn[] srcColumns = new DataColumn[dstColumns.Length];
                for (int j = 0; j < dstColumns.Length; j++)
                {
                    srcColumns[j] = src.Columns[dstColumns[j].ColumnName]!;
                }
 
                key = new DataKey(srcColumns, false); // DataKey will take ownership of srcColumns
            }
 
            return key;
        }
    }
}