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

@@ -24,10 +24,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution
{
// Setup: Mock file stream factory, mock db reader
var mockFileStreamFactory = new Mock<IFileStreamFactory>();
var mockDataReader = Common.CreateTestConnection(null, false).CreateCommand().ExecuteReaderAsync().Result;
// If: I setup a single resultset and then dispose it
ResultSet rs = new ResultSet(mockDataReader, Common.Ordinal, Common.Ordinal, mockFileStreamFactory.Object);
ResultSet rs = new ResultSet(Common.Ordinal, Common.Ordinal, mockFileStreamFactory.Object);
rs.Dispose();
// Then: The file that was created should have been deleted

View File

@@ -4,6 +4,7 @@
//
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Threading;
@@ -24,8 +25,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
{
// If:
// ... I create a new result set with a valid db data reader
DbDataReader mockReader = GetReader(null, false, string.Empty);
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, MemoryFileSystem.GetFileStreamFactory());
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, MemoryFileSystem.GetFileStreamFactory());
// Then:
// ... There should not be any data read yet
@@ -41,14 +41,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
}
[Fact]
public void ResultCreationInvalidReader()
public async Task ReadToEndNullReader()
{
// If:
// ... I create a new result set without a reader
// Then:
// ... It should throw an exception
Assert.Throws<ArgumentNullException>(() => new ResultSet(null, Common.Ordinal, Common.Ordinal, null));
// If: I create a new result set with a null db data reader
// Then: I should get an exception
var fsf = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fsf);
await Assert.ThrowsAsync<ArgumentNullException>(() => resultSet.ReadResultToEnd(null, CancellationToken.None));
}
[Fact]
@@ -67,9 +66,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
// ... and I read it to the end
DbDataReader mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// Then:
// ... The columns should be set
@@ -86,7 +85,35 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
// ... The callback for result set completion should have been fired
Assert.NotNull(resultSummaryFromCallback);
}
[Theory]
[MemberData(nameof(CallMethodWithoutReadingData))]
public void CallMethodWithoutReading(Action<ResultSet> testMethod)
{
// Setup: Create a new result set with valid db data reader
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
// If:
// ... I have a result set that has not been read
// ... and I attempt to call a method on it
// Then: It should throw an exception
Assert.ThrowsAny<Exception>(() => testMethod(resultSet));
}
public static IEnumerable<object> CallMethodWithoutReadingData
{
get
{
yield return new object[] {new Action<ResultSet>(rs => rs.GetSubset(0, 0).Wait())};
yield return new object[] {new Action<ResultSet>(rs => rs.UpdateRow(0, null).Wait())};
yield return new object[] {new Action<ResultSet>(rs => rs.AddRow(null).Wait())};
yield return new object[] {new Action<ResultSet>(rs => rs.RemoveRow(0))};
yield return new object[] {new Action<ResultSet>(rs => rs.GetRow(0))};
yield return new object[] {new Action<ResultSet>(rs => rs.GetExecutionPlan().Wait())};
}
}
[Theory]
[InlineData("JSON")]
[InlineData("XML")]
@@ -111,9 +138,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
// ... and I read it to the end
DbDataReader mockReader = GetReader(dataSets, false, Constants.StandardQuery);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// Then:
// ... There should only be one column
@@ -133,20 +160,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
Assert.Equal(1, subset.RowCount);
}
[Fact]
public async Task GetSubsetWithoutExecution()
{
// If:
// ... I create a new result set with a valid db data reader without executing it
DbDataReader mockReader = GetReader(null, false, string.Empty);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
// Then:
// ... Attempting to read a subset should fail miserably
await Assert.ThrowsAsync<InvalidOperationException>(() => resultSet.GetSubset(0, 0));
}
[Theory]
[InlineData(-1, 0)] // Too small start row
[InlineData(20, 0)] // Too large start row
@@ -158,8 +171,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
// ... And execute the result
DbDataReader mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... And attempt to get a subset with invalid parameters
// Then:
@@ -179,8 +192,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
// ... And execute the result set
DbDataReader mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... And attempt to get a subset with valid number of rows
ResultSetSubset subset = await resultSet.GetSubset(startRow, rowCount);
@@ -195,6 +208,159 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution
Assert.Equal(resultSet.Columns.Length, subset.Rows[0].Length);
}
[Theory]
[MemberData(nameof(RowInvalidParameterData))]
public async Task RowInvalidParameter(Action<ResultSet> actionToPerform)
{
// If: I create a new result set and execute it
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory();
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// Then: Attempting to read an invalid row should fail
Assert.ThrowsAny<Exception>(() => actionToPerform(resultSet));
}
public static IEnumerable<object> RowInvalidParameterData
{
get
{
foreach (var method in RowInvalidParameterMethods)
{
yield return new object[] {new Action<ResultSet>(rs => method(rs, -1))};
yield return new object[] {new Action<ResultSet>(rs => method(rs, 100))};
}
}
}
public static IEnumerable<Action<ResultSet, long>> RowInvalidParameterMethods
{
get
{
yield return (rs, id) => rs.RemoveRow(id);
yield return (rs, id) => rs.GetRow(id);
yield return (rs, id) => rs.UpdateRow(id, null).Wait();
}
}
[Fact]
public async Task RemoveRowSuccess()
{
// Setup: Create a result set that has the standard data set on it
var fileFactory = MemoryFileSystem.GetFileStreamFactory();
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// If: I delete a row from the result set
resultSet.RemoveRow(0);
// Then:
// ... The row count should decrease
// ... The last row should have moved up by 1
Assert.Equal(Common.StandardRows - 1, resultSet.RowCount);
Assert.Throws<ArgumentOutOfRangeException>(() => resultSet.GetRow(Common.StandardRows - 1));
}
[Fact]
public async Task AddRowNoRows()
{
// Setup:
// ... Create a standard result set with standard data
var fileFactory = MemoryFileSystem.GetFileStreamFactory();
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... Create a mock reader that has no rows
var emptyReader = GetReader(new[] {new TestResultSet(5, 0)}, false, Constants.StandardQuery);
// If: I add a row with a reader that has no rows
// Then:
// ... I should get an exception
await Assert.ThrowsAsync<InvalidOperationException>(() => resultSet.AddRow(emptyReader));
// ... The row count should not have changed
Assert.Equal(Common.StandardRows, resultSet.RowCount);
}
[Fact]
public async Task AddRowSuccess()
{
// Setup:
// ... Create a standard result set with standard data
var fileFactory = MemoryFileSystem.GetFileStreamFactory();
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... Create a mock reader that has one row
object[] row = Enumerable.Range(0, Common.StandardColumns).Select(i => "QQQ").ToArray();
IEnumerable<object[]> rows = new List<object[]>{ row };
TestResultSet[] results = {new TestResultSet(TestResultSet.GetStandardColumns(Common.StandardColumns), rows)};
var newRowReader = GetReader(results, false, Constants.StandardQuery);
// If: I add a new row to the result set
await resultSet.AddRow(newRowReader);
// Then:
// ... There should be a new row in the list of rows
Assert.Equal(Common.StandardRows + 1, resultSet.RowCount);
// ... The new row should be readable and all cells contain the test value
Assert.All(resultSet.GetRow(Common.StandardRows), cell => Assert.Equal("QQQ", cell.RawObject));
}
[Fact]
public async Task UpdateRowNoRows()
{
// Setup:
// ... Create a standard result set with standard data
var fileFactory = MemoryFileSystem.GetFileStreamFactory();
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... Create a mock reader that has no rows
var emptyReader = GetReader(new[] { new TestResultSet(5, 0) }, false, Constants.StandardQuery);
// If: I add a row with a reader that has no rows
// Then:
// ... I should get an exception
await Assert.ThrowsAsync<InvalidOperationException>(() => resultSet.UpdateRow(0, emptyReader));
// ... The row count should not have changed
Assert.Equal(Common.StandardRows, resultSet.RowCount);
}
[Fact]
public async Task UpdateRowSuccess()
{
// Setup:
// ... Create a standard result set with standard data
var fileFactory = MemoryFileSystem.GetFileStreamFactory();
var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery);
ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory);
await resultSet.ReadResultToEnd(mockReader, CancellationToken.None);
// ... Create a mock reader that has one row
object[] row = Enumerable.Range(0, Common.StandardColumns).Select(i => "QQQ").ToArray();
IEnumerable<object[]> rows = new List<object[]> { row };
TestResultSet[] results = { new TestResultSet(TestResultSet.GetStandardColumns(Common.StandardColumns), rows) };
var newRowReader = GetReader(results, false, Constants.StandardQuery);
// If: I add a new row to the result set
await resultSet.UpdateRow(0, newRowReader);
// Then:
// ... There should be the same number of rows
Assert.Equal(Common.StandardRows, resultSet.RowCount);
// ... The new row should be readable and all cells contain the test value
Assert.All(resultSet.GetRow(0), cell => Assert.Equal("QQQ", cell.RawObject));
}
private static DbDataReader GetReader(TestResultSet[] dataSet, bool throwOnRead, string query)
{
var info = Common.CreateTestConnectionInfo(dataSet, throwOnRead);

View File

@@ -27,7 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// If: I attempt to save with a null set of params
// Then: I should get a null argument exception
ResultSet rs = new ResultSet(
GetReader(null, false, Common.NoOpQuery), Common.Ordinal, Common.Ordinal,
Common.Ordinal, Common.Ordinal,
MemoryFileSystem.GetFileStreamFactory());
Assert.Throws<ArgumentNullException>(() => rs.SaveAs(
null,
@@ -41,7 +41,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// If: I attempt to save with a null set of params
// Then: I should get a null argument exception
ResultSet rs = new ResultSet(
GetReader(null, false, Common.NoOpQuery), Common.Ordinal, Common.Ordinal,
Common.Ordinal, Common.Ordinal,
MemoryFileSystem.GetFileStreamFactory());
Assert.Throws<ArgumentNullException>(() => rs.SaveAs(
new SaveResultsRequestParams(),
@@ -54,7 +54,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// If: I attempt to save a result set that hasn't completed execution
// Then: I should get an invalid operation exception
ResultSet rs = new ResultSet(
GetReader(null, false, Common.NoOpQuery), Common.Ordinal, Common.Ordinal,
Common.Ordinal, Common.Ordinal,
MemoryFileSystem.GetFileStreamFactory());
Assert.Throws<InvalidOperationException>(() => rs.SaveAs(
new SaveResultsRequestParams(),
@@ -68,7 +68,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// Setup:
// ... Create a result set that has been executed
ResultSet rs = new ResultSet(
GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery),
Common.Ordinal, Common.Ordinal,
MemoryFileSystem.GetFileStreamFactory());
@@ -92,10 +91,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// ... Create a result set with dummy data and read to the end
ResultSet rs = new ResultSet(
GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery),
Common.Ordinal, Common.Ordinal,
resultFactory);
await rs.ReadResultToEnd(CancellationToken.None);
await rs.ReadResultToEnd(GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery), CancellationToken.None);
// ... Create a mock writer for writing the save as file
Mock<IFileStreamWriter> saveWriter = GetMockWriter();
@@ -125,10 +123,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.SaveResults
// ... Create a result set with dummy data and read to the end
ResultSet rs = new ResultSet(
GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery),
Common.Ordinal, Common.Ordinal,
resultFactory);
await rs.ReadResultToEnd(CancellationToken.None);
await rs.ReadResultToEnd(GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery), CancellationToken.None);
// ... Create a mock writer for writing the save as file
Mock<IFileStreamWriter> saveWriter = GetMockWriter();

View File

@@ -64,7 +64,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution
// ... I have a resultset that hasn't been executed and I request a valid result set from it
// Then:
// ... It should throw an exception for having not been read
ResultSet rs = new ResultSet(new TestDbDataReader(null), Common.Ordinal, Common.Ordinal, MemoryFileSystem.GetFileStreamFactory());
ResultSet rs = new ResultSet(Common.Ordinal, Common.Ordinal, MemoryFileSystem.GetFileStreamFactory());
await Assert.ThrowsAsync<InvalidOperationException>(() => rs.GetSubset(0, 1));
}