// // 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 System.Data.SqlClient; using System.Linq; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; 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. /// public abstract class RowEditBase { /// /// Internal parameterless constructor, required for mocking /// 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, IEditTableMetadata associatedMetadata) { RowId = rowId; AssociatedResultSet = associatedResultSet; AssociatedObjectMetadata = associatedMetadata; } #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 IEditTableMetadata AssociatedObjectMetadata { get; } #endregion /// /// Converts the row edit into a SQL statement /// /// A SQL statement public abstract string GetScript(); /// /// 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); /// /// 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 (EditColumnWrapper 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}"; output.Parameters.Add(new SqlParameter(paramName, col.DbColumn.SqlDbType)); } else { // Add the clause component with the formatted value cellDataClause = $"= {SqlScriptFormatter.FormatValue(cellData, col.DbColumn)}"; } } } string completeComponent = $"({col.EscapedName} {cellDataClause})"; output.ClauseComponents.Add(completeComponent); } return output; } /// /// 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)}"; } } }