edit/commit Command (#262)

The main goal of this feature is to enable a command that will
1) Generate a parameterized command for each edit that is in the session
2) Execute that command against the server
3) Update the cached results of the table/view that's being edited with the committed changes (including computed/identity columns)

There's some secret sauce in here where I cheated around worrying about gaps in the updated results. This was accomplished by implementing an IComparable for row edit objects that ensures deletes are the *last* actions to occur and that they occur from the bottom of the list up (highest row ID to lowest). Thus, all other actions that are dependent on the row ID are performed first, then the largest row ID is deleted, then next largest, etc. Nevertheless, by the end of a commit the associated ResultSet is still the source of truth. It is expected that the results grid will need updating once changes are committed.

Also worth noting, although this pull request supports a "many edits, one commit" approach, it will work just fine for a "one edit, one commit" approach.

* WIP

* Adding basic commit support. Deletions work!

* Nailing down the commit logic, insert commits work!

* Updates work!

* Fixing bug in DbColumnWrapper IsReadOnly setting

* Comments

* ResultSet unit tests, fixing issue with seeking in mock writers

* Unit tests for RowCreate commands

* Unit tests for RowDelete

* RowUpdate unit tests

* Session and edit base tests

* Fixing broken unit tests

* Moving constants to constants file

* Addressing code review feedback

* Fixes from merge issues, string consts

* Removing ad-hoc code

* fixing as per @abist requests

* Fixing a couple more issues
This commit is contained in:
Benjamin Russell
2017-03-03 15:47:47 -08:00
committed by GitHub
parent f00136cffb
commit 52ac038ebe
44 changed files with 2546 additions and 2464 deletions

View File

@@ -5,12 +5,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
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
{
@@ -19,8 +23,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// </summary>
public sealed class RowUpdate : RowEditBase
{
private const string UpdateStatement = "UPDATE {0} SET {1} {2}";
private const string UpdateStatementMemoryOptimized = "UPDATE {0} WITH (SNAPSHOT) SET {1} {2}";
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}";
private readonly Dictionary<int, CellUpdate> cellUpdates;
private readonly IList<DbCellValue> associatedRow;
@@ -38,6 +45,73 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
associatedRow = associatedResultSet.GetRow(rowId);
}
/// <summary>
/// Sort order property. Sorts to same position as RowCreate
/// </summary>
protected override int SortId => 1;
#region Public Methods
/// <summary>
/// Applies the changes to the associated result set after successfully executing the
/// change on the database
/// </summary>
/// <param name="dataReader">
/// 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.
/// </param>
public override Task ApplyChanges(DbDataReader dataReader)
{
Validate.IsNotNull(nameof(dataReader), dataReader);
return AssociatedResultSet.UpdateRow(RowId, dataReader);
}
/// <summary>
/// Generates a command that can be executed to update a row -- and return the contents of
/// the updated row.
/// </summary>
/// <param name="connection">The connection the command should be associated with</param>
/// <returns>Command to update the row</returns>
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
DbCommand command = connection.CreateCommand();
// Build the "SET" portion of the statement
List<string> setComponents = new List<string>();
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;
}
/// <summary>
/// Constructs an update statement to change the associated row.
/// </summary>
@@ -45,7 +119,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
public override string GetScript()
{
// Build the "SET" portion of the statement
IEnumerable<string> setComponents = cellUpdates.Values.Select(cellUpdate =>
var setComponents = cellUpdates.Values.Select(cellUpdate =>
{
string formattedColumnName = SqlScriptFormatter.FormatIdentifier(cellUpdate.Column.ColumnName);
string formattedValue = SqlScriptFormatter.FormatValue(cellUpdate.Value, cellUpdate.Column);
@@ -56,10 +130,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
// Get the where clause
string whereClause = GetWhereClause(false).CommandText;
// Put it all together
string formatString = AssociatedObjectMetadata.IsMemoryOptimized ? UpdateStatementMemoryOptimized : UpdateStatement;
return string.Format(CultureInfo.InvariantCulture, formatString,
AssociatedObjectMetadata.EscapedMultipartName, setClause, whereClause);
// Get the start of the statement
string statementStart = GetStatementStart();
// Put the whole #! together
return string.Format(UpdateScript, statementStart, setClause, whereClause);
}
/// <summary>
@@ -106,5 +181,16 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
IsRevert = false // If we're in this branch, it is not a revert
};
}
#endregion
private string GetStatementStart()
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized
? UpdateScriptStartMemOptimized
: UpdateScriptStart;
return string.Format(formatString, AssociatedObjectMetadata.EscapedMultipartName);
}
}
}