mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user