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

@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Data.Common;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
@@ -26,7 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I attempt to create a CellUpdate with a null string value
// Then: I should get an exception thrown
Assert.Throws<ArgumentNullException>(() => new CellUpdate(new CellUpdateTestDbColumn(null), null));
Assert.Throws<ArgumentNullException>(() => new CellUpdate(GetWrapper<string>("ntext"), null));
}
[Fact]
@@ -34,7 +35,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I attempt to create a CellUpdate to set it to NULL (with mixed cases)
const string nullString = "NULL";
DbColumn col = new CellUpdateTestDbColumn(typeof(string));
DbColumnWrapper col = GetWrapper<string>("ntext");
CellUpdate cu = new CellUpdate(col, nullString);
// Then: The value should be a DBNull and the string value should be the same as what
@@ -49,7 +50,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
public void NullTextStringTest()
{
// If: I attempt to create a CellUpdate with the text 'NULL' (with mixed case)
DbColumn col = new CellUpdateTestDbColumn(typeof(string));
DbColumnWrapper col = GetWrapper<string>("ntext");
CellUpdate cu = new CellUpdate(col, "'NULL'");
// Then: The value should be NULL
@@ -64,7 +65,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
public void ByteArrayTest(string strValue, byte[] expectedValue, string expectedString)
{
// If: I attempt to create a CellUpdate for a binary column
DbColumn col = new CellUpdateTestDbColumn(typeof(byte[]));
DbColumnWrapper col = GetWrapper<byte[]>("binary");
CellUpdate cu = new CellUpdate(col, strValue);
// Then: The value should be a binary and should match the expected data
@@ -118,7 +119,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I attempt to create a CellUpdate for a binary column
// Then: It should throw an exception
DbColumn col = new CellUpdateTestDbColumn(typeof(byte[]));
DbColumnWrapper col = GetWrapper<byte[]>("binary");
Assert.Throws<FormatException>(() => new CellUpdate(col, "this is totally invalid"));
}
@@ -127,7 +128,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
public void BoolTest(string input, bool output, string outputString)
{
// If: I attempt to create a CellUpdate for a boolean column
DbColumn col = new CellUpdateTestDbColumn(typeof(bool));
DbColumnWrapper col = GetWrapper<bool>("bit");
CellUpdate cu = new CellUpdate(col, input);
// Then: The value should match what was expected
@@ -153,23 +154,22 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I create a CellUpdate for a bool column and provide an invalid numeric value
// Then: It should throw an exception
DbColumn col = new CellUpdateTestDbColumn(typeof(bool));
DbColumnWrapper col = GetWrapper<bool>("bit");
Assert.Throws<ArgumentOutOfRangeException>(() => new CellUpdate(col, "12345"));
}
[Theory]
[MemberData(nameof(RoundTripTestParams))]
public void RoundTripTest(Type dbColType, object obj)
public void RoundTripTest(DbColumnWrapper col, object obj)
{
// Setup: Figure out the test string
string testString = obj.ToString();
// If: I attempt to create a CellUpdate for a GUID column
DbColumn col = new CellUpdateTestDbColumn(dbColType);
// If: I attempt to create a CellUpdate
CellUpdate cu = new CellUpdate(col, testString);
// Then: The value and type should match what we put in
Assert.IsType(dbColType, cu.Value);
Assert.IsType(col.DataType, cu.Value);
Assert.Equal(obj, cu.Value);
Assert.Equal(testString, cu.ValueAsString);
Assert.Equal(col, cu.Column);
@@ -179,29 +179,35 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
get
{
yield return new object[] {typeof(Guid), Guid.NewGuid()};
yield return new object[] {typeof(TimeSpan), new TimeSpan(0, 1, 20, 0, 123)};
yield return new object[] {typeof(DateTime), new DateTime(2016, 04, 25, 9, 45, 0)};
yield return new object[] {GetWrapper<Guid>("uniqueidentifier"), Guid.NewGuid()};
yield return new object[] {GetWrapper<TimeSpan>("time"), new TimeSpan(0, 1, 20, 0, 123)};
yield return new object[] {GetWrapper<DateTime>("datetime"), new DateTime(2016, 04, 25, 9, 45, 0)};
yield return new object[]
{
typeof(DateTimeOffset),
GetWrapper<DateTimeOffset>("datetimeoffset"),
new DateTimeOffset(2016, 04, 25, 9, 45, 0, TimeSpan.FromHours(8))
};
yield return new object[] {typeof(long), 1000L};
yield return new object[] {typeof(decimal), new decimal(3.14)};
yield return new object[] {typeof(int), 1000};
yield return new object[] {typeof(short), (short) 1000};
yield return new object[] {typeof(byte), (byte) 5};
yield return new object[] {typeof(double), 3.14d};
yield return new object[] {typeof(float), 3.14f};
yield return new object[] {GetWrapper<long>("bigint"), 1000L};
yield return new object[] {GetWrapper<decimal>("decimal"), new decimal(3.14)};
yield return new object[] {GetWrapper<int>("int"), 1000};
yield return new object[] {GetWrapper<short>("smallint"), (short) 1000};
yield return new object[] {GetWrapper<byte>("tinyint"), (byte) 5};
yield return new object[] {GetWrapper<double>("float"), 3.14d};
yield return new object[] {GetWrapper<float>("real"), 3.14f};
}
}
private static DbColumnWrapper GetWrapper<T>(string dataTypeName)
{
return new DbColumnWrapper(new CellUpdateTestDbColumn(typeof(T), dataTypeName));
}
private class CellUpdateTestDbColumn : DbColumn
{
public CellUpdateTestDbColumn(Type dataType)
public CellUpdateTestDbColumn(Type dataType, string dataTypeName)
{
DataType = dataType;
DataTypeName = dataTypeName;
}
}
}

View File

@@ -69,18 +69,27 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
return columns.ToArray();
}
public static ResultSet GetResultSet(DbColumn[] columns, bool includeIdentity)
public static ResultSet GetResultSet(DbColumn[] columns, bool includeIdentity, int rowCount = 1)
{
object[][] rows = includeIdentity
? new[] { new object[] { "id", "1", "2", "3" } }
: new[] { new object[] { "1", "2", "3" } };
IEnumerable<object[]> rows = includeIdentity
? Enumerable.Repeat(new object[] { "id", "1", "2", "3" }, rowCount)
: Enumerable.Repeat(new object[] { "1", "2", "3" }, rowCount);
var testResultSet = new TestResultSet(columns, rows);
var reader = new TestDbDataReader(new[] { testResultSet });
var resultSet = new ResultSet(reader, 0, 0, MemoryFileSystem.GetFileStreamFactory());
resultSet.ReadResultToEnd(CancellationToken.None).Wait();
var resultSet = new ResultSet(0, 0, MemoryFileSystem.GetFileStreamFactory());
resultSet.ReadResultToEnd(reader, CancellationToken.None).Wait();
return resultSet;
}
public static DbDataReader GetNewRowDataReader(DbColumn[] columns, bool includeIdentity)
{
object[][] rows = includeIdentity
? new[] {new object[] {"id", "q", "q", "q"}}
: new[] {new object[] {"q", "q", "q"}};
var testResultSet = new TestResultSet(columns, rows);
return new TestDbDataReader(new [] {testResultSet});
}
public static void AddCells(RowEditBase rc, bool includeIdentity)
{
// Skip the first column since if identity, since identity columns can't be updated

View File

@@ -6,9 +6,11 @@
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
@@ -80,5 +82,110 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
RowCreate rc = new RowCreate(rowId, rs, etm);
Assert.Throws<InvalidOperationException>(() => rc.GetScript());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ApplyChanges(bool includeIdentity)
{
// Setup:
// ... Generate the parameters for the row create
const long rowId = 100;
DbColumn[] columns = Common.GetColumns(includeIdentity);
ResultSet rs = Common.GetResultSet(columns, includeIdentity);
IEditTableMetadata etm = Common.GetMetadata(columns);
// ... Setup a db reader for the result of an insert
var newRowReader = Common.GetNewRowDataReader(columns, includeIdentity);
// If: I ask for the change to be applied
RowCreate rc = new RowCreate(rowId, rs, etm);
await rc.ApplyChanges(newRowReader);
// Then: The result set should have an additional row in it
Assert.Equal(2, rs.RowCount);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void GetCommand(bool includeIdentity)
{
// Setup:
// ... Create a row create with cell updates
const long rowId = 100;
var columns = Common.GetColumns(includeIdentity);
var rs = Common.GetResultSet(columns, includeIdentity);
var etm = Common.GetMetadata(columns);
RowCreate rc = new RowCreate(rowId, rs, etm);
Common.AddCells(rc, includeIdentity);
// ... Mock db connection for building the command
var mockConn = new TestSqlConnection(null);
// If: I attempt to get a command for the edit
DbCommand cmd = rc.GetCommand(mockConn);
// Then:
// ... The command should not be null
Assert.NotNull(cmd);
// ... There should be parameters in it
Assert.Equal(3, cmd.Parameters.Count);
// ... It should be formatted into an insert script with output
Regex r = new Regex(@"INSERT INTO (.+)\((.+)\) OUTPUT (.+) VALUES \((.+)\)");
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 columns for input
string inCols = m.Groups[2].Value;
Assert.Equal(3, inCols.Split(',').Length);
// ... There should be 3 OR 4 columns for output that are inserted.
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 3 parameters
string[] param = m.Groups[4].Value.Split(',');
Assert.Equal(3, param.Length);
Assert.All(param, s => Assert.StartsWith("@Value", s.Trim()));
}
[Fact]
public void GetCommandNullConnection()
{
// Setup: Create a row create
const long rowId = 100;
var columns = Common.GetColumns(false);
var rs = Common.GetResultSet(columns, false);
var etm = Common.GetMetadata(columns);
RowCreate rc = new RowCreate(rowId, 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));
}
[Fact]
public void GetCommandMissingCell()
{
// Setup: Generate the parameters for the row create
const long rowId = 100;
var columns = Common.GetColumns(false);
var rs = Common.GetResultSet(columns, false);
var etm = Common.GetMetadata(columns);
var mockConn = new TestSqlConnection(null);
// If: I ask for a script to be generated without setting any values
// Then: An exception should be thrown for missing cells
RowCreate rc = new RowCreate(rowId, rs, etm);
Assert.Throws<InvalidOperationException>(() => rc.GetCommand(mockConn));
}
}
}

View File

@@ -5,9 +5,12 @@
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
@@ -34,11 +37,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
[Theory]
[InlineData(true)]
[InlineData(false)]
public void GetScriptTest(bool isHekaton)
public void GetScriptTest(bool isMemoryOptimized)
{
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 delete
RowDelete rd = new RowDelete(0, rs, etm);
@@ -50,13 +53,94 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// ... It should be formatted as a delete script
string scriptStart = $"DELETE FROM {etm.EscapedMultipartName}";
if (isHekaton)
if (isMemoryOptimized)
{
scriptStart += " WITH(SNAPSHOT)";
}
Assert.StartsWith(scriptStart, script);
}
[Fact]
public async Task ApplyChanges()
{
// Setup: Generate the parameters for the row delete object
// We don't care about the values besides the row ID
const long rowId = 0;
var columns = Common.GetColumns(false);
var rs = Common.GetResultSet(columns, false);
var etm = Common.GetMetadata(columns);
// If: I ask for the change to be applied
RowDelete rd = new RowDelete(rowId, rs, etm);
await rd.ApplyChanges(null); // Reader not used, can be null
// Then : The result set should have one less row in it
Assert.Equal(0, rs.RowCount);
}
[Theory]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(false, false)]
public void GetCommand(bool includeIdentity, bool isMemoryOptimized)
{
// Setup:
// ... Create a row delete
const long rowId = 0;
var columns = Common.GetColumns(includeIdentity);
var rs = Common.GetResultSet(columns, includeIdentity);
var etm = Common.GetMetadata(columns, !includeIdentity, isMemoryOptimized);
RowDelete rd = new RowDelete(rowId, rs, etm);
// ... Mock db connection for building the command
var mockConn = new TestSqlConnection(null);
// If: I attempt to get a command for the edit
DbCommand cmd = rd.GetCommand(mockConn);
// Then:
// ... The command should not be null
Assert.NotNull(cmd);
// ... Only the keys should be used for parameters
int expectedKeys = includeIdentity ? 1 : 3;
Assert.Equal(expectedKeys, cmd.Parameters.Count);
// ... It should be formatted into an delete script
string regexTest = isMemoryOptimized
? @"DELETE FROM (.+) WITH\(SNAPSHOT\) WHERE (.+)"
: @"DELETE FROM (.+) WHERE (.+)";
Regex r = new Regex(regexTest);
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 as many where components as there are keys
string[] whereComponents = m.Groups[2].Value.Split(new[] {"AND"}, StringSplitOptions.None);
Assert.Equal(expectedKeys, whereComponents.Length);
// ... Each component should have be equal to a parameter
Assert.All(whereComponents, c => Assert.True(Regex.IsMatch(c.Trim(), @"\(.+ = @.+\)")));
}
[Fact]
public void GetCommandNullConnection()
{
// Setup: Create a row delete
var columns = Common.GetColumns(false);
var rs = Common.GetResultSet(columns, false);
var etm = Common.GetMetadata(columns);
RowDelete rd = new RowDelete(0, rs, etm);
// If: I attempt to create a command with a null connection
// Then: It should throw an exception
Assert.Throws<ArgumentNullException>(() => rd.GetCommand(null));
}
[Fact]
public void SetCell()
{

View File

@@ -9,6 +9,7 @@ using System.Data.Common;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
@@ -99,13 +100,104 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
rt.ValidateWhereClauseNoKeys();
}
[Fact]
public void SortingByTypeTest()
{
// Setup: Create a result set and metadata we can reuse
var cols = Common.GetColumns(false);
var rs = Common.GetResultSet(cols, false);
var etm = Common.GetMetadata(cols);
// If: I request to sort a list of the three different edit operations
List<RowEditBase> rowEdits = new List<RowEditBase>
{
new RowDelete(0, rs, etm),
new RowUpdate(0, rs, etm),
new RowCreate(0, rs, etm)
};
rowEdits.Sort();
// Then: Delete should be the last operation to execute
// (we don't care about the order of the other two)
Assert.IsType<RowDelete>(rowEdits.Last());
}
[Fact]
public void SortingUpdatesByRowIdTest()
{
// Setup: Create a result set and metadata we can reuse
var cols = Common.GetColumns(false);
var rs = Common.GetResultSet(cols, false, 4);
var etm = Common.GetMetadata(cols);
// If: I sort 3 edit operations of the same type
List<RowEditBase> rowEdits = new List<RowEditBase>
{
new RowUpdate(3, rs, etm),
new RowUpdate(1, rs, etm),
new RowUpdate(2, rs, etm)
};
rowEdits.Sort();
// Then: They should be in order by row ID ASCENDING
Assert.Equal(1, rowEdits[0].RowId);
Assert.Equal(2, rowEdits[1].RowId);
Assert.Equal(3, rowEdits[2].RowId);
}
[Fact]
public void SortingCreatesByRowIdTest()
{
// Setup: Create a result set and metadata we can reuse
var cols = Common.GetColumns(false);
var rs = Common.GetResultSet(cols, false);
var etm = Common.GetMetadata(cols);
// If: I sort 3 edit operations of the same type
List<RowEditBase> rowEdits = new List<RowEditBase>
{
new RowCreate(3, rs, etm),
new RowCreate(1, rs, etm),
new RowCreate(2, rs, etm)
};
rowEdits.Sort();
// Then: They should be in order by row ID ASCENDING
Assert.Equal(1, rowEdits[0].RowId);
Assert.Equal(2, rowEdits[1].RowId);
Assert.Equal(3, rowEdits[2].RowId);
}
[Fact]
public void SortingDeletesByRowIdTest()
{
// Setup: Create a result set and metadata we can reuse
var cols = Common.GetColumns(false);
var rs = Common.GetResultSet(cols, false);
var etm = Common.GetMetadata(cols);
// If: I sort 3 delete operations of the same type
List<RowEditBase> rowEdits = new List<RowEditBase>
{
new RowDelete(1, rs, etm),
new RowDelete(3, rs, etm),
new RowDelete(2, rs, etm)
};
rowEdits.Sort();
// Then: They should be in order by row ID DESCENDING
Assert.Equal(3, rowEdits[0].RowId);
Assert.Equal(2, rowEdits[1].RowId);
Assert.Equal(1, rowEdits[2].RowId);
}
private static ResultSet GetResultSet(DbColumn[] columns, object[] row)
{
object[][] rows = {row};
var testResultSet = new TestResultSet(columns, rows);
var testReader = new TestDbDataReader(new [] {testResultSet});
var resultSet = new ResultSet(testReader, 0,0, MemoryFileSystem.GetFileStreamFactory());
resultSet.ReadResultToEnd(CancellationToken.None).Wait();
var resultSet = new ResultSet(0,0, MemoryFileSystem.GetFileStreamFactory());
resultSet.ReadResultToEnd(testReader, CancellationToken.None).Wait();
return resultSet;
}
@@ -179,6 +271,18 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
throw new NotImplementedException();
}
public override Task ApplyChanges(DbDataReader reader)
{
throw new NotImplementedException();
}
public override DbCommand GetCommand(DbConnection conn)
{
throw new NotImplementedException();
}
protected override int SortId => 0;
}
}
}

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

View File

@@ -19,7 +19,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
public class ServiceIntegrationTests
{
#region Session Operation Helper Tests
#region EditSession Operation Helper Tests
[Theory]
[InlineData(null)]
@@ -128,7 +128,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
efv.Validate();
// ... There should be a delete in the session
Session s = eds.ActiveSessions[Constants.OwnerUri];
EditSession s = eds.ActiveSessions[Constants.OwnerUri];
Assert.True(s.EditCache.Any(e => e.Value is RowDelete));
}
@@ -150,7 +150,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
efv.Validate();
// ... There should be a create in the session
Session s = eds.ActiveSessions[Constants.OwnerUri];
EditSession s = eds.ActiveSessions[Constants.OwnerUri];
Assert.True(s.EditCache.Any(e => e.Value is RowCreate));
}
@@ -174,7 +174,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
efv.Validate();
// ... The edit cache should be empty again
Session s = eds.ActiveSessions[Constants.OwnerUri];
EditSession s = eds.ActiveSessions[Constants.OwnerUri];
Assert.Empty(s.EditCache);
}
@@ -215,13 +215,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
edit.Verify(e => e.SetCell(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
}
private static Session GetDefaultSession()
private static EditSession GetDefaultSession()
{
// ... Create a session with a proper query and metadata
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
return s;
}
}

View File

@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.EditData;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
@@ -28,7 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I create a session object without a null query
// Then: It should throw an exception
Assert.Throws<ArgumentNullException>(() => new Session(null, Common.GetMetadata(new DbColumn[] {})));
Assert.Throws<ArgumentNullException>(() => new EditSession(null, Common.GetMetadata(new DbColumn[] {})));
}
[Fact]
@@ -38,7 +39,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// Then: It should throw an exception
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
Assert.Throws<ArgumentNullException>(() => new Session(rs, null));
Assert.Throws<ArgumentNullException>(() => new EditSession(rs, null));
}
[Fact]
@@ -48,12 +49,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// Then:
// ... The edit cache should exist and be empty
Assert.NotNull(s.EditCache);
Assert.Empty(s.EditCache);
Assert.Null(s.CommitTask);
// ... The next row ID should be equivalent to the number of rows in the result set
Assert.Equal(q.Batches[0].ResultSets[0].RowCount, s.NextRowId);
@@ -70,7 +72,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// Then: It should throw an exception
Query q = QueryExecution.Common.GetBasicExecutedQuery();
q.HasExecuted = false;
Assert.Throws<InvalidOperationException>(() => Session.ValidateQueryForSession(q));
Assert.Throws<InvalidOperationException>(() => EditSession.ValidateQueryForSession(q));
}
[Fact]
@@ -94,7 +96,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// If: I create a session object with a query that has !=1 result sets
// Then: It should throw an exception
Assert.Throws<InvalidOperationException>(() => Session.ValidateQueryForSession(query));
Assert.Throws<InvalidOperationException>(() => EditSession.ValidateQueryForSession(query));
}
[Fact]
@@ -102,7 +104,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
{
// If: I validate a query for a session with a valid query
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = Session.ValidateQueryForSession(q);
ResultSet rs = EditSession.ValidateQueryForSession(q);
// Then: I should get the only result set back
Assert.NotNull(rs);
@@ -121,7 +123,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// ... Add a mock edit to the edit cache to cause the .TryAdd to fail
var mockEdit = new Mock<RowEditBase>().Object;
@@ -146,7 +148,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I add a row to the session
long newId = s.CreateRow();
@@ -167,13 +169,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
[Theory]
[MemberData(nameof(RowIdOutOfRangeData))]
public void RowIdOutOfRange(long rowId, Action<Session, long> testAction)
public void RowIdOutOfRange(long rowId, Action<EditSession, long> testAction)
{
// Setup: Create a session with a proper query and metadata
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I delete a row that is out of range for the result set
// Then: I should get an exception
@@ -185,12 +187,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
get
{
// Delete Row
Action<Session, long> delAction = (s, l) => s.DeleteRow(l);
Action<EditSession, long> delAction = (s, l) => s.DeleteRow(l);
yield return new object[] { -1L, delAction };
yield return new object[] { 100L, delAction };
// Update Cell
Action<Session, long> upAction = (s, l) => s.UpdateCell(l, 0, null);
Action<EditSession, long> upAction = (s, l) => s.UpdateCell(l, 0, null);
yield return new object[] { -1L, upAction };
yield return new object[] { 100L, upAction };
}
@@ -206,7 +208,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// ... Add a mock edit to the edit cache to cause the .TryAdd to fail
var mockEdit = new Mock<RowEditBase>().Object;
@@ -228,7 +230,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I add a row to the session
s.DeleteRow(0);
@@ -249,7 +251,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I revert a row that doesn't have any pending changes
// Then: I should get an exception
@@ -264,7 +266,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// ... Add a mock edit to the edit cache to cause the .TryAdd to fail
var mockEdit = new Mock<RowEditBase>().Object;
@@ -290,7 +292,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// ... Add a mock edit to the edit cache to cause the .TryAdd to fail
var mockEdit = new Mock<RowEditBase>();
@@ -314,7 +316,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I update a cell on a row that does not have a pending edit
s.UpdateCell(0, 0, "");
@@ -339,7 +341,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// If: I try to script the edit cache with a null or whitespace output path
// Then: It should throw an exception
@@ -354,7 +356,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
Session s = new Session(rs, etm);
EditSession s = new EditSession(rs, etm);
// ... Add two mock edits that will generate a script
Mock<RowEditBase> edit = new Mock<RowEditBase>();
@@ -377,5 +379,163 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
}
#endregion
#region Commit Tests
[Fact]
public void CommitNullConnection()
{
// Setup: Create a basic session
EditSession s = GetBasicSession();
// If: I attempt to commit with a null connection
// Then: I should get an exception
Assert.Throws<ArgumentNullException>(
() => s.CommitEdits(null, () => Task.CompletedTask, e => Task.CompletedTask));
}
[Fact]
public void CommitNullSuccessHandler()
{
// Setup:
// ... Create a basic session
EditSession s = GetBasicSession();
// ... Mock db connection
DbConnection conn = new TestSqlConnection(null);
// If: I attempt to commit with a null success handler
// Then: I should get an exception
Assert.Throws<ArgumentNullException>(() => s.CommitEdits(conn, null, e => Task.CompletedTask));
}
[Fact]
public void CommitNullFailureHandler()
{
// Setup:
// ... Create a basic session
EditSession s = GetBasicSession();
// ... Mock db connection
DbConnection conn = new TestSqlConnection(null);
// If: I attempt to commit with a null success handler
// Then: I should get an exception
Assert.Throws<ArgumentNullException>(() => s.CommitEdits(conn, () => Task.CompletedTask, null));
}
[Fact]
public void CommitInProgress()
{
// Setup:
// ... Basic session and db connection
EditSession s = GetBasicSession();
DbConnection conn = new TestSqlConnection(null);
// ... Mock a task that has not completed
Task notCompleted = new Task(() => {});
s.CommitTask = notCompleted;
// If: I attempt to commit while a task is in progress
// Then: I should get an exception
Assert.Throws<InvalidOperationException>(
() => s.CommitEdits(conn, () => Task.CompletedTask, e => Task.CompletedTask));
}
[Fact]
public async Task CommitSuccess()
{
// Setup:
// ... Basic session and db connection
EditSession s = GetBasicSession();
DbConnection conn = new TestSqlConnection(null);
// ... Add a mock commands for fun
Mock<RowEditBase> edit = new Mock<RowEditBase>();
edit.Setup(e => e.GetCommand(It.IsAny<DbConnection>())).Returns<DbConnection>(dbc => dbc.CreateCommand());
edit.Setup(e => e.ApplyChanges(It.IsAny<DbDataReader>())).Returns(Task.FromResult(0));
s.EditCache[0] = edit.Object;
// If: I commit these changes (and await completion)
bool successCalled = false;
bool failureCalled = false;
s.CommitEdits(conn,
() => {
successCalled = true;
return Task.FromResult(0);
},
e => {
failureCalled = true;
return Task.FromResult(0);
});
await s.CommitTask;
// Then:
// ... The task should still exist
Assert.NotNull(s.CommitTask);
// ... The success handler should have been called (not failure)
Assert.True(successCalled);
Assert.False(failureCalled);
// ... The mock edit should have generated a command and applied changes
edit.Verify(e => e.GetCommand(conn), Times.Once);
edit.Verify(e => e.ApplyChanges(It.IsAny<DbDataReader>()), Times.Once);
// ... The edit cache should be empty
Assert.Empty(s.EditCache);
}
[Fact]
public async Task CommitFailure()
{
// Setup:
// ... Basic session and db connection
EditSession s = GetBasicSession();
DbConnection conn = new TestSqlConnection(null);
// ... Add a mock edit that will explode on generating a command
Mock<RowEditBase> edit = new Mock<RowEditBase>();
edit.Setup(e => e.GetCommand(It.IsAny<DbConnection>())).Throws<Exception>();
s.EditCache[0] = edit.Object;
// If: I commit these changes (and await completion)
bool successCalled = false;
bool failureCalled = false;
s.CommitEdits(conn,
() => {
successCalled = true;
return Task.FromResult(0);
},
e => {
failureCalled = true;
return Task.FromResult(0);
});
await s.CommitTask;
// Then:
// ... The task should still exist
Assert.NotNull(s.CommitTask);
// ... The error handler should have been called (not success)
Assert.False(successCalled);
Assert.True(failureCalled);
// ... The mock edit should have been asked to generate a command
edit.Verify(e => e.GetCommand(conn), Times.Once);
// ... The edit cache should not be empty
Assert.NotEmpty(s.EditCache);
}
#endregion
private static EditSession GetBasicSession()
{
Query q = QueryExecution.Common.GetBasicExecutedQuery();
ResultSet rs = q.Batches[0].ResultSets[0];
IEditTableMetadata etm = Common.GetMetadata(rs.Columns);
return new EditSession(rs, etm);
}
}
}