mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-17 01:25:40 -05:00
Edit Data Service (#241)
This is a very large change. I'll try to outline what's going on.
1. This adds the **EditDataService** which manages editing **Sessions**.
1. Each session has a **ResultSet** (from the QueryExecutionService) which has the rows of the table and basic metadata about the columns
2. Each session also has an **IEditTableMetadata** implementation which is derived from SMO metadata which provides more in-depth and trustworthy data about the table than SqlClient alone can.
3. Each session holds a list of **RowEditBase** abstract class implementations
1. **RowUpdate** - Update cells in a row (generates `UPDATE` statement)
2. **RowDelete** - Delete an entire row (generates `DELETE` statement)
3. **RowCreate** - Add a new row (generates `INSERT INTO` statement)
4. Row edits have a collection of **CellUpdates** that hold updates for individual cells (except for RowDelete)
1. Cell updates are generated from text
5. RowEditBase offers some baseline functionality
1. Generation of `WHERE` clauses (which can be parameterized)
2. Validation of whether a column can be updated
2. New API Actions
1. edit/initialize - Queries for the contents of a table/view, builds SMO metadata, sets up a session
2. edit/createRow - Adds a new RowCreate to the Session
3. edit/deleteRow - Adds a new RowDelete to the Session
4. edit/updateCell - Adds a CellUpdate to a RowCreate or RowUpdate in the Session
5. edit/revertRow - Removes a RowCreate, RowDelete, or RowUpdate from the Session
6. edit/script - Generates a script for the changes in the Session and stores to disk
7. edit/dispose - Removes a Session and releases the query
3. Smaller updates (unit test mock improvements, tweaks to query execution service)
**There are more updates planned -- this is just to get eyeballs on the main body of code**
* Initial stubs for edit data service
* Stubbing out update management code
* Adding rudimentary dispose request
* More stubbing out of update row code
* Adding complete edit command contracts, stubbing out request handlers
* Adding basic implementation of get script
* More in progress work to implement base of row edits
* More in progress work to implement base of row edits
* Adding string => object conversion logic and various cleanup
* Adding a formatter for using values in scripts
* Splitting IMessageSender into IEventSender and IRequestSender
* Adding inter-service method for executing queries
* Adding inter-service method for disposing of a query
* Changing edit contract to include the object to edit
* Fully fleshing out edit session initialization
* Generation of delete scripts is working
* Adding scripter for update statements
* Adding scripting functionality for INSERT statements
* Insert, Update, and Delete all working with SMO metadata
* Polishing for SqlScriptFormatter
* Unit tests and reworked byte[] conversion
* Replacing the awful and inflexible Dictionary<string, string>[][] with a much better test data set class
* Fixing syntax error in generated UPDATE statements
* Adding unit tests for RowCreate
* Adding tests for the row edit base class
* Adding row delete tests
* Adding RowUpdate tests, validation for number of key columns
* Adding tests for the unit class
* Adding get script tests for the session
* Service integration tests, except initialization tests
* Service integration tests, except initialization tests
* Adding messages to sr.strings
* Adding messages to sr.strings
* Fixing broken unit tests
* Adding factory pattern for SMO metadata provider
* Copyright and other comments
* Addressing first round of comments
* Refactoring EditDataService to have a single method for handling
session-dependent operations
* Refactoring Edit Data contracts to inherit from a Session and Row
operation params base class
* Copyright additions
* Small tweak to strings
* Updated unit tests to test the refactors
* More revisions as per pull request comments
This commit is contained in:
@@ -15,6 +15,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public string DisplayValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the cell is NULL
|
||||
/// </summary>
|
||||
public bool IsNull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw object for the cell, for use internally
|
||||
/// </summary>
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlTypes;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
@@ -16,10 +18,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public class DbColumnWrapper : DbColumn
|
||||
{
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// All types supported by the server, stored as a hash set to provide O(1) lookup
|
||||
/// </summary>
|
||||
internal static readonly HashSet<string> AllServerDataTypes = new HashSet<string>
|
||||
private static readonly HashSet<string> AllServerDataTypes = new HashSet<string>
|
||||
{
|
||||
"bigint",
|
||||
"binary",
|
||||
@@ -52,6 +56,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
"datetime2"
|
||||
};
|
||||
|
||||
private const string SqlXmlDataTypeName = "xml";
|
||||
private const string DbTypeXmlDataTypeName = "DBTYPE_XML";
|
||||
private const string UnknownTypeName = "unknown";
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for a DbColumnWrapper
|
||||
/// </summary>
|
||||
@@ -81,21 +91,49 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
NumericScale = column.NumericScale;
|
||||
UdtAssemblyQualifiedName = column.UdtAssemblyQualifiedName;
|
||||
DataType = column.DataType;
|
||||
DataTypeName = column.DataTypeName;
|
||||
DataTypeName = column.DataTypeName.ToLowerInvariant();
|
||||
|
||||
// Determine the SqlDbType
|
||||
SqlDbType type;
|
||||
if (Enum.TryParse(DataTypeName, true, out type))
|
||||
{
|
||||
SqlDbType = type;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (DataTypeName)
|
||||
{
|
||||
case "numeric":
|
||||
SqlDbType = SqlDbType.Decimal;
|
||||
break;
|
||||
case "sql_variant":
|
||||
SqlDbType = SqlDbType.Variant;
|
||||
break;
|
||||
case "timestamp":
|
||||
SqlDbType = SqlDbType.VarBinary;
|
||||
break;
|
||||
case "sysname":
|
||||
SqlDbType = SqlDbType.NVarChar;
|
||||
break;
|
||||
default:
|
||||
SqlDbType = DataTypeName.EndsWith(".sys.hierarchyid") ? SqlDbType.NVarChar : SqlDbType.Udt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want the display name for the column to always exist
|
||||
ColumnName = string.IsNullOrEmpty(column.ColumnName)
|
||||
? SR.QueryServiceColumnNull
|
||||
: column.ColumnName;
|
||||
|
||||
switch (column.DataTypeName)
|
||||
switch (DataTypeName)
|
||||
{
|
||||
case "varchar":
|
||||
case "nvarchar":
|
||||
IsChars = true;
|
||||
|
||||
Debug.Assert(column.ColumnSize.HasValue);
|
||||
if (column.ColumnSize.Value == int.MaxValue)
|
||||
Debug.Assert(ColumnSize.HasValue);
|
||||
if (ColumnSize.Value == int.MaxValue)
|
||||
{
|
||||
//For Yukon, special case nvarchar(max) with column name == "Microsoft SQL Server 2005 XML Showplan" -
|
||||
//assume it is an XML showplan.
|
||||
@@ -131,8 +169,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
case "rowversion":
|
||||
IsBytes = true;
|
||||
|
||||
Debug.Assert(column.ColumnSize.HasValue);
|
||||
if (column.ColumnSize.Value == int.MaxValue)
|
||||
Debug.Assert(ColumnSize.HasValue);
|
||||
if (ColumnSize.Value == int.MaxValue)
|
||||
{
|
||||
IsLong = true;
|
||||
}
|
||||
@@ -141,7 +179,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
IsSqlVariant = true;
|
||||
break;
|
||||
default:
|
||||
if (!AllServerDataTypes.Contains(column.DataTypeName))
|
||||
if (!AllServerDataTypes.Contains(DataTypeName))
|
||||
{
|
||||
// treat all UDT's as long/bytes data types to prevent the CLR from attempting
|
||||
// to load the UDT assembly into our process to call ToString() on the object.
|
||||
@@ -216,6 +254,43 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public bool IsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SqlDbType of the column, for use in a SqlParameter
|
||||
/// </summary>
|
||||
public SqlDbType SqlDbType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is an XML Reader type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logic taken from SSDT determination of whether a column is a SQL XML type. It may not
|
||||
/// be possible to have XML readers from .NET Core SqlClient.
|
||||
/// </remarks>
|
||||
public bool IsSqlXmlType => DataTypeName.Equals(SqlXmlDataTypeName, StringComparison.OrdinalIgnoreCase) ||
|
||||
DataTypeName.Equals(DbTypeXmlDataTypeName, StringComparison.OrdinalIgnoreCase) ||
|
||||
DataType == typeof(System.Xml.XmlReader);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is an unknown type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logic taken from SSDT determination of unknown columns. It may not even be possible to
|
||||
/// have "unknown" column types with the .NET Core SqlClient.
|
||||
/// </remarks>
|
||||
public bool IsUnknownType => DataType == typeof(object) &&
|
||||
DataTypeName.Equals(UnknownTypeName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column can be updated, based on whether it's an auto increment
|
||||
/// column, is an XML reader column, and if it's read only.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logic taken from SSDT determination of updatable columns
|
||||
/// </remarks>
|
||||
public bool IsUpdatable => !IsAutoIncrement.HasTrue() &&
|
||||
!IsReadOnly.HasTrue() &&
|
||||
!IsSqlXmlType;
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
result.RawObject = null;
|
||||
result.DisplayValue = null;
|
||||
result.IsNull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -207,6 +208,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
T resultObject = convertFunc(length.ValueLength);
|
||||
result.RawObject = resultObject;
|
||||
result.DisplayValue = toStringFunc == null ? result.RawObject.ToString() : toStringFunc(resultObject);
|
||||
result.IsNull = false;
|
||||
}
|
||||
|
||||
return new FileStreamReadResult(result, length.TotalLength);
|
||||
|
||||
@@ -151,11 +151,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
RequestContext<ExecuteRequestResult> requestContext)
|
||||
{
|
||||
// Setup actions to perform upon successful start and on failure to start
|
||||
Func<Task> queryCreationAction = () => requestContext.SendResult(new ExecuteRequestResult());
|
||||
Func<string, Task> queryFailAction = requestContext.SendError;
|
||||
Func<Query, Task<bool>> queryCreateSuccessAction = async q => {
|
||||
await requestContext.SendResult(new ExecuteRequestResult());
|
||||
return true;
|
||||
};
|
||||
Func<string, Task> queryCreateFailureAction = requestContext.SendError;
|
||||
|
||||
// Use the internal handler to launch the query
|
||||
return InterServiceExecuteQuery(executeParams, requestContext, queryCreationAction, queryFailAction);
|
||||
return InterServiceExecuteQuery(executeParams, requestContext, queryCreateSuccessAction, queryCreateFailureAction, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -328,26 +331,59 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// Query execution meant to be called from another service. Utilizes callbacks to allow
|
||||
/// custom actions to be taken upon creation of query and failure to create query.
|
||||
/// </summary>
|
||||
/// <param name="executeParams">Params for creating the new query</param>
|
||||
/// <param name="eventSender">Object that can send events for query execution progress</param>
|
||||
/// <param name="queryCreatedAction">
|
||||
/// Action to perform when query has been successfully created, right before execution of
|
||||
/// the query
|
||||
/// <param name="executeParams">Parameters for execution</param>
|
||||
/// <param name="queryEventSender">Event sender that will send progressive events during execution of the query</param>
|
||||
/// <param name="queryCreateSuccessFunc">
|
||||
/// Callback for when query has been created successfully. If result is <c>true</c>, query
|
||||
/// will be executed asynchronously. If result is <c>false</c>, query will be disposed. May
|
||||
/// be <c>null</c>
|
||||
/// </param>
|
||||
/// <param name="failureAction">Action to perform if query was not successfully created</param>
|
||||
public async Task InterServiceExecuteQuery(ExecuteRequestParamsBase executeParams, IEventSender eventSender,
|
||||
Func<Task> queryCreatedAction, Func<string, Task> failureAction)
|
||||
/// <param name="queryCreateFailFunc">
|
||||
/// Callback for when query failed to be created successfully. Error message is provided.
|
||||
/// May be <c>null</c>.
|
||||
/// </param>
|
||||
/// <param name="querySuccessFunc">
|
||||
/// Callback to call when query has completed execution successfully. May be <c>null</c>.
|
||||
/// </param>
|
||||
/// <param name="queryFailureFunc">
|
||||
/// Callback to call when query has completed execution with errors. May be <c>null</c>.
|
||||
/// </param>
|
||||
public async Task InterServiceExecuteQuery(ExecuteRequestParamsBase executeParams,
|
||||
IEventSender queryEventSender,
|
||||
Func<Query, Task<bool>> queryCreateSuccessFunc,
|
||||
Func<string, Task> queryCreateFailFunc,
|
||||
Query.QueryAsyncEventHandler querySuccessFunc,
|
||||
Query.QueryAsyncEventHandler queryFailureFunc)
|
||||
{
|
||||
Validate.IsNotNull(nameof(executeParams), executeParams);
|
||||
Validate.IsNotNull(nameof(eventSender), eventSender);
|
||||
Validate.IsNotNull(nameof(queryCreatedAction), queryCreatedAction);
|
||||
Validate.IsNotNull(nameof(failureAction), failureAction);
|
||||
|
||||
// Get a new active query
|
||||
Query newQuery = await CreateAndActivateNewQuery(executeParams, queryCreatedAction, failureAction);
|
||||
Validate.IsNotNull(nameof(queryEventSender), queryEventSender);
|
||||
|
||||
Query newQuery;
|
||||
try
|
||||
{
|
||||
// Get a new active query
|
||||
newQuery = CreateQuery(executeParams);
|
||||
if (queryCreateSuccessFunc != null && !await queryCreateSuccessFunc(newQuery))
|
||||
{
|
||||
// The callback doesn't want us to continue, for some reason
|
||||
// It's ok if we leave the query behind in the active query list, the next call
|
||||
// to execute will replace it.
|
||||
newQuery.Dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Call the failure callback if it was provided
|
||||
if (queryCreateFailFunc != null)
|
||||
{
|
||||
await queryCreateFailFunc(e.Message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the query asynchronously
|
||||
ExecuteAndCompleteQuery(executeParams.OwnerUri, eventSender, newQuery);
|
||||
ExecuteAndCompleteQuery(executeParams.OwnerUri, newQuery, queryEventSender, querySuccessFunc, queryFailureFunc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -390,63 +426,47 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
private async Task<Query> CreateAndActivateNewQuery(ExecuteRequestParamsBase executeParams, Func<Task> successAction, Func<string, Task> failureAction)
|
||||
private Query CreateQuery(ExecuteRequestParamsBase executeParams)
|
||||
{
|
||||
try
|
||||
// Attempt to get the connection for the editor
|
||||
ConnectionInfo connectionInfo;
|
||||
if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo))
|
||||
{
|
||||
// Attempt to get the connection for the editor
|
||||
ConnectionInfo connectionInfo;
|
||||
if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo))
|
||||
{
|
||||
await failureAction(SR.QueryServiceQueryInvalidOwnerUri);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Attempt to clean out any old query on the owner URI
|
||||
Query oldQuery;
|
||||
if (ActiveQueries.TryGetValue(executeParams.OwnerUri, out oldQuery) && oldQuery.HasExecuted)
|
||||
{
|
||||
oldQuery.Dispose();
|
||||
ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery);
|
||||
}
|
||||
|
||||
// Retrieve the current settings for executing the query with
|
||||
QueryExecutionSettings querySettings = Settings.QueryExecutionSettings;
|
||||
|
||||
// Apply execution parameter settings
|
||||
querySettings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions;
|
||||
|
||||
// If we can't add the query now, it's assumed the query is in progress
|
||||
Query newQuery = new Query(GetSqlText(executeParams), connectionInfo, querySettings, BufferFileFactory);
|
||||
if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery))
|
||||
{
|
||||
await failureAction(SR.QueryServiceQueryInProgress);
|
||||
newQuery.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Successfully created query
|
||||
await successAction();
|
||||
|
||||
return newQuery;
|
||||
throw new ArgumentOutOfRangeException(nameof(executeParams.OwnerUri), SR.QueryServiceQueryInvalidOwnerUri);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
// Attempt to clean out any old query on the owner URI
|
||||
Query oldQuery;
|
||||
if (ActiveQueries.TryGetValue(executeParams.OwnerUri, out oldQuery) && oldQuery.HasExecuted)
|
||||
{
|
||||
await failureAction(e.Message);
|
||||
return null;
|
||||
oldQuery.Dispose();
|
||||
ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery);
|
||||
}
|
||||
|
||||
// Retrieve the current settings for executing the query with
|
||||
QueryExecutionSettings settings = Settings.QueryExecutionSettings;
|
||||
|
||||
// Apply execution parameter settings
|
||||
settings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions;
|
||||
|
||||
// If we can't add the query now, it's assumed the query is in progress
|
||||
Query newQuery = new Query(GetSqlText(executeParams), connectionInfo, settings, BufferFileFactory);
|
||||
if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery))
|
||||
{
|
||||
newQuery.Dispose();
|
||||
throw new InvalidOperationException(SR.QueryServiceQueryInProgress);
|
||||
}
|
||||
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
private static void ExecuteAndCompleteQuery(string ownerUri, IEventSender eventSender, Query query)
|
||||
private static void ExecuteAndCompleteQuery(string ownerUri, Query query,
|
||||
IEventSender eventSender,
|
||||
Query.QueryAsyncEventHandler querySuccessCallback,
|
||||
Query.QueryAsyncEventHandler queryFailureCallback)
|
||||
{
|
||||
// Skip processing if the query is null
|
||||
if (query == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the query completion/failure callbacks
|
||||
Query.QueryAsyncEventHandler callback = async q =>
|
||||
// Setup the callback to send the complete event
|
||||
Query.QueryAsyncEventHandler completeCallback = async q =>
|
||||
{
|
||||
// Send back the results
|
||||
QueryCompleteParams eventParams = new QueryCompleteParams
|
||||
@@ -457,9 +477,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
await eventSender.SendEvent(QueryCompleteEvent.Type, eventParams);
|
||||
};
|
||||
query.QueryCompleted += completeCallback;
|
||||
query.QueryFailed += completeCallback;
|
||||
|
||||
query.QueryCompleted += callback;
|
||||
query.QueryFailed += callback;
|
||||
// Add the callbacks that were provided by the caller
|
||||
// If they're null, that's no problem
|
||||
query.QueryCompleted += querySuccessCallback;
|
||||
query.QueryFailed += queryFailureCallback;
|
||||
|
||||
// Setup the batch callbacks
|
||||
Batch.BatchAsyncEventHandler batchStartCallback = async b =>
|
||||
|
||||
@@ -183,6 +183,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public IList<DbCellValue> GetRow(long rowId)
|
||||
{
|
||||
// Sanity check to make sure that results have been read beforehand
|
||||
if (!hasBeenRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
|
||||
// Sanity check to make sure that the row exists
|
||||
if (rowId >= RowCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rowId), SR.QueryServiceResultSetStartRowOutOfRange);
|
||||
}
|
||||
|
||||
using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName))
|
||||
{
|
||||
return fileStreamReader.ReadRow(fileOffsets[rowId], Columns);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a subset of the rows from the result set
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user