//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
///
/// An update to apply to a row of a result set. This will generate an UPDATE statement.
///
public sealed class RowUpdate : RowEditBase
{
private const string UpdateScriptStart = @"UPDATE {0}";
private const string UpdateScriptStartMemOptimized = @"UPDATE {0} WITH (SNAPSHOT)";
private const string UpdateScript = @"{0} SET {1} {2}";
private const string UpdateScriptOutput = @"{0} SET {1} OUTPUT {2} {3}";
internal readonly ConcurrentDictionary cellUpdates;
private readonly IList associatedRow;
///
/// Constructs a new RowUpdate to be added to the cache.
///
/// Internal ID of the row that will be updated with this object
/// Result set for the rows of the object to update
/// Metadata provider for the object to update
public RowUpdate(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
: base(rowId, associatedResultSet, associatedMetadata)
{
cellUpdates = new ConcurrentDictionary();
associatedRow = associatedResultSet.GetRow(rowId);
}
///
/// Sort order property. Sorts to same position as RowCreate
///
protected override int SortId => 1;
#region Public Methods
///
/// Applies the changes to the associated result set after successfully executing the
/// change on the database
///
///
/// Reader returned from the execution of the command to update a row. Should contain
/// a single row that represents all the values of the row.
///
public override Task ApplyChanges(DbDataReader dataReader)
{
Validate.IsNotNull(nameof(dataReader), dataReader);
return AssociatedResultSet.UpdateRow(RowId, dataReader);
}
///
/// Generates a command that can be executed to update a row -- and return the contents of
/// the updated row.
///
/// The connection the command should be associated with
/// Command to update the row
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
DbCommand command = connection.CreateCommand();
// Build the "SET" portion of the statement
List setComponents = new List();
foreach (var updateElement in cellUpdates)
{
string formattedColumnName = SqlScriptFormatter.FormatIdentifier(updateElement.Value.Column.ColumnName);
string paramName = $"@Value{RowId}{updateElement.Key}";
setComponents.Add($"{formattedColumnName} = {paramName}");
SqlParameter parameter = new SqlParameter(paramName, updateElement.Value.Column.SqlDbType)
{
Value = updateElement.Value.Value
};
command.Parameters.Add(parameter);
}
string setComponentsJoined = string.Join(", ", setComponents);
// Build the "OUTPUT" portion of the statement
var outColumns = from c in AssociatedResultSet.Columns
let formatted = SqlScriptFormatter.FormatIdentifier(c.ColumnName)
select $"inserted.{formatted}";
string outColumnsJoined = string.Join(", ", outColumns);
// Get the where clause
WhereClause where = GetWhereClause(true);
command.Parameters.AddRange(where.Parameters.ToArray());
// Get the start of the statement
string statementStart = GetStatementStart();
// Put the whole #! together
command.CommandText = string.Format(UpdateScriptOutput, statementStart, setComponentsJoined,
outColumnsJoined, where.CommandText);
command.CommandType = CommandType.Text;
return command;
}
///
/// Generates a edit row that represents a row with pending update. The cells pending
/// updates are merged into the unchanged cells.
///
/// Original, cached cell contents
/// EditRow with pending updates
public override EditRow GetEditRow(DbCellValue[] cachedRow)
{
Validate.IsNotNull(nameof(cachedRow), cachedRow);
// For each cell that is pending update, replace the db cell value with a new one
foreach (var cellUpdate in cellUpdates)
{
cachedRow[cellUpdate.Key] = cellUpdate.Value.AsDbCellValue;
}
return new EditRow
{
Id = RowId,
Cells = cachedRow,
State = EditRow.EditRowState.DirtyUpdate
};
}
///
/// Constructs an update statement to change the associated row.
///
/// An UPDATE statement
public override string GetScript()
{
// Build the "SET" portion of the statement
var setComponents = cellUpdates.Values.Select(cellUpdate =>
{
string formattedColumnName = SqlScriptFormatter.FormatIdentifier(cellUpdate.Column.ColumnName);
string formattedValue = SqlScriptFormatter.FormatValue(cellUpdate.Value, cellUpdate.Column);
return $"{formattedColumnName} = {formattedValue}";
});
string setClause = string.Join(", ", setComponents);
// Get the where clause
string whereClause = GetWhereClause(false).CommandText;
// Get the start of the statement
string statementStart = GetStatementStart();
// Put the whole #! together
return string.Format(UpdateScript, statementStart, setClause, whereClause);
}
///
/// Reverts the value of a cell to its original value
///
/// Ordinal of the column to revert
/// The value that was
public override EditRevertCellResult RevertCell(int columnId)
{
Validate.IsWithinRange(nameof(columnId), columnId, 0, associatedRow.Count - 1);
// Remove the cell update
// NOTE: This is best effort. The only way TryRemove can fail is if it is already
// removed. If this happens, it is OK.
CellUpdate cellUpdate;
cellUpdates.TryRemove(columnId, out cellUpdate);
return new EditRevertCellResult
{
IsRowDirty = cellUpdates.Count > 0,
Cell = new EditCell(associatedRow[columnId], false)
};
}
///
/// Sets the value of the cell in the associated row. If is
/// identical to the original value, this will remove the cell update from the row update.
///
/// Ordinal of the columns that will be set
/// String representation of the value the user input
///
/// The string representation of the new value (after conversion to target object) if the
/// a change is made. null is returned if the cell is reverted to it's original value.
///
public override EditUpdateCellResult SetCell(int columnId, string newValue)
{
// Validate the value and convert to object
ValidateColumnIsUpdatable(columnId);
CellUpdate update = new CellUpdate(AssociatedResultSet.Columns[columnId], newValue);
// If the value is the same as the old value, we shouldn't make changes
// NOTE: We must use .Equals in order to ignore object to object comparisons
if (update.Value.Equals(associatedRow[columnId].RawObject))
{
// Remove any pending change and stop processing this (we don't care if we fail to remove something)
CellUpdate cu;
cellUpdates.TryRemove(columnId, out cu);
return new EditUpdateCellResult
{
IsRowDirty = cellUpdates.Count > 0,
Cell = new EditCell(associatedRow[columnId], false)
};
}
// The change is real, so set it
cellUpdates.AddOrUpdate(columnId, update, (i, cu) => update);
return new EditUpdateCellResult
{
IsRowDirty = true,
Cell = update.AsEditCell
};
}
#endregion
private string GetStatementStart()
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized
? UpdateScriptStartMemOptimized
: UpdateScriptStart;
return string.Format(formatString, AssociatedObjectMetadata.EscapedMultipartName);
}
}
}