//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data.Common;
using Microsoft.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.SqlScriptFormatters;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
///
/// Base class for row edit operations. Provides basic information and helper functionality
/// that all RowEdit implementations can use. Defines functionality that must be implemented
/// in all child classes. Implements a custom IComparable to enable sorting by type of the edit
/// and then by an overrideable
///
public abstract class RowEditBase : IComparable
{
///
/// Internal parameterless constructor, required for mocking
///
// ReSharper disable once UnusedMember.Global
protected internal RowEditBase() { }
///
/// Base constructor for a row edit. Stores the state that should be available to all row
/// edit implementations.
///
/// The internal ID of the row that is being edited
/// The result set that will be updated
/// Metadata provider for the object to edit
protected RowEditBase(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
{
if (!associatedMetadata.HasExtendedProperties)
{
throw new ArgumentException(SR.EditDataMetadataNotExtended);
}
RowId = rowId;
AssociatedObjectMetadata = associatedMetadata;
AssociatedResultSet = associatedResultSet;
AssociatedObjectMetadata.Columns = EditTableMetadata.FilterColumnMetadata(AssociatedObjectMetadata.Columns, AssociatedResultSet.Columns);
}
#region Properties
///
/// The internal ID of the row to which this edit applies, relative to the result set
///
public long RowId { get; }
///
/// The result set that is associated with this row edit
///
public ResultSet AssociatedResultSet { get; }
///
/// The metadata for the table this edit is associated to
///
public EditTableMetadata AssociatedObjectMetadata { get; }
///
/// Sort ID for a row edit. Ensures that when a collection of RowEditBase objects are
/// sorted, the appropriate types are sorted to the top.
///
protected abstract int SortId { get; }
#endregion
#region Abstract Methods
///
/// Applies the changes to the associated result set
///
///
/// Data reader from execution of the command to commit the change to the db
///
public abstract Task ApplyChanges(DbDataReader dataReader);
///
/// Gets a command that will commit the change to the db
///
/// The connection to associate the command to
/// Command to commit the change to the db
public abstract DbCommand GetCommand(DbConnection connection);
///
/// Generates a row that has the pending update applied. The dirty status of the row is
/// reflected in the returned EditRow.
///
/// The original, cached row values
/// An EditRow with the pending changes applied
public abstract EditRow GetEditRow(DbCellValue[] cachedRow);
///
/// Converts the row edit into a SQL statement
///
/// A SQL statement
public abstract string GetScript();
///
/// Reverts a specific cell in row with pending edits
///
/// Ordinal ID of the column to revert
/// String value of the original value of the cell
public abstract EditRevertCellResult RevertCell(int columnId);
///
/// Changes the value a cell in the row.
///
/// Ordinal of the column in the row to update
/// The new value for the cell
/// The value of the cell after applying validation logic
public abstract EditUpdateCellResult SetCell(int columnId, string newValue);
#endregion
#region Protected Helper Methods
///
/// Performs validation of column ID and if column can be updated.
///
///
/// If is less than 0 or greater than the number of columns
/// in the row
///
/// If the column is not updatable
/// Ordinal of the column to update
protected void ValidateColumnIsUpdatable(int columnId)
{
// Sanity check that the column ID is within the range of columns
if (columnId >= AssociatedResultSet.Columns.Length || columnId < 0)
{
throw new ArgumentOutOfRangeException(nameof(columnId), SR.EditDataColumnIdOutOfRange);
}
DbColumnWrapper column = AssociatedResultSet.Columns[columnId];
if (!column.IsUpdatable)
{
throw new InvalidOperationException(SR.EditDataColumnCannotBeEdited);
}
}
///
/// Generates a WHERE clause that uses the key columns of the table to uniquely identity
/// the row that will be updated.
///
///
/// Whether or not to generate a parameterized where clause. If true verbatim values
/// will be replaced with paremeters (like @Param12). The parameters must be added to the
/// SqlCommand used to execute the commit.
///
/// A object
protected WhereClause GetWhereClause(bool parameterize)
{
WhereClause output = new WhereClause();
if (!AssociatedObjectMetadata.KeyColumns.Any())
{
throw new InvalidOperationException(SR.EditDataColumnNoKeyColumns);
}
IList row = AssociatedResultSet.GetRow(RowId);
foreach (EditColumnMetadata col in AssociatedObjectMetadata.KeyColumns)
{
// Put together a clause for the value of the cell
DbCellValue cellData = row[col.Ordinal];
string cellDataClause;
if (cellData.IsNull)
{
cellDataClause = "IS NULL";
}
else
{
if (cellData.RawObject is byte[] ||
col.DbColumn.DataTypeName.Equals("TEXT", StringComparison.OrdinalIgnoreCase) ||
col.DbColumn.DataTypeName.Equals("NTEXT", StringComparison.OrdinalIgnoreCase))
{
// Special cases for byte[] and TEXT/NTEXT types
cellDataClause = "IS NOT NULL";
}
else
{
// General case is to just use the value from the cell
if (parameterize)
{
// Add a parameter and parameterized clause component
// NOTE: We include the row ID to make sure the parameter is unique if
// we execute multiple row edits at once.
string paramName = $"@Param{RowId}{col.Ordinal}";
cellDataClause = $"= {paramName}";
SqlParameter parameter = new SqlParameter(paramName, col.DbColumn.SqlDbType)
{
Value = cellData.RawObject
};
output.Parameters.Add(parameter);
}
else
{
// Add the clause component with the formatted value
cellDataClause = $"= {ToSqlScript.FormatValue(cellData, col.DbColumn)}";
}
}
}
string completeComponent = $"({col.EscapedName} {cellDataClause})";
output.ClauseComponents.Add(completeComponent);
}
return output;
}
#endregion
#region IComparable Implementation
///
/// Compares a row edit against another row edit. If they are the same type, then we
/// compare using an overrideable "same type" comparer. If they are different types, they
/// are sorted by their sort indexes.
///
/// In general, RowCreate and RowUpdates are sorted to the top. RowDeletes are sorted last.
/// If there are ties, default behavior is to sort by row ID ascending.
///
/// The other row edit to compare against
///
/// A positive value if this edit should go first, a negative value if the other edit
/// should go first. 0 is returned if there is a tie.
///
public int CompareTo(RowEditBase other)
{
// If the other is null, this one will come out on top
if (other == null)
{
return 1;
}
// If types are the same, use the type's tiebreaking sorter
if (GetType() == other.GetType())
{
return CompareToSameType(other);
}
// If the type's sort index is the same, use our tiebreaking sorter
// If they are different, use that as the comparison
int sortIdComparison = SortId.CompareTo(other.SortId);
return sortIdComparison == 0
? CompareByRowId(other)
: sortIdComparison;
}
///
/// Default behavior for sorting if the two compared row edits are the same type. Sorts
/// by row ID ascending.
///
/// The other row edit to compare against
protected virtual int CompareToSameType(RowEditBase rowEdit)
{
return CompareByRowId(rowEdit);
}
///
/// Compares two row edits by their row ID ascending.
///
/// The other row edit to compare against
private int CompareByRowId(RowEditBase rowEdit)
{
return RowId.CompareTo(rowEdit.RowId);
}
#endregion
///
/// Represents a WHERE clause that can be used for identifying a row in a table.
///
protected class WhereClause
{
///
/// Constructs and initializes a new where clause
///
public WhereClause()
{
Parameters = new List();
ClauseComponents = new List();
}
///
/// SqlParameters used in a parameterized query. If this object was generated without
/// parameterization, this will be an empty list
///
public List Parameters { get; }
///
/// Strings that make up the WHERE clause, such as "([col1] = 'something')"
///
public List ClauseComponents { get; }
///
/// Total text of the WHERE clause that joins all the components with AND
///
public string CommandText => $"WHERE {string.Join(" AND ", ClauseComponents)}";
}
}
}