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

@@ -54,8 +54,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
private readonly QueryExecutionService queryExecutionService;
private readonly Lazy<ConcurrentDictionary<string, Session>> editSessions = new Lazy<ConcurrentDictionary<string, Session>>(
() => new ConcurrentDictionary<string, Session>());
private readonly Lazy<ConcurrentDictionary<string, EditSession>> editSessions = new Lazy<ConcurrentDictionary<string, EditSession>>(
() => new ConcurrentDictionary<string, EditSession>());
#endregion
@@ -64,7 +64,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <summary>
/// Dictionary mapping OwnerURIs to active sessions
/// </summary>
internal ConcurrentDictionary<string, Session> ActiveSessions => editSessions.Value;
internal ConcurrentDictionary<string, EditSession> ActiveSessions => editSessions.Value;
#endregion
@@ -86,14 +86,14 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
#region Request Handlers
internal async Task HandleSessionRequest<TResult>(SessionOperationParams sessionParams,
RequestContext<TResult> requestContext, Func<Session, TResult> sessionOperation)
RequestContext<TResult> requestContext, Func<EditSession, TResult> sessionOperation)
{
try
{
Session session = GetActiveSessionOrThrow(sessionParams.OwnerUri);
EditSession editSession = GetActiveSessionOrThrow(sessionParams.OwnerUri);
// Get the result from execution of the session operation
TResult result = sessionOperation(session);
// Get the result from execution of the editSession operation
TResult result = sessionOperation(editSession);
await requestContext.SendResult(result);
}
catch (Exception e)
@@ -135,9 +135,9 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
// Sanity check the owner URI
Validate.IsNotNullOrWhitespaceString(nameof(disposeParams.OwnerUri), disposeParams.OwnerUri);
// Attempt to remove the session
Session session;
if (!ActiveSessions.TryRemove(disposeParams.OwnerUri, out session))
// Attempt to remove the editSession
EditSession editSession;
if (!ActiveSessions.TryRemove(disposeParams.OwnerUri, out editSession))
{
await requestContext.SendError(SR.EditDataSessionNotFound);
return;
@@ -219,6 +219,31 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
session => session.UpdateCell(updateParams.RowId, updateParams.ColumnId, updateParams.NewValue));
}
internal async Task HandleCommitRequest(EditCommitParams commitParams,
RequestContext<EditCommitResult> requestContext)
{
// Setup a callback for if the edits have been successfully written to the db
Func<Task> successHandler = () => requestContext.SendResult(new EditCommitResult());
// Setup a callback for if the edits failed to be written to db
Func<Exception, Task> failureHandler = e => requestContext.SendError(e.Message);
try
{
// Get the editSession
EditSession editSession = GetActiveSessionOrThrow(commitParams.OwnerUri);
// Get a connection for doing the committing
DbConnection conn = await connectionService.GetOrOpenConnection(commitParams.OwnerUri,
ConnectionType.Edit);
editSession.CommitEdits(conn, successHandler, failureHandler);
}
catch (Exception e)
{
await failureHandler(e);
}
}
#endregion
#region Private Helpers
@@ -229,19 +254,19 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <exception cref="Exception">If the edit session doesn't exist</exception>
/// <param name="ownerUri">Owner URI for the edit session</param>
/// <returns>The edit session that corresponds to the owner URI</returns>
private Session GetActiveSessionOrThrow(string ownerUri)
private EditSession GetActiveSessionOrThrow(string ownerUri)
{
// Sanity check the owner URI is provided
Validate.IsNotNullOrWhitespaceString(nameof(ownerUri), ownerUri);
// Attempt to get the session, throw if unable
Session session;
if (!ActiveSessions.TryGetValue(ownerUri, out session))
// Attempt to get the editSession, throw if unable
EditSession editSession;
if (!ActiveSessions.TryGetValue(ownerUri, out editSession))
{
throw new Exception(SR.EditDataSessionNotFound);
}
return session;
return editSession;
}
private async Task QueryCompleteCallback(Query query, EditInitializeParams initParams,
@@ -254,19 +279,19 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
try
{
// Validate the query for a session
ResultSet resultSet = Session.ValidateQueryForSession(query);
// Validate the query for a editSession
ResultSet resultSet = EditSession.ValidateQueryForSession(query);
// Get a connection we'll use for SMO metadata lookup (and committing, later on)
DbConnection conn = await connectionService.GetOrOpenConnection(initParams.OwnerUri, ConnectionType.Edit);
var metadata = metadataFactory.GetObjectMetadata(conn, resultSet.Columns,
initParams.ObjectName, initParams.ObjectType);
// Create the session and add it to the sessions list
Session session = new Session(resultSet, metadata);
if (!ActiveSessions.TryAdd(initParams.OwnerUri, session))
// Create the editSession and add it to the sessions list
EditSession editSession = new EditSession(resultSet, metadata);
if (!ActiveSessions.TryAdd(initParams.OwnerUri, editSession))
{
throw new InvalidOperationException("Failed to create edit session, session already exists.");
throw new InvalidOperationException("Failed to create edit editSession, editSession already exists.");
}
readyParams.Success = true;
}