//
// 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)}";
}
}
}