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

@@ -0,0 +1,30 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.EditData.Contracts
{
/// <summary>
/// Parameters for a request to commit pending edit operations
/// </summary>
public class EditCommitParams : SessionOperationParams
{
}
/// <summary>
/// Parameters to return upon successful completion of commiting pending edit operations
/// </summary>
public class EditCommitResult
{
}
public class EditCommitRequest
{
public static readonly
RequestType<EditCommitParams, EditCommitResult> Type =
RequestType<EditCommitParams, EditCommitResult>.Create("edit/commit");
}
}

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

View File

@@ -5,8 +5,10 @@
using System;
using System.Collections.Concurrent;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
@@ -18,22 +20,18 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// Represents an edit "session" bound to the results of a query, containing a cache of edits
/// that are pending. Provides logic for performing edit operations.
/// </summary>
public class Session
public class EditSession
{
#region Member Variables
private readonly ResultSet associatedResultSet;
private readonly IEditTableMetadata objectMetadata;
#endregion
/// <summary>
/// Constructs a new edit session bound to the result set and metadat object provided
/// </summary>
/// <param name="resultSet">The result set of the table to be edited</param>
/// <param name="objMetadata">Metadata provider for the table to be edited</param>
public Session(ResultSet resultSet, IEditTableMetadata objMetadata)
public EditSession(ResultSet resultSet, IEditTableMetadata objMetadata)
{
Validate.IsNotNull(nameof(resultSet), resultSet);
Validate.IsNotNull(nameof(objMetadata), objMetadata);
@@ -47,6 +45,12 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
#region Properties
/// <summary>
/// The task that is running to commit the changes to the db
/// Internal for unit test purposes.
/// </summary>
internal Task CommitTask { get; set; }
/// <summary>
/// The internal ID for the next row in the table. Internal for unit testing purposes only.
/// </summary>
@@ -55,7 +59,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <summary>
/// The cache of pending updates. Internal for unit test purposes only
/// </summary>
internal ConcurrentDictionary<long, RowEditBase> EditCache { get;}
internal ConcurrentDictionary<long, RowEditBase> EditCache { get; }
#endregion
@@ -109,6 +113,29 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
return newRowId;
}
/// <summary>
/// Commits the edits in the cache to the database and then to the associated result set of
/// this edit session. This is launched asynchronously.
/// </summary>
/// <param name="connection">The connection to use for executing the query</param>
/// <param name="successHandler">Callback to perform when the commit process has finished</param>
/// <param name="errorHandler">Callback to perform if the commit process has failed at some point</param>
public void CommitEdits(DbConnection connection, Func<Task> successHandler, Func<Exception, Task> errorHandler)
{
Validate.IsNotNull(nameof(connection), connection);
Validate.IsNotNull(nameof(successHandler), successHandler);
Validate.IsNotNull(nameof(errorHandler), errorHandler);
// Make sure that there isn't a commit task in progress
if (CommitTask != null && !CommitTask.IsCompleted)
{
throw new InvalidOperationException(SR.EditDataCommitInProgress);
}
// Start up the commit process
CommitTask = CommitEditsInternal(connection, successHandler, errorHandler);
}
/// <summary>
/// Creates a delete row update and adds it to the update cache
/// </summary>
@@ -149,6 +176,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
}
}
/// <summary>
/// Generates a single script file with all the pending edits scripted.
/// </summary>
/// <param name="outputPath">The path to output the script to</param>
/// <returns></returns>
public string ScriptEdits(string outputPath)
{
// Validate the output path
@@ -203,7 +235,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
// Attempt to get the row that is being edited, create a new update object if one
// doesn't exist
RowEditBase editRow = EditCache.GetOrAdd(rowId, new RowUpdate(rowId, associatedResultSet, objectMetadata));
// NOTE: This *must* be done as a lambda. RowUpdate creation requires that the row
// exist in the result set. We only want a new RowUpdate to be created if the edit
// doesn't already exist in the cache
RowEditBase editRow = EditCache.GetOrAdd(rowId, key => new RowUpdate(rowId, associatedResultSet, objectMetadata));
// Pass the call to the row update
return editRow.SetCell(columnId, newValue);
@@ -211,5 +246,36 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
#endregion
private async Task CommitEditsInternal(DbConnection connection, Func<Task> successHandler, Func<Exception, Task> errorHandler)
{
try
{
// @TODO: Add support for transactional commits
// Trust the RowEdit to sort itself appropriately
var editOperations = EditCache.Values.ToList();
editOperations.Sort();
foreach (var editOperation in editOperations)
{
// Get the command from the edit operation and execute it
using (DbCommand editCommand = editOperation.GetCommand(connection))
using (DbDataReader reader = await editCommand.ExecuteReaderAsync())
{
// Apply the changes of the command to the result set
await editOperation.ApplyChanges(reader);
}
// If we succeeded in applying the changes, then remove this from the cache
// @TODO: Prevent edit sessions from being modified while a commit is in progress
RowEditBase re;
EditCache.TryRemove(editOperation.RowId, out re);
}
await successHandler();
}
catch (Exception e)
{
await errorHandler(e);
}
}
}
}

View File

@@ -4,9 +4,9 @@
//
using System;
using System.Data.Common;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
@@ -26,7 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// </summary>
/// <param name="column">Column the cell will be under</param>
/// <param name="valueAsString">The string from the client to convert to an object</param>
public CellUpdate(DbColumn column, string valueAsString)
public CellUpdate(DbColumnWrapper column, string valueAsString)
{
Validate.IsNotNull(nameof(column), column);
Validate.IsNotNull(nameof(valueAsString), valueAsString);
@@ -89,7 +89,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <summary>
/// The column that the cell will be placed in
/// </summary>
public DbColumn Column { get; }
public DbColumnWrapper Column { get; }
/// <summary>
/// The object representation of the cell provided by the client

View File

@@ -5,10 +5,16 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
@@ -17,7 +23,9 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// </summary>
public sealed class RowCreate : RowEditBase
{
private const string InsertStatement = "INSERT INTO {0}({1}) VALUES ({2})";
private const string InsertStart = "INSERT INTO {0}({1})";
private const string InsertCompleteScript = "{0} VALUES ({1})";
private const string InsertCompleteOutput = "{0} OUTPUT {1} VALUES ({2})";
private readonly CellUpdate[] newCells;
@@ -34,42 +42,121 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
}
/// <summary>
/// Generates the INSERT INTO statement that will apply the row creation
/// Sort ID for a RowCreate object. Setting to 1 ensures that these are the first changes
/// to be committed
/// </summary>
/// <returns>INSERT INTO statement</returns>
public override string GetScript()
{
List<string> columnNames = new List<string>();
List<string> columnValues = new List<string>();
protected override int SortId => 1;
// Build the column list and value list
#region Public Methods
/// <summary>
/// Applies the changes to the associated result set after successfully executing the
/// change on the database
/// </summary>
/// <param name="dataReader">
/// Reader returned from the execution of the command to insert a new row. Should contain
/// a single row that represents the newly added row.
/// </param>
public override Task ApplyChanges(DbDataReader dataReader)
{
Validate.IsNotNull(nameof(dataReader), dataReader);
return AssociatedResultSet.AddRow(dataReader);
}
/// <summary>
/// Generates a command that can be executed to insert a new row -- and return the newly
/// inserted row.
/// </summary>
/// <param name="connection">The connection the command should be associated with</param>
/// <returns>Command to insert the new row</returns>
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
// Process all the columns. Add the column to the output columns, add updateable
// columns to the input parameters
List<string> outColumns = new List<string>();
List<string> inColumns = new List<string>();
DbCommand command = connection.CreateCommand();
for (int i = 0; i < AssociatedResultSet.Columns.Length; i++)
{
DbColumnWrapper column = AssociatedResultSet.Columns[i];
CellUpdate cell = newCells[i];
// If the column is not updatable, then skip it
// Add the column to the output
outColumns.Add($"inserted.{SqlScriptFormatter.FormatIdentifier(column.ColumnName)}");
// Skip columns that cannot be updated
if (!column.IsUpdatable)
{
continue;
}
// If the cell doesn't have a value, but is updatable, don't try to create the script
// If we're missing a cell, then we cannot continue
if (cell == null)
{
throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue);
}
// Add the column and the data to their respective lists
columnNames.Add(SqlScriptFormatter.FormatIdentifier(column.ColumnName));
columnValues.Add(SqlScriptFormatter.FormatValue(cell.Value, column));
// Create a parameter for the value and add it to the command
// Add the parameterization to the list and add it to the command
string paramName = $"@Value{RowId}{i}";
inColumns.Add(paramName);
SqlParameter param = new SqlParameter(paramName, cell.Column.SqlDbType)
{
Value = cell.Value
};
command.Parameters.Add(param);
}
string joinedInColumns = string.Join(", ", inColumns);
string joinedOutColumns = string.Join(", ", outColumns);
// Get the start clause
string start = GetTableClause();
// Put together the components of the statement
string joinedColumnNames = string.Join(", ", columnNames);
string joinedColumnValues = string.Join(", ", columnValues);
return string.Format(InsertStatement, AssociatedObjectMetadata.EscapedMultipartName, joinedColumnNames,
joinedColumnValues);
// Put the whole #! together
command.CommandText = string.Format(InsertCompleteOutput, start, joinedOutColumns, joinedInColumns);
command.CommandType = CommandType.Text;
return command;
}
/// <summary>
/// Generates the INSERT INTO statement that will apply the row creation
/// </summary>
/// <returns>INSERT INTO statement</returns>
public override string GetScript()
{
// Process all the cells, and generate the values
List<string> values = new List<string>();
for (int i = 0; i < AssociatedResultSet.Columns.Length; i++)
{
DbColumnWrapper column = AssociatedResultSet.Columns[i];
CellUpdate cell = newCells[i];
// Skip columns that cannot be updated
if (!column.IsUpdatable)
{
continue;
}
// If we're missing a cell, then we cannot continue
if (cell == null)
{
throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue);
}
// Format the value and add it to the list
values.Add(SqlScriptFormatter.FormatValue(cell.Value, column));
}
string joinedValues = string.Join(", ", values);
// Get the start clause
string start = GetTableClause();
// Put the whole #! together
return string.Format(InsertCompleteScript, start, joinedValues);
}
/// <summary>
@@ -99,5 +186,19 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
};
return eucr;
}
#endregion
private string GetTableClause()
{
// Get all the columns that will be provided
var inColumns = from c in AssociatedResultSet.Columns
where c.IsUpdatable
select SqlScriptFormatter.FormatIdentifier(c.ColumnName);
// Package it into a single INSERT statement starter
string inColumnsJoined = string.Join(", ", inColumns);
return string.Format(InsertStart, AssociatedObjectMetadata.EscapedMultipartName, inColumnsJoined);
}
}
}

View File

@@ -4,9 +4,12 @@
//
using System;
using System.Data.Common;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
@@ -29,15 +32,53 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
}
/// <summary>
/// Sort ID for a RowDelete object. Setting to 2 ensures that these are the LAST changes
/// to be committed
/// </summary>
protected override int SortId => 2;
/// <summary>
/// Applies the changes to the associated result set after successfully executing the
/// change on the database
/// </summary>
/// <param name="dataReader">
/// Reader returned from the execution of the command to insert a new row. Should NOT
/// contain any rows.
/// </param>
public override Task ApplyChanges(DbDataReader dataReader)
{
// Take the result set and remove the row from it
AssociatedResultSet.RemoveRow(RowId);
return Task.FromResult(0);
}
/// <summary>
/// Generates a command for deleting the selected row
/// </summary>
/// <returns></returns>
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
// Return a SqlCommand with formatted with the parameters from the where clause
WhereClause where = GetWhereClause(true);
string commandText = GetCommandText(where.CommandText);
DbCommand command = connection.CreateCommand();
command.CommandText = commandText;
command.Parameters.AddRange(where.Parameters.ToArray());
return command;
}
/// <summary>
/// Generates a DELETE statement to delete this row
/// </summary>
/// <returns>String of the DELETE statement</returns>
public override string GetScript()
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized ? DeleteMemoryOptimizedStatement : DeleteStatement;
return string.Format(CultureInfo.InvariantCulture, formatString,
AssociatedObjectMetadata.EscapedMultipartName, GetWhereClause(false).CommandText);
return GetCommandText(GetWhereClause(false).CommandText);
}
/// <summary>
@@ -51,5 +92,23 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
throw new InvalidOperationException(SR.EditDataDeleteSetCell);
}
protected override int CompareToSameType(RowEditBase rowEdit)
{
// We want to sort by row ID *IN REVERSE* to make sure we delete from the bottom first.
// If we delete from the top first, it will change IDs, making all subsequent deletes
// off by one or more!
return RowId.CompareTo(rowEdit.RowId) * -1;
}
private string GetCommandText(string whereText)
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized
? DeleteMemoryOptimizedStatement
: DeleteStatement;
return string.Format(CultureInfo.InvariantCulture, formatString,
AssociatedObjectMetadata.EscapedMultipartName, whereText);
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
@@ -18,9 +19,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <summary>
/// Base class for row edit operations. Provides basic information and helper functionality
/// that all RowEdit implementations can use. Defines functionality that must be implemented
/// in all child classes.
/// in all child classes. Implements a custom IComparable to enable sorting by type of the edit
/// and then by an overrideable
/// </summary>
public abstract class RowEditBase
public abstract class RowEditBase : IComparable<RowEditBase>
{
/// <summary>
/// Internal parameterless constructor, required for mocking
@@ -58,8 +60,31 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// </summary>
public IEditTableMetadata AssociatedObjectMetadata { get; }
/// <summary>
/// Sort ID for a row edit. Ensures that when a collection of RowEditBase objects are
/// sorted, the appropriate types are sorted to the top.
/// </summary>
protected abstract int SortId { get; }
#endregion
#region Abstract Methods
/// <summary>
/// Applies the changes to the associated result set
/// </summary>
/// <param name="dataReader">
/// Data reader from execution of the command to commit the change to the db
/// </param>
public abstract Task ApplyChanges(DbDataReader dataReader);
/// <summary>
/// Gets a command that will commit the change to the db
/// </summary>
/// <param name="connection">The connection to associate the command to</param>
/// <returns>Command to commit the change to the db</returns>
public abstract DbCommand GetCommand(DbConnection connection);
/// <summary>
/// Converts the row edit into a SQL statement
/// </summary>
@@ -74,6 +99,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <returns>The value of the cell after applying validation logic</returns>
public abstract EditUpdateCellResult SetCell(int columnId, string newValue);
#endregion
#region Protected Helper Methods
/// <summary>
/// Performs validation of column ID and if column can be updated.
/// </summary>
@@ -146,7 +175,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
// we execute multiple row edits at once.
string paramName = $"@Param{RowId}{col.Ordinal}";
cellDataClause = $"= {paramName}";
output.Parameters.Add(new SqlParameter(paramName, col.DbColumn.SqlDbType));
SqlParameter parameter = new SqlParameter(paramName, col.DbColumn.SqlDbType)
{
Value = cellData.RawObject
};
output.Parameters.Add(parameter);
}
else
{
@@ -163,6 +196,66 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
return output;
}
#endregion
#region IComparable Implementation
/// <summary>
/// Compares a row edit against another row edit. If they are the same type, then we
/// compare using an overrideable "same type" comparer. If they are different types, they
/// are sorted by their sort indexes.
///
/// In general, RowCreate and RowUpdates are sorted to the top. RowDeletes are sorted last.
/// If there are ties, default behavior is to sort by row ID ascending.
/// </summary>
/// <param name="other">The other row edit to compare against</param>
/// <returns>
/// A positive value if this edit should go first, a negative value if the other edit
/// should go first. 0 is returned if there is a tie.
/// </returns>
public int CompareTo(RowEditBase other)
{
// If the other is null, this one will come out on top
if (other == null)
{
return 1;
}
// If types are the same, use the type's tiebreaking sorter
if (GetType() == other.GetType())
{
return CompareToSameType(other);
}
// If the type's sort index is the same, use our tiebreaking sorter
// If they are different, use that as the comparison
int sortIdComparison = SortId.CompareTo(other.SortId);
return sortIdComparison == 0
? CompareByRowId(other)
: sortIdComparison;
}
/// <summary>
/// Default behavior for sorting if the two compared row edits are the same type. Sorts
/// by row ID ascending.
/// </summary>
/// <param name="rowEdit">The other row edit to compare against</param>
protected virtual int CompareToSameType(RowEditBase rowEdit)
{
return CompareByRowId(rowEdit);
}
/// <summary>
/// Compares two row edits by their row ID ascending.
/// </summary>
/// <param name="rowEdit">The other row edit to compare against</param>
private int CompareByRowId(RowEditBase rowEdit)
{
return RowId.CompareTo(rowEdit.RowId);
}
#endregion
/// <summary>
/// Represents a WHERE clause that can be used for identifying a row in a table.
/// </summary>

View File

@@ -5,12 +5,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
{
@@ -19,8 +23,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// </summary>
public sealed class RowUpdate : RowEditBase
{
private const string UpdateStatement = "UPDATE {0} SET {1} {2}";
private const string UpdateStatementMemoryOptimized = "UPDATE {0} WITH (SNAPSHOT) SET {1} {2}";
private const string UpdateScriptStart = @"UPDATE {0}";
private const string UpdateScriptStartMemOptimized = @"UPDATE {0} WITH (SNAPSHOT)";
private const string UpdateScript = @"{0} SET {1} {2}";
private const string UpdateScriptOutput = @"{0} SET {1} OUTPUT {2} {3}";
private readonly Dictionary<int, CellUpdate> cellUpdates;
private readonly IList<DbCellValue> associatedRow;
@@ -38,6 +45,73 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
associatedRow = associatedResultSet.GetRow(rowId);
}
/// <summary>
/// Sort order property. Sorts to same position as RowCreate
/// </summary>
protected override int SortId => 1;
#region Public Methods
/// <summary>
/// Applies the changes to the associated result set after successfully executing the
/// change on the database
/// </summary>
/// <param name="dataReader">
/// Reader returned from the execution of the command to update a row. Should contain
/// a single row that represents all the values of the row.
/// </param>
public override Task ApplyChanges(DbDataReader dataReader)
{
Validate.IsNotNull(nameof(dataReader), dataReader);
return AssociatedResultSet.UpdateRow(RowId, dataReader);
}
/// <summary>
/// Generates a command that can be executed to update a row -- and return the contents of
/// the updated row.
/// </summary>
/// <param name="connection">The connection the command should be associated with</param>
/// <returns>Command to update the row</returns>
public override DbCommand GetCommand(DbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
DbCommand command = connection.CreateCommand();
// Build the "SET" portion of the statement
List<string> setComponents = new List<string>();
foreach (var updateElement in cellUpdates)
{
string formattedColumnName = SqlScriptFormatter.FormatIdentifier(updateElement.Value.Column.ColumnName);
string paramName = $"@Value{RowId}{updateElement.Key}";
setComponents.Add($"{formattedColumnName} = {paramName}");
SqlParameter parameter = new SqlParameter(paramName, updateElement.Value.Column.SqlDbType)
{
Value = updateElement.Value.Value
};
command.Parameters.Add(parameter);
}
string setComponentsJoined = string.Join(", ", setComponents);
// Build the "OUTPUT" portion of the statement
var outColumns = from c in AssociatedResultSet.Columns
let formatted = SqlScriptFormatter.FormatIdentifier(c.ColumnName)
select $"inserted.{formatted}";
string outColumnsJoined = string.Join(", ", outColumns);
// Get the where clause
WhereClause where = GetWhereClause(true);
command.Parameters.AddRange(where.Parameters.ToArray());
// Get the start of the statement
string statementStart = GetStatementStart();
// Put the whole #! together
command.CommandText = string.Format(UpdateScriptOutput, statementStart, setComponentsJoined,
outColumnsJoined, where.CommandText);
command.CommandType = CommandType.Text;
return command;
}
/// <summary>
/// Constructs an update statement to change the associated row.
/// </summary>
@@ -45,7 +119,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
public override string GetScript()
{
// Build the "SET" portion of the statement
IEnumerable<string> setComponents = cellUpdates.Values.Select(cellUpdate =>
var setComponents = cellUpdates.Values.Select(cellUpdate =>
{
string formattedColumnName = SqlScriptFormatter.FormatIdentifier(cellUpdate.Column.ColumnName);
string formattedValue = SqlScriptFormatter.FormatValue(cellUpdate.Value, cellUpdate.Column);
@@ -56,10 +130,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
// Get the where clause
string whereClause = GetWhereClause(false).CommandText;
// Put it all together
string formatString = AssociatedObjectMetadata.IsMemoryOptimized ? UpdateStatementMemoryOptimized : UpdateStatement;
return string.Format(CultureInfo.InvariantCulture, formatString,
AssociatedObjectMetadata.EscapedMultipartName, setClause, whereClause);
// Get the start of the statement
string statementStart = GetStatementStart();
// Put the whole #! together
return string.Format(UpdateScript, statementStart, setClause, whereClause);
}
/// <summary>
@@ -106,5 +181,16 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
IsRevert = false // If we're in this branch, it is not a revert
};
}
#endregion
private string GetStatementStart()
{
string formatString = AssociatedObjectMetadata.IsMemoryOptimized
? UpdateScriptStartMemOptimized
: UpdateScriptStart;
return string.Format(formatString, AssociatedObjectMetadata.EscapedMultipartName);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
@@ -27,5 +105,19 @@
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader><resheader name="version"><value>1.3</value></resheader><resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader><resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader><data name="TestLocalizationConstant"><value>ES_LOCALIZATION</value></data>
</root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="TestLocalizationConstant" xml:space="preserve">
<value>ES_LOCALIZATION</value>
</data>
</root>

View File

@@ -120,423 +120,431 @@
<data name="ConnectionServiceConnectErrorNullParams" xml:space="preserve">
<value>Connection parameters cannot be null</value>
<comment></comment>
</data>
</data>
<data name="ConnectionServiceListDbErrorNullOwnerUri" xml:space="preserve">
<value>OwnerUri cannot be null or empty</value>
<comment></comment>
</data>
</data>
<data name="ConnectionServiceListDbErrorNotConnected" xml:space="preserve">
<value>SpecifiedUri '{0}' does not have existing connection</value>
<comment>.
Parameters: 0 - uri (string) </comment>
</data>
</data>
<data name="ConnectionServiceDbErrorDefaultNotConnected" xml:space="preserve">
<value>Specified URI '{0}' does not have a default connection</value>
<comment>.
Parameters: 0 - uri (string) </comment>
</data>
</data>
<data name="ConnectionServiceConnStringInvalidAuthType" xml:space="preserve">
<value>Invalid value '{0}' for AuthenticationType. Valid values are 'Integrated' and 'SqlLogin'.</value>
<comment>.
Parameters: 0 - authType (string) </comment>
</data>
</data>
<data name="ConnectionServiceConnStringInvalidIntent" xml:space="preserve">
<value>Invalid value '{0}' for ApplicationIntent. Valid values are 'ReadWrite' and 'ReadOnly'.</value>
<comment>.
Parameters: 0 - intent (string) </comment>
</data>
</data>
<data name="ConnectionServiceConnectionCanceled" xml:space="preserve">
<value>Connection canceled</value>
<comment></comment>
</data>
</data>
<data name="ConnectionParamsValidateNullOwnerUri" xml:space="preserve">
<value>OwnerUri cannot be null or empty</value>
<comment></comment>
</data>
</data>
<data name="ConnectionParamsValidateNullConnection" xml:space="preserve">
<value>Connection details object cannot be null</value>
<comment></comment>
</data>
</data>
<data name="ConnectionParamsValidateNullServerName" xml:space="preserve">
<value>ServerName cannot be null or empty</value>
<comment></comment>
</data>
</data>
<data name="ConnectionParamsValidateNullSqlAuth" xml:space="preserve">
<value>{0} cannot be null or empty when using SqlLogin authentication</value>
<comment>.
Parameters: 0 - component (string) </comment>
</data>
</data>
<data name="ErrorUnexpectedCodeObjectType" xml:space="preserve">
<value>Cannot convert SqlCodeObject Type {0} to Type {1}</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceCancelAlreadyCompleted" xml:space="preserve">
<value>The query has already completed, it cannot be cancelled</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceCancelDisposeFailed" xml:space="preserve">
<value>Query successfully cancelled, failed to dispose query. Owner URI not found.</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceQueryCancelled" xml:space="preserve">
<value>Query was canceled by user</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSubsetBatchNotCompleted" xml:space="preserve">
<value>The batch has not completed, yet</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSubsetBatchOutOfRange" xml:space="preserve">
<value>Batch index cannot be less than 0 or greater than the number of batches</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSubsetResultSetOutOfRange" xml:space="preserve">
<value>Result set index cannot be less than 0 or greater than the number of result sets</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceDataReaderByteCountInvalid" xml:space="preserve">
<value>Maximum number of bytes to return must be greater than zero</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceDataReaderCharCountInvalid" xml:space="preserve">
<value>Maximum number of chars to return must be greater than zero</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceDataReaderXmlCountInvalid" xml:space="preserve">
<value>Maximum number of XML bytes to return must be greater than zero</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceFileWrapperWriteOnly" xml:space="preserve">
<value>Access method cannot be write-only</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceFileWrapperNotInitialized" xml:space="preserve">
<value>FileStreamWrapper must be initialized before performing operations</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceFileWrapperReadOnly" xml:space="preserve">
<value>This FileStreamWrapper cannot be used for writing</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceAffectedOneRow" xml:space="preserve">
<value>(1 row affected)</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceAffectedRows" xml:space="preserve">
<value>({0} rows affected)</value>
<comment>.
Parameters: 0 - rows (long) </comment>
</data>
</data>
<data name="QueryServiceCompletedSuccessfully" xml:space="preserve">
<value>Commands completed successfully.</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceErrorFormat" xml:space="preserve">
<value>Msg {0}, Level {1}, State {2}, Line {3}{4}{5}</value>
<comment>.
Parameters: 0 - msg (int), 1 - lvl (int), 2 - state (int), 3 - line (int), 4 - newLine (string), 5 - message (string) </comment>
</data>
</data>
<data name="QueryServiceQueryFailed" xml:space="preserve">
<value>Query failed: {0}</value>
<comment>.
Parameters: 0 - message (string) </comment>
</data>
</data>
<data name="QueryServiceColumnNull" xml:space="preserve">
<value>(No column name)</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceRequestsNoQuery" xml:space="preserve">
<value>The requested query does not exist</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceQueryInvalidOwnerUri" xml:space="preserve">
<value>This editor is not connected to a database</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceQueryInProgress" xml:space="preserve">
<value>A query is already in progress for this editor session. Please cancel this query or wait for its completion.</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceMessageSenderNotSql" xml:space="preserve">
<value>Sender for OnInfoMessage event must be a SqlConnection</value>
<comment></comment>
</data>
<data name="QueryServiceResultSetReaderNull" xml:space="preserve">
<value>Reader cannot be null</value>
</data>
<data name="QueryServiceResultSetAddNoRows" xml:space="preserve">
<value>Cannot add row to result buffer, data reader does not contain rows</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSaveAsResultSetNotComplete" xml:space="preserve">
<value>Result cannot be saved until query execution has completed</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSaveAsMiscStartingError" xml:space="preserve">
<value>Internal error occurred while starting save task</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSaveAsInProgress" xml:space="preserve">
<value>A save request to the same path is in progress</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceSaveAsFail" xml:space="preserve">
<value>Failed to save {0}: {1}</value>
<comment>.
Parameters: 0 - fileName (string), 1 - message (string) </comment>
</data>
</data>
<data name="QueryServiceResultSetNotRead" xml:space="preserve">
<value>Cannot read subset unless the results have been read from the server</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceResultSetStartRowOutOfRange" xml:space="preserve">
<value>Start row cannot be less than 0 or greater than the number of rows in the result set</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceResultSetRowCountOutOfRange" xml:space="preserve">
<value>Row count must be a positive integer</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceResultSetNoColumnSchema" xml:space="preserve">
<value>Could not retrieve column schema for result set</value>
<comment></comment>
</data>
</data>
<data name="QueryServiceExecutionPlanNotFound" xml:space="preserve">
<value>Could not retrieve an execution plan from the result set </value>
<comment></comment>
</data>
</data>
<data name="PeekDefinitionAzureError" xml:space="preserve">
<value>This feature is currently not supported on Azure SQL DB and Data Warehouse: {0}</value>
<comment>.
Parameters: 0 - errorMessage (string) </comment>
</data>
</data>
<data name="PeekDefinitionError" xml:space="preserve">
<value>An unexpected error occurred during Peek Definition execution: {0}</value>
<comment>.
Parameters: 0 - errorMessage (string) </comment>
</data>
</data>
<data name="PeekDefinitionNoResultsError" xml:space="preserve">
<value>No results were found.</value>
<comment></comment>
</data>
</data>
<data name="PeekDefinitionDatabaseError" xml:space="preserve">
<value>No database object was retrieved.</value>
<comment></comment>
</data>
</data>
<data name="PeekDefinitionNotConnectedError" xml:space="preserve">
<value>Please connect to a server.</value>
<comment></comment>
</data>
</data>
<data name="PeekDefinitionTimedoutError" xml:space="preserve">
<value>Operation timed out.</value>
<comment></comment>
</data>
</data>
<data name="PeekDefinitionTypeNotSupportedError" xml:space="preserve">
<value>This object type is currently not supported by this feature.</value>
<comment></comment>
</data>
</data>
<data name="ErrorEmptyStringReplacement" xml:space="preserve">
<value>Replacement of an empty string by an empty string.</value>
<comment></comment>
</data>
</data>
<data name="WorkspaceServicePositionLineOutOfRange" xml:space="preserve">
<value>Position is outside of file line range</value>
<comment></comment>
</data>
</data>
<data name="WorkspaceServicePositionColumnOutOfRange" xml:space="preserve">
<value>Position is outside of column range for line {0}</value>
<comment>.
Parameters: 0 - line (int) </comment>
</data>
</data>
<data name="WorkspaceServiceBufferPositionOutOfOrder" xml:space="preserve">
<value>Start position ({0}, {1}) must come before or be equal to the end position ({2}, {3})</value>
<comment>.
Parameters: 0 - sLine (int), 1 - sCol (int), 2 - eLine (int), 3 - eCol (int) </comment>
</data>
</data>
<data name="EditDataSessionNotFound" xml:space="preserve">
<value>Edit session does not exist.</value>
<comment></comment>
</data>
</data>
<data name="EditDataUnsupportedObjectType" xml:space="preserve">
<value>Database object {0} cannot be used for editing.</value>
<comment>.
Parameters: 0 - typeName (string) </comment>
</data>
</data>
<data name="EditDataQueryNotCompleted" xml:space="preserve">
<value>Query has not completed execution</value>
<comment></comment>
</data>
</data>
<data name="EditDataQueryImproperResultSets" xml:space="preserve">
<value>Query did not generate exactly one result set</value>
<comment></comment>
</data>
</data>
<data name="EditDataFailedAddRow" xml:space="preserve">
<value>Failed to add new row to update cache</value>
<comment></comment>
</data>
</data>
<data name="EditDataRowOutOfRange" xml:space="preserve">
<value>Given row ID is outside the range of rows in the edit cache</value>
<comment></comment>
</data>
</data>
<data name="EditDataUpdatePending" xml:space="preserve">
<value>An update is already pending for this row and must be reverted first</value>
<comment></comment>
</data>
</data>
<data name="EditDataUpdateNotPending" xml:space="preserve">
<value>Given row ID does not have pending updated</value>
<comment></comment>
</data>
</data>
<data name="EditDataObjectMetadataNotFound" xml:space="preserve">
<value>Table or view metadata could not be found</value>
<comment></comment>
</data>
</data>
<data name="EditDataInvalidFormatBinary" xml:space="preserve">
<value>Invalid format for binary column</value>
<comment></comment>
</data>
</data>
<data name="EditDataInvalidFormatBoolean" xml:space="preserve">
<value>Allowed values for boolean columns are 0, 1, "true", or "false"</value>
<comment></comment>
</data>
</data>
<data name="EditDataCreateScriptMissingValue" xml:space="preserve">
<value>A required cell value is missing</value>
<comment></comment>
</data>
</data>
<data name="EditDataDeleteSetCell" xml:space="preserve">
<value>A delete is pending for this row, a cell update cannot be applied.</value>
<comment></comment>
</data>
</data>
<data name="EditDataColumnIdOutOfRange" xml:space="preserve">
<value>Column ID must be in the range of columns for the query</value>
<comment></comment>
</data>
</data>
<data name="EditDataColumnCannotBeEdited" xml:space="preserve">
<value>Column cannot be edited</value>
<comment></comment>
</data>
</data>
<data name="EditDataColumnNoKeyColumns" xml:space="preserve">
<value>No key columns were found</value>
<comment></comment>
</data>
</data>
<data name="EditDataScriptFilePathNull" xml:space="preserve">
<value>An output filename must be provided</value>
<comment></comment>
</data>
</data>
<data name="EditDataCommitInProgress" xml:space="preserve">
<value>A commit task is in progress. Please wait for completion.</value>
<comment></comment>
</data>
<data name="EE_BatchSqlMessageNoProcedureInfo" xml:space="preserve">
<value>Msg {0}, Level {1}, State {2}, Line {3}</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchSqlMessageWithProcedureInfo" xml:space="preserve">
<value>Msg {0}, Level {1}, State {2}, Procedure {3}, Line {4}</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchSqlMessageNoLineInfo" xml:space="preserve">
<value>Msg {0}, Level {1}, State {2}</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchError_Exception" xml:space="preserve">
<value>An error occurred while the batch was being processed. The error message is: {0}</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchExecutionInfo_RowsAffected" xml:space="preserve">
<value>({0} row(s) affected)</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionNotYetCompleteError" xml:space="preserve">
<value>The previous execution is not yet complete.</value>
<comment></comment>
</data>
</data>
<data name="EE_ScriptError_Error" xml:space="preserve">
<value>A scripting error occurred.</value>
<comment></comment>
</data>
</data>
<data name="EE_ScriptError_ParsingSyntax" xml:space="preserve">
<value>Incorrect syntax was encountered while {0} was being parsed.</value>
<comment></comment>
</data>
</data>
<data name="EE_ScriptError_FatalError" xml:space="preserve">
<value>A fatal error occurred.</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionInfo_FinalizingLoop" xml:space="preserve">
<value>Execution completed {0} times...</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionInfo_QueryCancelledbyUser" xml:space="preserve">
<value>You cancelled the query.</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchExecutionError_Halting" xml:space="preserve">
<value>An error occurred while the batch was being executed.</value>
<comment></comment>
</data>
</data>
<data name="EE_BatchExecutionError_Ignoring" xml:space="preserve">
<value>An error occurred while the batch was being executed, but the error has been ignored.</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionInfo_InitilizingLoop" xml:space="preserve">
<value>Starting execution loop of {0} times...</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionError_CommandNotSupported" xml:space="preserve">
<value>Command {0} is not supported.</value>
<comment></comment>
</data>
</data>
<data name="EE_ExecutionError_VariableNotFound" xml:space="preserve">
<value>The variable {0} could not be found.</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionEngineError" xml:space="preserve">
<value>SQL Execution error: {0}</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionError" xml:space="preserve">
<value>Batch parser wrapper execution: {0} found... at line {1}: {2} Description: {3}</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionEngineBatchMessage" xml:space="preserve">
<value>Batch parser wrapper execution engine batch message received: Message: {0} Detailed message: {1}</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionEngineBatchResultSetProcessing" xml:space="preserve">
<value>Batch parser wrapper execution engine batch ResultSet processing: DataReader.FieldCount: {0} DataReader.RecordsAffected: {1}</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionEngineBatchResultSetFinished" xml:space="preserve">
<value>Batch parser wrapper execution engine batch ResultSet finished.</value>
<comment></comment>
</data>
</data>
<data name="BatchParserWrapperExecutionEngineBatchCancelling" xml:space="preserve">
<value>Canceling batch parser wrapper batch execution.</value>
<comment></comment>
</data>
</data>
<data name="EE_ScriptError_Warning" xml:space="preserve">
<value>Scripting warning.</value>
<comment></comment>
</data>
</data>
<data name="TroubleshootingAssistanceMessage" xml:space="preserve">
<value>For more information about this error, see the troubleshooting topics in the product documentation.</value>
<comment></comment>
</data>
</data>
<data name="BatchParser_CircularReference" xml:space="preserve">
<value>File '{0}' recursively included.</value>
<comment></comment>
</data>
</data>
<data name="BatchParser_CommentNotTerminated" xml:space="preserve">
<value>Missing end comment mark '*/'.</value>
<comment></comment>
</data>
</data>
<data name="BatchParser_StringNotTerminated" xml:space="preserve">
<value>Unclosed quotation mark after the character string.</value>
<comment></comment>
</data>
</data>
<data name="BatchParser_IncorrectSyntax" xml:space="preserve">
<value>Incorrect syntax was encountered while parsing '{0}'.</value>
<comment></comment>
</data>
</data>
<data name="BatchParser_VariableNotDefined" xml:space="preserve">
<value>Variable {0} is not defined.</value>
<comment></comment>
</data>
</data>
<data name="TestLocalizationConstant" xml:space="preserve">
<value>EN_LOCALIZATION</value>
<comment></comment>
</data>
</root>
</data>
<data name="SqlScriptFormatterDecimalMissingPrecision" xml:space="preserve">
<value>Decimal column is missing numeric precision or numeric scale</value>
<comment></comment>
</data>
</root>

View File

@@ -37,7 +37,6 @@ ConnectionServiceConnStringInvalidIntent(string intent) = Invalid value '{0}' fo
ConnectionServiceConnectionCanceled = Connection canceled
######
### Connection Params Validation Errors
ConnectionParamsValidateNullOwnerUri = OwnerUri cannot be null or empty
@@ -110,7 +109,7 @@ QueryServiceQueryInProgress = A query is already in progress for this editor ses
QueryServiceMessageSenderNotSql = Sender for OnInfoMessage event must be a SqlConnection
QueryServiceResultSetReaderNull = Reader cannot be null
QueryServiceResultSetAddNoRows = Cannot add row to result buffer, data reader does not contain rows
### Save As Requests
@@ -199,6 +198,8 @@ EditDataColumnNoKeyColumns = No key columns were found
EditDataScriptFilePathNull = An output filename must be provided
EditDataCommitInProgress = A commit task is in progress. Please wait for completion.
############################################################################
# DacFx Resources
@@ -264,3 +265,8 @@ BatchParser_VariableNotDefined = Variable {0} is not defined.
# Workspace Service
TestLocalizationConstant = EN_LOCALIZATION
############################################################################
# Utilities
SqlScriptFormatterDecimalMissingPrecision = Decimal column is missing numeric precision or numeric scale

View File

@@ -169,11 +169,6 @@
<target state="new">Sender for OnInfoMessage event must be a SqlConnection</target>
<note></note>
</trans-unit>
<trans-unit id="QueryServiceResultSetReaderNull">
<source>Reader cannot be null</source>
<target state="new">Reader cannot be null</target>
<note></note>
</trans-unit>
<trans-unit id="QueryServiceSaveAsResultSetNotComplete">
<source>Result cannot be saved until query execution has completed</source>
<target state="new">Result cannot be saved until query execution has completed</target>
@@ -526,6 +521,21 @@
<note>.
Parameters: 0 - uri (string) </note>
</trans-unit>
<trans-unit id="EditDataCommitInProgress">
<source>A commit task is in progress. Please wait for completion.</source>
<target state="new">A commit task is in progress. Please wait for completion.</target>
<note></note>
</trans-unit>
<trans-unit id="SqlScriptFormatterDecimalMissingPrecision">
<source>Decimal column is missing numeric precision or numeric scale</source>
<target state="new">Decimal column is missing numeric precision or numeric scale</target>
<note></note>
</trans-unit>
<trans-unit id="QueryServiceResultSetAddNoRows">
<source>Cannot add row to result buffer, data reader does not contain rows</source>
<target state="new">Cannot add row to result buffer, data reader does not contain rows</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -286,7 +286,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
// This resultset has results (ie, SELECT/etc queries)
ResultSet resultSet = new ResultSet(reader, resultSetOrdinal, Id, outputFileFactory);
ResultSet resultSet = new ResultSet(resultSetOrdinal, Id, outputFileFactory);
resultSet.ResultCompletion += ResultSetCompletion;
// Add the result set to the results of the query
@@ -297,7 +297,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
// Read until we hit the end of the result set
await resultSet.ReadResultToEnd(cancellationToken).ConfigureAwait(false);
await resultSet.ReadResultToEnd(reader, cancellationToken).ConfigureAwait(false);
} while (await reader.NextResultAsync(cancellationToken));

View File

@@ -85,7 +85,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
IsIdentity = column.IsIdentity;
IsKey = column.IsKey;
IsLong = column.IsLong;
IsReadOnly = column.IsLong;
IsReadOnly = column.IsReadOnly;
IsUnique = column.IsUnique;
NumericPrecision = column.NumericPrecision;
NumericScale = column.NumericScale;

View File

@@ -17,7 +17,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
int WriteRow(StorageDataReader dataReader);
void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns);
void Seek(long offset);
void FlushBuffer();
}
}

View File

@@ -75,6 +75,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <param name="columns">The list of columns to output</param>
public abstract void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns);
/// <summary>
/// Not implemented, do not use.
/// </summary>
[Obsolete]
public void Seek(long offset)
{
throw new InvalidOperationException("SaveAs writers are meant to be written once contiguously.");
}
/// <summary>
/// Flushes the file stream buffer
/// </summary>

View File

@@ -212,6 +212,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
throw new InvalidOperationException("This type of writer is meant to write values from a DbDataReader only.");
}
/// <summary>
/// Seeks to a given offset in the file, relative to the beginning of the file
/// </summary>
public void Seek(long offset)
{
fileStream.Seek(offset, SeekOrigin.Begin);
}
/// <summary>
/// Flushes the internal buffer to the file stream
/// </summary>

View File

@@ -33,11 +33,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
#region Member Variables
/// <summary>
/// The reader to use for this resultset
/// </summary>
private readonly StorageDataReader dataReader;
/// <summary>
/// For IDisposable pattern, whether or not object has been disposed
/// </summary>
@@ -69,30 +64,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
private readonly string outputFileName;
/// <summary>
/// Row count to use in special scenarios where we want to override the number of rows.
/// </summary>
private long? rowCountOverride;
/// <summary>
/// The special action which applied to this result set
/// </summary>
private readonly SpecialAction specialAction;
/// <summary>
/// Total number of bytes written to the file. Used to jump to end of the file for append
/// scenarios. Internal for unit test validation.
/// </summary>
internal long totalBytesWritten;
#endregion
/// <summary>
/// Creates a new result set and initializes its state
/// </summary>
/// <param name="reader">The reader from executing a query</param>
/// <param name="ordinal">The ID of the resultset, the ordinal of the result within the batch</param>
/// <param name="batchOrdinal">The ID of the batch, the ordinal of the batch within the query</param>
/// <param name="factory">Factory for creating a reader/writer</param>
public ResultSet(DbDataReader reader, int ordinal, int batchOrdinal, IFileStreamFactory factory)
public ResultSet(int ordinal, int batchOrdinal, IFileStreamFactory factory)
{
// Sanity check to make sure we got a reader
Validate.IsNotNull(nameof(reader), SR.QueryServiceResultSetReaderNull);
dataReader = new StorageDataReader(reader);
Id = ordinal;
BatchId = batchOrdinal;
// Initialize the storage
totalBytesWritten = 0;
outputFileName = factory.CreateFile();
fileOffsets = new LongList<long>();
specialAction = new SpecialAction();
@@ -103,7 +105,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
SaveTasks = new ConcurrentDictionary<string, Task>();
}
#region Properties
#region Eventing
/// <summary>
/// Asynchronous handler for when saving query results succeeds
@@ -129,6 +131,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
public event ResultSetAsyncEventHandler ResultCompletion;
#endregion
#region Properties
/// <summary>
/// Whether the resultSet is in the process of being disposed
/// </summary>
@@ -153,7 +159,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// <summary>
/// The number of rows for this result set
/// </summary>
public long RowCount { get; private set; }
public long RowCount => rowCountOverride ?? fileOffsets.Count;
/// <summary>
/// All save tasks currently saving this ResultSet
@@ -173,8 +179,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
Id = Id,
BatchId = BatchId,
RowCount = RowCount,
SpecialAction = ProcessSpecialAction()
SpecialAction = hasBeenRead ? ProcessSpecialAction() : null
};
}
}
@@ -183,6 +188,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
#region Public Methods
/// <summary>
/// Returns a specific row from the result set.
/// </summary>
/// <remarks>
/// Creates a new file reader for a single reader. This method should only be used for one
/// off requests, not for requesting a large subset of the results.
/// </remarks>
/// <param name="rowId">The internal ID of the row to read</param>
/// <returns>The requested row</returns>
public IList<DbCellValue> GetRow(long rowId)
{
// Sanity check to make sure that results have been read beforehand
@@ -313,14 +327,20 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// <summary>
/// Reads from the reader until there are no more results to read
/// </summary>
/// <param name="dbDataReader">The data reader for getting results from the db</param>
/// <param name="cancellationToken">Cancellation token for cancelling the query</param>
public async Task ReadResultToEnd(CancellationToken cancellationToken)
public async Task ReadResultToEnd(DbDataReader dbDataReader, CancellationToken cancellationToken)
{
// Sanity check to make sure we got a reader
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
try
{
// Mark that result has been read
hasBeenRead = true;
StorageDataReader dataReader = new StorageDataReader(dbDataReader);
// Open a writer for the file
var fileWriter = fileStreamFactory.GetWriter(outputFileName);
using (fileWriter)
@@ -331,13 +351,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
throw new InvalidOperationException(SR.QueryServiceResultSetNoColumnSchema);
}
Columns = dataReader.Columns;
long currentFileOffset = 0;
while (await dataReader.ReadAsync(cancellationToken))
{
RowCount++;
fileOffsets.Add(currentFileOffset);
currentFileOffset += fileWriter.WriteRow(dataReader);
fileOffsets.Add(totalBytesWritten);
totalBytesWritten += fileWriter.WriteRow(dataReader);
}
}
// Check if resultset is 'for xml/json'. If it is, set isJson/isXml value in column metadata
@@ -353,6 +370,50 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
}
/// <summary>
/// Removes a row from the result set cache
/// </summary>
/// <param name="internalId">Internal ID of the row</param>
public void RemoveRow(long internalId)
{
// Make sure that the results have been read
if (!hasBeenRead)
{
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
}
// Simply remove the row from the list of row offsets
fileOffsets.RemoveAt(internalId);
}
/// <summary>
/// Adds a new row to the result set by reading the row from the provided db data reader
/// </summary>
/// <param name="dbDataReader">The result of a command to insert a new row should be UNREAD</param>
public async Task AddRow(DbDataReader dbDataReader)
{
// Write the new row to the end of the file
long newOffset = await AppendRowToBuffer(dbDataReader);
// Add the row to file offset list
fileOffsets.Add(newOffset);
}
/// <summary>
/// Updates the values in a row with the
/// </summary>
/// <param name="rowId"></param>
/// <param name="dbDataReader"></param>
/// <returns></returns>
public async Task UpdateRow(long rowId, DbDataReader dbDataReader)
{
// Write the updated row to the end of the file
long newOffset = await AppendRowToBuffer(dbDataReader);
// Update the file offset of the row in question
fileOffsets[rowId] = newOffset;
}
/// <summary>
/// Saves the contents of this result set to a file using the IFileStreamFactory provided
/// </summary>
@@ -508,13 +569,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
Columns[0].IsXml = true;
isSingleColumnXmlJsonResultSet = true;
RowCount = 1;
rowCountOverride = 1;
}
else if (Columns[0].ColumnName.Equals(NameOfForJsonColumn, StringComparison.Ordinal))
{
Columns[0].IsJson = true;
isSingleColumnXmlJsonResultSet = true;
RowCount = 1;
rowCountOverride = 1;
}
}
}
@@ -526,7 +587,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
// Check if this result set is a showplan
if (dataReader.Columns.Length == 1 && string.Compare(dataReader.Columns[0].ColumnName, YukonXmlShowPlanColumn, StringComparison.OrdinalIgnoreCase) == 0)
if (Columns.Length == 1 && string.Compare(Columns[0].ColumnName, YukonXmlShowPlanColumn, StringComparison.OrdinalIgnoreCase) == 0)
{
specialAction.ExpectYukonXMLShowPlan = true;
}
@@ -534,6 +595,36 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
return specialAction;
}
/// <summary>
/// Adds a single row to the end of the buffer file. INTENDED FOR SINGLE ROW INSERTION ONLY.
/// </summary>
/// <param name="dbDataReader">An UNREAD db data reader</param>
/// <returns>The offset into the file where the row was inserted</returns>
private async Task<long> AppendRowToBuffer(DbDataReader dbDataReader)
{
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
if (!hasBeenRead)
{
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
}
if (!dbDataReader.HasRows)
{
throw new InvalidOperationException(SR.QueryServiceResultSetAddNoRows);
}
StorageDataReader dataReader = new StorageDataReader(dbDataReader);
using (IFileStreamWriter writer = fileStreamFactory.GetWriter(outputFileName))
{
// Write the row to the end of the file
long currentFileOffset = totalBytesWritten;
writer.Seek(currentFileOffset);
await dataReader.ReadAsync(CancellationToken.None);
totalBytesWritten += writer.WriteRow(dataReader);
return currentFileOffset;
}
}
#endregion
}
}

View File

@@ -185,8 +185,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility
// Make sure we have numeric precision and numeric scale
if (!column.NumericPrecision.HasValue || !column.NumericScale.HasValue)
{
// @TODO Move to constants
throw new InvalidOperationException("Decimal column is missing numeric precision or numeric scale");
throw new InvalidOperationException(SR.SqlScriptFormatterDecimalMissingPrecision);
}
// Convert the value to a decimal, then convert that to a string