mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-27 09:35:38 -05:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user