//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using System;
using System.Data.Common;
using System.Globalization;
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.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
///
/// An error indicating that a delete action will delete multiple rows.
///
public class EditDataDeleteException : Exception
{
public EditDataDeleteException()
{
}
public EditDataDeleteException(string message)
: base(message)
{
}
public EditDataDeleteException(string message, Exception inner)
: base(message, inner)
{
}
}
///
/// Represents a row that should be deleted. This will generate a DELETE statement
///
public sealed class RowDelete : RowEditBase
{
private const string DeleteStatement = "DELETE FROM {0} {1}";
private const string DeleteMemoryOptimizedStatement = "DELETE FROM {0} WITH(SNAPSHOT) {1}";
private const string VerifyStatement = "SELECT COUNT (*) FROM ";
///
/// Constructs a new RowDelete object
///
/// Internal ID of the row to be deleted
/// Result set that is being edited
/// Improved metadata of the object being edited
public RowDelete(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
: base(rowId, associatedResultSet, associatedMetadata)
{
}
///
/// Sort ID for a RowDelete object. Setting to 2 ensures that these are the LAST changes
/// to be committed
///
protected override int SortId => 2;
///
/// 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 insert a new row. Should NOT
/// contain any rows.
///
public override Task ApplyChanges(DbDataReader dataReader)
{
// Take the result set and remove the row from it
AssociatedResultSet.RemoveRow(RowId);
return Task.FromResult(0);
}
///
/// Generates a command for deleting the selected row
///
///
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
// Return a SqlCommand with formatted with the parameters from the where clause
WhereClause where = GetWhereClause(true);
string commandText = GetCommandText(where.CommandText);
string verifyText = GetVerifyText(where.CommandText);
if (HasDuplicateRows(where, verifyText, connection))
{
throw new EditDataDeleteException("Cannot delete: Action will delete more than one row");
}
DbCommand command = connection.CreateCommand();
command.CommandText = commandText;
command.Parameters.AddRange(where.Parameters.ToArray());
return command;
}
///
/// Runs a query using the where clause to determine if duplicates are found (causes issues when deleting).
/// If duplicates are found, the check returns true, else it returns false;
///
private bool HasDuplicateRows(WhereClause where, string input, DbConnection connection)
{
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = input;
command.Parameters.AddRange(where.Parameters.ToArray());
try
{
return (Convert.ToInt32(command.ExecuteScalar())) > 1;
}
finally
{
command.Parameters.Clear();
}
}
}
///
/// Generates a edit row that represents a row pending deletion. All the original cells are
/// intact but the state is dirty.
///
/// Original, cached cell contents
/// EditRow that is pending deletion
public override EditRow GetEditRow(DbCellValue[] cachedRow)
{
Validate.IsNotNull(nameof(cachedRow), cachedRow);
return new EditRow
{
Id = RowId,
Cells = cachedRow.Select(cell => new EditCell(cell, true)).ToArray(),
State = EditRow.EditRowState.DirtyDelete
};
}
///
/// Generates a DELETE statement to delete this row
///
/// String of the DELETE statement
public override string GetScript()
{
return GetCommandText(GetWhereClause(false).CommandText);
}
///
/// Generates a WHERE statement to verify the row delete is unique.
///
/// String of the WHERE statement
public string GetVerifyScript()
{
return GetVerifyText(GetWhereClause(false).CommandText);
}
///
/// This method should not be called. A cell cannot be reverted on a row that is pending
/// deletion.
///
/// Ordinal of the column to update
public override EditRevertCellResult RevertCell(int columnId)
{
throw new InvalidOperationException(SR.EditDataDeleteSetCell);
}
///
/// This method should not be called. A cell cannot be updated on a row that is pending
/// deletion.
///
/// Always thrown
/// Ordinal of the column to update
/// New value for the cell
public override EditUpdateCellResult SetCell(int columnId, string newValue)
{
throw new InvalidOperationException(SR.EditDataDeleteSetCell);
}
protected override int CompareToSameType(RowEditBase rowEdit)
{
// We want to sort by row ID *IN REVERSE* to make sure we delete from the bottom first.
// If we delete from the top first, it will change IDs, making all subsequent deletes
// off by one or more!
return RowId.CompareTo(rowEdit.RowId) * -1;
}
private string GetVerifyText(string whereText)
{
return $"{VerifyStatement}{AssociatedObjectMetadata.EscapedMultipartName} {whereText}";
}
private string GetCommandText(string whereText)
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized
? DeleteMemoryOptimizedStatement
: DeleteStatement;
return string.Format(CultureInfo.InvariantCulture, formatString,
AssociatedObjectMetadata.EscapedMultipartName, whereText);
}
}
}