Remove SELECT * from edit/initialize Query (#288)

* Major refactor of EditDataMetadata providers

* EditMetadataFactory generates "basic" EditTableMetadata objects based entirely on SMO metadata
* SmoEditTableMetadata no longer depends on SMO, making it unecessary to mock it
* Renamed SmoEditTableMetadata to EditTableMetadata
* EditTableMetadata can be extended with DbColumnWrappers

* Moving logic for extending a EditColumnMetadata into that class

* I *think* this will work for async execution of initialize tasks

* Fixing unit tests for new Edit(Table|Column)Metadata classes

* Async stuff that works! And passes unit tests

* Adding unit tests
Adding .idea to gitignore

* Adding message to the EditSessionReadyEvent

* Fixes from dev merge

* Fixing unit tests that Rider didn't catch as failing
May have been a bit heavy-handed with the async/await stuff

* Couple changes as per PR comments
This commit is contained in:
Benjamin Russell
2017-03-22 10:53:24 -07:00
committed by GitHub
parent d7ecfb1a87
commit 16b3874f28
30 changed files with 2325 additions and 1417 deletions

View File

@@ -14,6 +14,12 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.Contracts
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Message to explain why a session failed. Should only be set when <see cref="Success"/>
/// is <c>false</c>.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Whether or not the session is ready
/// </summary>

View File

@@ -0,0 +1,113 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Data;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
/// <summary>
/// Small class that stores information needed by the edit data service to properly process
/// edits into scripts.
/// </summary>
public class EditColumnMetadata
{
/// <summary>
/// Constructs a simple edit column metadata provider
/// </summary>
public EditColumnMetadata()
{
HasExtendedProperties = false;
}
#region Basic Properties (properties provided by SMO)
/// <summary>
/// If set, this is a string representation of the default value. If set to null, then the
/// column does not have a default value.
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// Escaped identifier for the name of the column
/// </summary>
public string EscapedName { get; set; }
/// <summary>
/// Whether or not the column is computed
/// </summary>
public bool IsComputed { get; set; }
/// <summary>
/// Whether or not the column is deterministically computed
/// </summary>
public bool IsDeterministic { get; set; }
/// <summary>
/// Whether or not the column is an identity column
/// </summary>
public bool IsIdentity { get; set; }
/// <summary>
/// The ordinal ID of the column
/// </summary>
public int Ordinal { get; set; }
#endregion
#region Extended Properties (properties provided by SqlClient)
public DbColumnWrapper DbColumn { get; private set; }
/// <summary>
/// Whether or not the column has extended properties
/// </summary>
public bool HasExtendedProperties { get; private set; }
/// <summary>
/// Whether or not the column is calculated on the server side. This could be a computed
/// column or a identity column.
/// </summary>
public bool? IsCalculated { get; private set; }
/// <summary>
/// Whether or not the column is used in a key to uniquely identify a row
/// </summary>
public bool? IsKey { get; private set; }
/// <summary>
/// Whether or not the column can be trusted for uniqueness
/// </summary>
public bool? IsTrustworthyForUniqueness { get; private set; }
#endregion
/// <summary>
/// Extracts extended column properties from the database columns from SQL Client
/// </summary>
/// <param name="dbColumn">The column information provided by SQL Client</param>
public void Extend(DbColumnWrapper dbColumn)
{
Validate.IsNotNull(nameof(dbColumn), dbColumn);
DbColumn = dbColumn;
// A column is trustworthy for uniqueness if it can be updated or it has an identity
// property. If both of these are false (eg, timestamp) we can't trust it to uniquely
// identify a row in the table
IsTrustworthyForUniqueness = dbColumn.IsUpdatable || dbColumn.IsIdentity.HasTrue();
// A key column is determined by whether it is a key
IsKey = dbColumn.IsKey;
// A column is calculated if it is identity, computed, or otherwise not updatable
IsCalculated = IsIdentity || IsComputed || !dbColumn.IsUpdatable;
// Mark the column as extended
HasExtendedProperties = true;
}
}
}

View File

@@ -1,53 +0,0 @@
//
// 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.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
/// <summary>
/// Small class that stores information needed by the edit data service to properly process
/// edits into scripts.
/// </summary>
public class EditColumnWrapper
{
/// <summary>
/// The DB column
/// </summary>
public DbColumnWrapper DbColumn { get; set; }
/// <summary>
/// If set, this is a string representation of the default value. If set to null, then the
/// column does not have a default value.
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// Escaped identifier for the name of the column
/// </summary>
public string EscapedName { get; set; }
/// <summary>
/// Whether or not the column is calculated on the server side. This could be a computed
/// column or a identity column.
/// </summary>
public bool IsCalculated { get; set; }
/// <summary>
/// Whether or not the column is used in a key to uniquely identify a row
/// </summary>
public bool IsKey { get; set; }
/// <summary>
/// Whether or not the column can be trusted for uniqueness
/// </summary>
public bool IsTrustworthyForUniqueness { get; set; }
/// <summary>
/// The ordinal ID of the column
/// </summary>
public int Ordinal { get; set; }
}
}

View File

@@ -13,7 +13,6 @@ using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType;
@@ -57,10 +56,6 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
private readonly Lazy<ConcurrentDictionary<string, EditSession>> editSessions = new Lazy<ConcurrentDictionary<string, EditSession>>(
() => new ConcurrentDictionary<string, EditSession>());
private readonly Lazy<ConcurrentDictionary<string, TaskCompletionSource<bool>>> initializeWaitHandles =
new Lazy<ConcurrentDictionary<string, TaskCompletionSource<bool>>>(
() => new ConcurrentDictionary<string, TaskCompletionSource<bool>>());
#endregion
#region Properties
@@ -70,12 +65,6 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// </summary>
internal ConcurrentDictionary<string, EditSession> ActiveSessions => editSessions.Value;
/// <summary>
/// Dictionary mapping OwnerURIs to wait handlers for initialize tasks. Pretty much only
/// provided for unit test scenarios.
/// </summary>
internal ConcurrentDictionary<string, TaskCompletionSource<bool>> InitializeWaitHandles => initializeWaitHandles.Value;
#endregion
/// <summary>
@@ -159,63 +148,32 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
internal async Task HandleInitializeRequest(EditInitializeParams initParams,
RequestContext<EditInitializeResult> requestContext)
{
Func<Exception, Task> executionFailureHandler = (e) => SendSessionReadyEvent(requestContext, initParams.OwnerUri, false, e.Message);
Func<Task> executionSuccessHandler = () => SendSessionReadyEvent(requestContext, initParams.OwnerUri, true, null);
EditSession.Connector connector = () => connectionService.GetOrOpenConnection(initParams.OwnerUri, ConnectionType.Edit);
EditSession.QueryRunner queryRunner = q => SessionInitializeQueryRunner(initParams.OwnerUri, requestContext, q);
try
{
{
// Make sure we have info to process this request
Validate.IsNotNullOrWhitespaceString(nameof(initParams.OwnerUri), initParams.OwnerUri);
Validate.IsNotNullOrWhitespaceString(nameof(initParams.ObjectName), initParams.ObjectName);
Validate.IsNotNullOrWhitespaceString(nameof(initParams.ObjectType), initParams.ObjectType);
// Try to add a new wait handler to the
if (!InitializeWaitHandles.TryAdd(initParams.OwnerUri, new TaskCompletionSource<bool>()))
// Create a session and add it to the session list
EditSession session = new EditSession(metadataFactory, initParams.ObjectName, initParams.ObjectType);
if (!ActiveSessions.TryAdd(initParams.OwnerUri, session))
{
throw new InvalidOperationException(SR.EditDataInitializeInProgress);
throw new InvalidOperationException(SR.EditDataSessionAlreadyExists);
}
// Setup a callback for when the query has successfully created
Func<Query, Task<bool>> queryCreateSuccessCallback = async query =>
{
await requestContext.SendResult(new EditInitializeResult());
return true;
};
// Setup a callback for when the query failed to be created
Func<string, Task> queryCreateFailureCallback = async message =>
{
await requestContext.SendError(message);
CompleteInitializeWaitHandler(initParams.OwnerUri, false);
};
// Setup a callback for when the query completes execution successfully
Query.QueryAsyncEventHandler queryCompleteSuccessCallback =
q => QueryCompleteCallback(q, initParams, requestContext);
// Setup a callback for when the query completes execution with failure
Query.QueryAsyncEventHandler queryCompleteFailureCallback = async query =>
{
EditSessionReadyParams readyParams = new EditSessionReadyParams
{
OwnerUri = initParams.OwnerUri,
Success = false
};
await requestContext.SendEvent(EditSessionReadyEvent.Type, readyParams);
CompleteInitializeWaitHandler(initParams.OwnerUri, false);
};
// Put together a query for the results and execute it
ExecuteStringParams executeParams = new ExecuteStringParams
{
Query = $"SELECT * FROM {SqlScriptFormatter.FormatMultipartIdentifier(initParams.ObjectName)}",
OwnerUri = initParams.OwnerUri
};
await queryExecutionService.InterServiceExecuteQuery(executeParams, requestContext,
queryCreateSuccessCallback, queryCreateFailureCallback,
queryCompleteSuccessCallback, queryCompleteFailureCallback);
// Initialize the session
session.Initialize(connector, queryRunner, executionSuccessHandler, executionFailureHandler);
}
catch (Exception e)
{
await requestContext.SendError(e.Message);
CompleteInitializeWaitHandler(initParams.OwnerUri, false);
}
}
@@ -313,52 +271,64 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
return editSession;
}
private async Task QueryCompleteCallback(Query query, EditInitializeParams initParams,
IEventSender requestContext)
private async Task<EditSession.EditSessionQueryExecutionState> SessionInitializeQueryRunner(string ownerUri,
IEventSender eventSender, string query)
{
EditSessionReadyParams readyParams = new EditSessionReadyParams
// Open a task completion source, effectively creating a synchronous block
TaskCompletionSource<EditSession.EditSessionQueryExecutionState> taskCompletion =
new TaskCompletionSource<EditSession.EditSessionQueryExecutionState>();
// Setup callback for successful query creation
// NOTE: We do not want to set the task completion source, since we will continue executing the query after
Func<Query, Task<bool>> queryCreateSuccessCallback = q => Task.FromResult(true);
// Setup callback for failed query creation
Func<string, Task> queryCreateFailureCallback = m =>
{
OwnerUri = initParams.OwnerUri
taskCompletion.SetResult(new EditSession.EditSessionQueryExecutionState(null, m));
return Task.FromResult(0);
};
try
// Setup callback for successful query execution
Query.QueryAsyncEventHandler queryCompleteSuccessCallback = q =>
{
// Validate the query for a editSession
ResultSet resultSet = EditSession.ValidateQueryForSession(query);
taskCompletion.SetResult(new EditSession.EditSessionQueryExecutionState(q));
return Task.FromResult(0);
};
// 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 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 editSession, editSession already exists.");
}
readyParams.Success = true;
}
catch (Exception)
// Setup callback for failed query execution
Query.QueryAsyncEventHandler queryCompleteFailureCallback = q =>
{
// Request that the query be disposed
await queryExecutionService.InterServiceDisposeQuery(initParams.OwnerUri, null, null);
readyParams.Success = false;
}
taskCompletion.SetResult(new EditSession.EditSessionQueryExecutionState(null));
return Task.FromResult(0);
};
// Send the edit session ready notification
await requestContext.SendEvent(EditSessionReadyEvent.Type, readyParams);
CompleteInitializeWaitHandler(initParams.OwnerUri, true);
// Execute the query
ExecuteStringParams executeParams = new ExecuteStringParams
{
Query = query,
OwnerUri = ownerUri
};
await queryExecutionService.InterServiceExecuteQuery(executeParams, eventSender,
queryCreateSuccessCallback, queryCreateFailureCallback,
queryCompleteSuccessCallback, queryCompleteFailureCallback);
// Wait for the completion source to complete, this will wait until the query has
// completed and sent all its events.
return await taskCompletion.Task;
}
private void CompleteInitializeWaitHandler(string ownerUri, bool result)
private static Task SendSessionReadyEvent(IEventSender eventSender, string ownerUri, bool success,
string message)
{
// If there isn't a wait handler, just ignore it
TaskCompletionSource<bool> initializeWaiter;
if (ownerUri != null && InitializeWaitHandles.TryRemove(ownerUri, out initializeWaiter))
var sessionReadyParams = new EditSessionReadyParams
{
initializeWaiter.SetResult(result);
}
OwnerUri = ownerUri,
Message = message,
Success = success
};
return eventSender.SendEvent(EditSessionReadyEvent.Type, sessionReadyParams);
}
#endregion

View File

@@ -25,28 +25,37 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
public class EditSession
{
private readonly ResultSet associatedResultSet;
private readonly IEditTableMetadata objectMetadata;
private ResultSet associatedResultSet;
private readonly IEditMetadataFactory metadataFactory;
private EditTableMetadata objectMetadata;
private readonly string objectName;
private readonly string objectType;
/// <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 EditSession(ResultSet resultSet, IEditTableMetadata objMetadata)
/// <param name="metaFactory">Factory for creating metadata</param>
/// <param name="objName">The name of the object to edit</param>
/// <param name="objType">The type of the object to edit</param>
public EditSession(IEditMetadataFactory metaFactory, string objName, string objType)
{
Validate.IsNotNull(nameof(resultSet), resultSet);
Validate.IsNotNull(nameof(objMetadata), objMetadata);
Validate.IsNotNull(nameof(metaFactory), metaFactory);
Validate.IsNotNullOrWhitespaceString(nameof(objName), objName);
Validate.IsNotNullOrWhitespaceString(nameof(objType), objType);
// Setup the internal state
associatedResultSet = resultSet;
objectMetadata = objMetadata;
NextRowId = associatedResultSet.RowCount;
EditCache = new ConcurrentDictionary<long, RowEditBase>();
metadataFactory = metaFactory;
objectName = objName;
objectType = objType;
}
#region Properties
public delegate Task<DbConnection> Connector();
public delegate Task<EditSessionQueryExecutionState> QueryRunner(string query);
/// <summary>
/// The task that is running to commit the changes to the db
/// Internal for unit test purposes.
@@ -61,12 +70,43 @@ 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; private set; }
/// <summary>
/// The task that is running to initialize the edit session
/// </summary>
internal Task InitializeTask { get; set; }
/// <summary>
/// Whether or not the session has been initialized
/// </summary>
public bool IsInitialized { get; internal set; }
#endregion
#region Public Methods
public void Initialize(Connector connector, QueryRunner queryRunner, Func<Task> successHandler, Func<Exception, Task> errorHandler)
{
if (IsInitialized)
{
throw new InvalidOperationException(SR.EditDataSessionAlreadyInitialized);
}
if (InitializeTask != null)
{
throw new InvalidOperationException(SR.EditDataSessionAlreadyInitializing);
}
Validate.IsNotNull(nameof(connector), connector);
Validate.IsNotNull(nameof(queryRunner), queryRunner);
Validate.IsNotNull(nameof(successHandler), successHandler);
Validate.IsNotNull(nameof(errorHandler), errorHandler);
// Start up the initialize process
InitializeTask = InitializeInternal(connector, queryRunner, successHandler, errorHandler);
}
/// <summary>
/// Validates that a query can be used for an edit session. The target result set is returned
/// </summary>
@@ -100,6 +140,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <returns>The internal ID of the newly created row</returns>
public EditCreateRowResult CreateRow()
{
ThrowIfNotInitialized();
// Create a new row ID (atomically, since this could be accesses concurrently)
long newRowId = NextRowId++;
@@ -113,13 +155,13 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
}
// Set the default values of the row if we know them
string[] defaultValues = new string[objectMetadata.Columns.Count];
for(int i = 0; i < objectMetadata.Columns.Count; i++)
string[] defaultValues = new string[objectMetadata.Columns.Length];
for(int i = 0; i < objectMetadata.Columns.Length; i++)
{
EditColumnWrapper col = objectMetadata.Columns[i];
EditColumnMetadata col = objectMetadata.Columns[i];
// If the column is calculated, return the calculated placeholder as the display value
if (col.IsCalculated)
if (col.IsCalculated.HasTrue())
{
defaultValues[i] = SR.EditDataComputedColumnPlaceholder;
}
@@ -150,6 +192,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <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)
{
ThrowIfNotInitialized();
Validate.IsNotNull(nameof(connection), connection);
Validate.IsNotNull(nameof(successHandler), successHandler);
Validate.IsNotNull(nameof(errorHandler), errorHandler);
@@ -173,6 +217,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <param name="rowId">The internal ID of the row to delete</param>
public void DeleteRow(long rowId)
{
ThrowIfNotInitialized();
// Sanity check the row ID
if (rowId >= NextRowId || rowId < 0)
{
@@ -196,6 +242,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <returns>An array of rows with pending edits applied</returns>
public async Task<EditRow[]> GetRows(long startIndex, int rowCount)
{
ThrowIfNotInitialized();
// Get the cached rows from the result set
ResultSetSubset cachedRows = startIndex < associatedResultSet.RowCount
? await associatedResultSet.GetSubset(startIndex, rowCount)
@@ -249,6 +297,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <returns>String version of the old value for the cell</returns>
public string RevertCell(long rowId, int columnId)
{
ThrowIfNotInitialized();
// Attempt to get the row edit with the given ID
RowEditBase pendingEdit;
if (!EditCache.TryGetValue(rowId, out pendingEdit))
@@ -269,6 +319,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <param name="rowId">The internal ID of the row to reset</param>
public void RevertRow(long rowId)
{
ThrowIfNotInitialized();
// Attempt to remove the row with the given ID
RowEditBase removedEdit;
if (!EditCache.TryRemove(rowId, out removedEdit))
@@ -284,6 +336,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <returns></returns>
public string ScriptEdits(string outputPath)
{
ThrowIfNotInitialized();
// Validate the output path
// @TODO: Reinstate this code once we have an interface around file generation
//if (outputPath == null)
@@ -328,6 +382,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// <param name="newValue">The new string value of the cell to update</param>
public EditUpdateCellResult UpdateCell(long rowId, int columnId, string newValue)
{
ThrowIfNotInitialized();
// Sanity check to make sure that the row ID is in the range of possible values
if (rowId >= NextRowId || rowId < 0)
{
@@ -347,6 +403,38 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
#endregion
private async Task InitializeInternal(Connector connector, QueryRunner queryRunner,
Func<Task> successHandler, Func<Exception, Task> failureHandler)
{
try
{
// Step 1) Look up the SMO metadata
objectMetadata = metadataFactory.GetObjectMetadata(await connector(), objectName, objectType);
// Step 2) Get and execute a query for the rows in the object we're looking up
EditSessionQueryExecutionState state = await queryRunner(ConstructInitializeQuery());
if (state.Query == null)
{
// TODO: Move to SR file
string message = state.Message ?? SR.EditDataQueryFailed;
throw new Exception(message);
}
// Step 3) Setup the internal state
associatedResultSet = ValidateQueryForSession(state.Query);
NextRowId = associatedResultSet.RowCount;
EditCache = new ConcurrentDictionary<long, RowEditBase>();
IsInitialized = true;
// Step 4) Return our success
await successHandler();
}
catch (Exception e)
{
await failureHandler(e);
}
}
private async Task CommitEditsInternal(DbConnection connection, Func<Task> successHandler, Func<Exception, Task> errorHandler)
{
try
@@ -378,5 +466,50 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
await errorHandler(e);
}
}
private string ConstructInitializeQuery()
{
// Using the columns we know, put together a query for the rows in the table
var columns = objectMetadata.Columns.Select(col => col.EscapedName);
var columnClause = string.Join(", ", columns);
return $"SELECT ${columnClause} FROM ${objectMetadata.EscapedMultipartName}";
}
private void ThrowIfNotInitialized()
{
if (!IsInitialized)
{
throw new InvalidOperationException(SR.EditDataSessionNotInitialized);
}
}
/// <summary>
/// State object to return upon completion of an edit session intialization query
/// </summary>
public class EditSessionQueryExecutionState
{
/// <summary>
/// The query object that was used to execute the edit initialization query. If
/// <c>null</c> the query was not successfully executed.
/// </summary>
public Query Query { get; set; }
/// <summary>
/// Any message that may have occurred during execution of the query (ie, exceptions).
/// If this is and <see cref="Query"/> are <c>null</c> then the error messages were
/// returned via message events.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Constructs a new instance. Sets the values of the properties.
/// </summary>
public EditSessionQueryExecutionState(Query query, string message = null)
{
Query = query;
Message = message;
}
}
}
}

View File

@@ -0,0 +1,85 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
/// <summary>
/// Provides metadata about the table or view being edited
/// </summary>
public class EditTableMetadata
{
/// <summary>
/// Constructs a simple edit table metadata provider
/// </summary>
public EditTableMetadata()
{
HasExtendedProperties = false;
}
#region Basic Properties (properties provided by SMO)
/// <summary>
/// List of columns in the object being edited
/// </summary>
public EditColumnMetadata[] Columns { get; set; }
/// <summary>
/// Full escaped multipart identifier for the object being edited
/// </summary>
public string EscapedMultipartName { get; set; }
/// <summary>
/// Whether or not the object being edited is memory optimized
/// </summary>
public bool IsMemoryOptimized { get; set; }
#endregion
#region Extended Properties (properties provided by SqlClient)
/// <summary>
/// Whether or not the table has had extended properties added to it
/// </summary>
public bool HasExtendedProperties { get; private set; }
/// <summary>
/// List of columns that are used to uniquely identify a row
/// </summary>
public EditColumnMetadata[] KeyColumns { get; private set; }
#endregion
/// <summary>
/// Extracts extended column properties from the database columns from SQL Client
/// </summary>
/// <param name="dbColumnWrappers">The column information provided by SQL Client</param>
public void Extend(DbColumnWrapper[] dbColumnWrappers)
{
Validate.IsNotNull(nameof(dbColumnWrappers), dbColumnWrappers);
// Iterate over the column wrappers and improve the columns we have
for (int i = 0; i < Columns.Length; i++)
{
Columns[i].Extend(dbColumnWrappers[i]);
}
// Determine what the key columns are
KeyColumns = Columns.Where(c => c.IsKey.HasTrue()).ToArray();
if (KeyColumns.Length == 0)
{
// We didn't find any explicit key columns. Instead, we'll use all columns that are
// trustworthy for uniqueness (usually all the columns)
KeyColumns = Columns.Where(c => c.IsTrustworthyForUniqueness.HasTrue()).ToArray();
}
// Mark that the table is now extended
HasExtendedProperties = true;
}
}
}

View File

@@ -4,7 +4,6 @@
//
using System.Data.Common;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
@@ -17,10 +16,9 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// Generates a edit-ready metadata object
/// </summary>
/// <param name="connection">Connection to use for getting metadata</param>
/// <param name="columns">List of columns from a query against the object</param>
/// <param name="objectName">Name of the object to return metadata for</param>
/// <param name="objectType">Type of the object to return metadata for</param>
/// <returns>Metadata about the object requested</returns>
IEditTableMetadata GetObjectMetadata(DbConnection connection, DbColumnWrapper[] columns, string objectName, string objectType);
EditTableMetadata GetObjectMetadata(DbConnection connection, string objectName, string objectType);
}
}

View File

@@ -1,36 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
/// <summary>
/// An interface used in edit scenarios that defines properties for what columns are primary
/// keys, and other metadata of the table.
/// </summary>
public interface IEditTableMetadata
{
/// <summary>
/// All columns in the table that's being edited
/// </summary>
IReadOnlyList<EditColumnWrapper> Columns { get; }
/// <summary>
/// The escaped name of the table that's being edited
/// </summary>
string EscapedMultipartName { get; }
/// <summary>
/// Whether or not this table is a memory optimized table
/// </summary>
bool IsMemoryOptimized { get; }
/// <summary>
/// Columns that can be used to uniquely identify the a row
/// </summary>
IReadOnlyList<EditColumnWrapper> KeyColumns { get; }
}
}

View File

@@ -4,12 +4,13 @@
//
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
@@ -22,11 +23,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// Generates a edit-ready metadata object using SMO
/// </summary>
/// <param name="connection">Connection to use for getting metadata</param>
/// <param name="columns">List of columns from a query against the object</param>
/// <param name="objectName">Name of the object to return metadata for</param>
/// <param name="objectType">Type of the object to return metadata for</param>
/// <returns>Metadata about the object requested</returns>
public IEditTableMetadata GetObjectMetadata(DbConnection connection, DbColumnWrapper[] columns, string objectName, string objectType)
public EditTableMetadata GetObjectMetadata(DbConnection connection, string objectName, string objectType)
{
// Get a connection to the database for SMO purposes
SqlConnection sqlConn = connection as SqlConnection;
@@ -44,25 +44,59 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
sqlConn = reliableConn.GetUnderlyingConnection();
}
// Connect with SMO and get the metadata for the table
Server server = new Server(new ServerConnection(sqlConn));
TableViewTableTypeBase result;
TableViewTableTypeBase smoResult;
switch (objectType.ToLowerInvariant())
{
case "table":
result = server.Databases[sqlConn.Database].Tables[objectName];
smoResult = server.Databases[sqlConn.Database].Tables[objectName];
break;
case "view":
result = server.Databases[sqlConn.Database].Views[objectName];
smoResult = server.Databases[sqlConn.Database].Views[objectName];
break;
default:
throw new ArgumentOutOfRangeException(nameof(objectType), SR.EditDataUnsupportedObjectType(objectType));
}
if (result == null)
if (smoResult == null)
{
throw new ArgumentOutOfRangeException(nameof(objectName), SR.EditDataObjectMetadataNotFound);
}
return new SmoEditTableMetadata(columns, result);
// Generate the edit column metadata
List<EditColumnMetadata> editColumns = new List<EditColumnMetadata>();
for (int i = 0; i < smoResult.Columns.Count; i++)
{
Column smoColumn = smoResult.Columns[i];
// The default value may be escaped
string defaultValue = smoColumn.DefaultConstraint == null
? null
: SqlScriptFormatter.UnwrapLiteral(smoColumn.DefaultConstraint.Text);
EditColumnMetadata column = new EditColumnMetadata
{
DefaultValue = defaultValue,
EscapedName = SqlScriptFormatter.FormatIdentifier(smoColumn.Name),
Ordinal = i,
};
editColumns.Add(column);
}
// Only tables can be memory-optimized
Table smoTable = smoResult as Table;
bool isMemoryOptimized = smoTable != null && smoTable.IsMemoryOptimized;
// Escape the parts of the name
string[] objectNameParts = {smoResult.Schema, smoResult.Name};
string escapedMultipartName = SqlScriptFormatter.FormatMultipartIdentifier(objectNameParts);
return new EditTableMetadata
{
Columns = editColumns.ToArray(),
EscapedMultipartName = escapedMultipartName,
IsMemoryOptimized = isMemoryOptimized,
};
}
}
}

View File

@@ -1,109 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
/// <summary>
/// Provides metadata about the table or view being edited
/// </summary>
public class SmoEditTableMetadata : IEditTableMetadata
{
private readonly List<EditColumnWrapper> columns;
private readonly List<EditColumnWrapper> keyColumns;
/// <summary>
/// Constructor that extracts useful metadata from the provided metadata objects
/// </summary>
/// <param name="dbColumns">DB columns from the ResultSet</param>
/// <param name="smoObject">SMO metadata object for the table/view being edited</param>
public SmoEditTableMetadata(IList<DbColumnWrapper> dbColumns, TableViewTableTypeBase smoObject)
{
Validate.IsNotNull(nameof(dbColumns), dbColumns);
Validate.IsNotNull(nameof(smoObject), smoObject);
// Make sure that we have equal columns on both metadata providers
Debug.Assert(dbColumns.Count == smoObject.Columns.Count);
// Create the columns for edit usage
columns = new List<EditColumnWrapper>();
for (int i = 0; i < dbColumns.Count; i++)
{
Column smoColumn = smoObject.Columns[i];
DbColumnWrapper dbColumn = dbColumns[i];
// A column is trustworthy for uniqueness if it can be updated or it has an identity
// property. If both of these are false (eg, timestamp) we can't trust it to uniquely
// identify a row in the table
bool isTrustworthyForUniqueness = dbColumn.IsUpdatable || smoColumn.Identity;
// The default value may be escaped
string defaultValue = smoColumn.DefaultConstraint == null
? null
: SqlScriptFormatter.UnwrapLiteral(smoColumn.DefaultConstraint.Text);
EditColumnWrapper column = new EditColumnWrapper
{
DbColumn = dbColumn,
Ordinal = i,
DefaultValue = defaultValue,
EscapedName = SqlScriptFormatter.FormatIdentifier(dbColumn.ColumnName),
IsTrustworthyForUniqueness = isTrustworthyForUniqueness,
// A key column is determined by whether it is in the primary key and trustworthy
IsKey = smoColumn.InPrimaryKey && isTrustworthyForUniqueness,
// A column is calculated if it is identity, computed, or otherwise not updatable
IsCalculated = smoColumn.Identity || smoColumn.Computed || !dbColumn.IsUpdatable
};
columns.Add(column);
}
// Determine what the key columns are
keyColumns = columns.Where(c => c.IsKey).ToList();
if (keyColumns.Count == 0)
{
// We didn't find any explicit key columns. Instead, we'll use all columns that are
// trustworthy for uniqueness (usually all the columns)
keyColumns = columns.Where(c => c.IsTrustworthyForUniqueness).ToList();
}
// If a table is memory optimized it is Hekaton. If it's a view, then it can't be Hekaton
Table smoTable = smoObject as Table;
IsMemoryOptimized = smoTable != null && smoTable.IsMemoryOptimized;
// Escape the parts of the name
string[] objectNameParts = {smoObject.Schema, smoObject.Name};
EscapedMultipartName = SqlScriptFormatter.FormatMultipartIdentifier(objectNameParts);
}
/// <summary>
/// Read-only list of columns in the object being edited
/// </summary>
public IReadOnlyList<EditColumnWrapper> Columns => columns.AsReadOnly();
/// <summary>
/// Full escaped multipart identifier for the object being edited
/// </summary>
public string EscapedMultipartName { get; }
/// <summary>
/// Whether or not the object being edited is memory optimized
/// </summary>
public bool IsMemoryOptimized { get; }
/// <summary>
/// Read-only list of columns that are used to uniquely identify a row
/// </summary>
public IReadOnlyList<EditColumnWrapper> KeyColumns => keyColumns.AsReadOnly();
}
}

View File

@@ -35,7 +35,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <param name="rowId">Internal ID of the row that is being created</param>
/// <param name="associatedResultSet">The result set for the rows in the table we're editing</param>
/// <param name="associatedMetadata">The metadata for table we're editing</param>
public RowCreate(long rowId, ResultSet associatedResultSet, IEditTableMetadata associatedMetadata)
public RowCreate(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
: base(rowId, associatedResultSet, associatedMetadata)
{
newCells = new CellUpdate[associatedResultSet.Columns.Length];

View File

@@ -28,7 +28,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <param name="rowId">Internal ID of the row to be deleted</param>
/// <param name="associatedResultSet">Result set that is being edited</param>
/// <param name="associatedMetadata">Improved metadata of the object being edited</param>
public RowDelete(long rowId, ResultSet associatedResultSet, IEditTableMetadata associatedMetadata)
public RowDelete(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
: base(rowId, associatedResultSet, associatedMetadata)
{
}

View File

@@ -36,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <param name="rowId">The internal ID of the row that is being edited</param>
/// <param name="associatedResultSet">The result set that will be updated</param>
/// <param name="associatedMetadata">Metadata provider for the object to edit</param>
protected RowEditBase(long rowId, ResultSet associatedResultSet, IEditTableMetadata associatedMetadata)
protected RowEditBase(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
{
RowId = rowId;
AssociatedResultSet = associatedResultSet;
@@ -58,7 +58,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <summary>
/// The metadata for the table this edit is associated to
/// </summary>
public IEditTableMetadata AssociatedObjectMetadata { get; }
public EditTableMetadata AssociatedObjectMetadata { get; }
/// <summary>
/// Sort ID for a row edit. Ensures that when a collection of RowEditBase objects are
@@ -162,7 +162,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
}
IList<DbCellValue> row = AssociatedResultSet.GetRow(RowId);
foreach (EditColumnWrapper col in AssociatedObjectMetadata.KeyColumns)
foreach (EditColumnMetadata col in AssociatedObjectMetadata.KeyColumns)
{
// Put together a clause for the value of the cell
DbCellValue cellData = row[col.Ordinal];

View File

@@ -39,7 +39,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
/// <param name="rowId">Internal ID of the row that will be updated with this object</param>
/// <param name="associatedResultSet">Result set for the rows of the object to update</param>
/// <param name="associatedMetadata">Metadata provider for the object to update</param>
public RowUpdate(long rowId, ResultSet associatedResultSet, IEditTableMetadata associatedMetadata)
public RowUpdate(long rowId, ResultSet associatedResultSet, EditTableMetadata associatedMetadata)
: base(rowId, associatedResultSet, associatedMetadata)
{
cellUpdates = new ConcurrentDictionary<int, CellUpdate>();