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

@@ -3,11 +3,14 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Data.Common;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
@@ -69,12 +72,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
[Theory]
[InlineData(true)]
[InlineData(false)]
public void GetScriptTest(bool isHekaton)
public void GetScriptTest(bool isMemoryOptimized)
{
// Setup: Create a fake table to update
DbColumn[] columns = Common.GetColumns(true);
ResultSet rs = Common.GetResultSet(columns, true);
IEditTableMetadata etm = Common.GetMetadata(columns, false, isHekaton);
IEditTableMetadata etm = Common.GetMetadata(columns, false, isMemoryOptimized);
// If: I ask for a script to be generated for update
RowUpdate ru = new RowUpdate(0, rs, etm);
@@ -86,7 +89,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Assert.NotNull(script);
// ... It should be formatted as an update script
string regexString = isHekaton
string regexString = isMemoryOptimized
? @"UPDATE (.+) WITH \(SNAPSHOT\) SET (.*) WHERE .+"
: @"UPDATE (.+) SET (.*) WHERE .+";
Regex r = new Regex(regexString);
@@ -101,5 +104,117 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Assert.Equal(3, updateSplit.Length);
Assert.All(updateSplit, s => Assert.Equal(2, s.Split('=').Length));
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void GetCommand(bool includeIdentity, bool isMemoryOptimized)
{
// Setup:
// ... Create a row update with cell updates
var columns = Common.GetColumns(includeIdentity);
var rs = Common.GetResultSet(columns, includeIdentity);
var etm = Common.GetMetadata(columns, !includeIdentity, isMemoryOptimized);
RowUpdate ru = new RowUpdate(0, rs, etm);
Common.AddCells(ru, includeIdentity);
// ... Mock db connection for building the command
var mockConn = new TestSqlConnection(null);
// If: I ask for a command to be generated for update
DbCommand cmd = ru.GetCommand(mockConn);
// Then:
// ... The command should not be null
Assert.NotNull(cmd);
// ... There should be an appropriate number of parameters in it
// (1 or 3 keys, 3 value parameters)
int expectedKeys = includeIdentity ? 1 : 3;
Assert.Equal(expectedKeys + 3, cmd.Parameters.Count);
// ... It should be formatted into an update script with output
string regexFormat = isMemoryOptimized
? @"UPDATE (.+) WITH \(SNAPSHOT\) SET (.+) OUTPUT (.+) WHERE (.+)"
: @"UPDATE (.+) SET (.+) OUTPUT(.+) WHERE (.+)";
Regex r = new Regex(regexFormat);
var m = r.Match(cmd.CommandText);
Assert.True(m.Success);
// ... There should be a table
string tbl = m.Groups[1].Value;
Assert.Equal(etm.EscapedMultipartName, tbl);
// ... There should be 3 parameters for input
string[] inCols = m.Groups[2].Value.Split(',');
Assert.Equal(3, inCols.Length);
Assert.All(inCols, s => Assert.Matches(@"\[.+\] = @Value\d+", s));
// ... There should be 3 OR 4 columns for output
string[] outCols = m.Groups[3].Value.Split(',');
Assert.Equal(includeIdentity ? 4 : 3, outCols.Length);
Assert.All(outCols, s => Assert.StartsWith("inserted.", s.Trim()));
// ... There should be 1 OR 3 columns for where components
string[] whereComponents = m.Groups[4].Value.Split(new[] {"AND"}, StringSplitOptions.None);
Assert.Equal(expectedKeys, whereComponents.Length);
Assert.All(whereComponents, s => Assert.Matches(@"\(.+ = @Param\d+\)", s));
}
[Fact]
public void GetCommandNullConnection()
{
// Setup: Create a row create
var columns = Common.GetColumns(false);
var rs = Common.GetResultSet(columns, false);
var etm = Common.GetMetadata(columns);
RowUpdate rc = new RowUpdate(0, rs, etm);
// If: I attempt to create a command with a null connection
// Then: It should throw an exception
Assert.Throws<ArgumentNullException>(() => rc.GetCommand(null));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ApplyChanges(bool includeIdentity)
{
// Setup:
// ... Create a row update (no cell updates needed)
var columns = Common.GetColumns(includeIdentity);
var rs = Common.GetResultSet(columns, includeIdentity);
var etm = Common.GetMetadata(columns, !includeIdentity);
RowUpdate ru = new RowUpdate(0, rs, etm);
long oldBytesWritten = rs.totalBytesWritten;
// ... Setup a db reader for the result of an update
var newRowReader = Common.GetNewRowDataReader(columns, includeIdentity);
// If: I ask for the change to be applied
await ru.ApplyChanges(newRowReader);
// Then:
// ... The result set should have the same number of rows as before
Assert.Equal(1, rs.RowCount);
Assert.True(oldBytesWritten < rs.totalBytesWritten);
}
[Fact]
public async Task ApplyChangesNullReader()
{
// Setup:
// ... Create a row update (no cell updates needed)
var columns = Common.GetColumns(true);
var rs = Common.GetResultSet(columns, true);
var etm = Common.GetMetadata(columns, false);
RowUpdate ru = new RowUpdate(0, rs, etm);
// If: I ask for the changes to be applied with a null db reader
// Then: I should get an exception
await Assert.ThrowsAsync<ArgumentNullException>(() => ru.ApplyChanges(null));
}
}
}