From e3ec6eb739a2057b13704349eefec5da3ba7ec5a Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Tue, 9 Jul 2019 17:03:55 -0700 Subject: [PATCH] Handling the HierarchyId for edit data scenario (#709) * Handling the HierarchyId for edit data scenario * comments --- .../EditData/EditColumnMetadata.cs | 16 +++++ .../EditData/EditDataService.cs | 37 ++++++++-- .../EditData/EditSession.cs | 26 +++++-- .../EditData/SmoEditMetadataFactory.cs | 1 + .../EditData/UpdateManagement/RowCreate.cs | 68 ++++++++++++------- .../EditData/UpdateManagement/RowUpdate.cs | 44 ++++++++---- .../Contracts/DbColumnWrapper.cs | 16 +++-- 7 files changed, 153 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditColumnMetadata.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditColumnMetadata.cs index 54739ce5..71a3bce9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditColumnMetadata.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditColumnMetadata.cs @@ -36,6 +36,22 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData /// public string EscapedName { get; set; } + /// + /// The column's expression for select statement + /// + public string ExpressionForSelectStatement + { + get + { + return IsHierarchyId ? string.Format("{0}.ToString() AS {0}", EscapedName) : EscapedName; + } + } + + /// + /// Whether or not the column's data type is HierarchyId + /// + public bool IsHierarchyId { get; set; } + /// /// Whether or not the column is computed /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs index ec29013f..e83d042c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs @@ -8,6 +8,7 @@ using System.Collections.Concurrent; using System.Data.Common; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; @@ -149,11 +150,12 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData internal async Task HandleInitializeRequest(EditInitializeParams initParams, RequestContext requestContext) { - Func executionFailureHandler = (e) => SendSessionReadyEvent(requestContext, initParams.OwnerUri, false, e.Message); - Func executionSuccessHandler = () => SendSessionReadyEvent(requestContext, initParams.OwnerUri, true, null); + InitializeEditRequestContext context = new InitializeEditRequestContext(requestContext); + Func executionFailureHandler = (e) => SendSessionReadyEvent(context, initParams.OwnerUri, false, e.Message); + Func executionSuccessHandler = () => SendSessionReadyEvent(context, initParams.OwnerUri, true, null); EditSession.Connector connector = () => connectionService.GetOrOpenConnection(initParams.OwnerUri, ConnectionType.Edit, alwaysPersistSecurity: true); - EditSession.QueryRunner queryRunner = q => SessionInitializeQueryRunner(initParams.OwnerUri, requestContext, q); + EditSession.QueryRunner queryRunner = q => SessionInitializeQueryRunner(initParams.OwnerUri, context, q); try { @@ -169,6 +171,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData throw new InvalidOperationException(SR.EditDataSessionAlreadyExists); } + context.ResultSetHandler = (ResultSetEventParams resultSetEventParams) => { session.UpdateColumnInformationWithMetadata(resultSetEventParams.ResultSetSummary.ColumnInfo); }; + // Initialize the session session.Initialize(initParams, connector, queryRunner, executionSuccessHandler, executionFailureHandler); @@ -214,7 +218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData await requestContext.SendResult(result); } - catch(Exception e) + catch (Exception e) { await requestContext.SendError(e.Message); } @@ -341,4 +345,29 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData #endregion } + + /// + /// Context for InitializeEditRequest, to provide a way to update the result set before sending it to UI. + /// + internal class InitializeEditRequestContext : IEventSender + { + private RequestContext _context; + + public Action ResultSetHandler { get; set; } + + public InitializeEditRequestContext(RequestContext context) + { + this._context = context; + } + + public Task SendEvent(EventType eventType, TParams eventParams) + { + if (eventParams is ResultSetEventParams && this.ResultSetHandler != null) + { + this.ResultSetHandler(eventParams as ResultSetEventParams); + } + return _context.SendEvent(eventType, eventParams); + } + + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs index 299db9fe..91206df8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs @@ -152,7 +152,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData /// public static void CheckResultsForInvalidColumns(ResultSet results, string tableName) { - if(SchemaContainsMultipleItems(results.Columns, col => col.BaseCatalogName) + if (SchemaContainsMultipleItems(results.Columns, col => col.BaseCatalogName) || SchemaContainsMultipleItems(results.Columns, col => col.BaseSchemaName) || SchemaContainsMultipleItems(results.Columns, col => col.BaseTableName)) { @@ -168,7 +168,8 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData throw new InvalidOperationException(SR.EditDataAliasesNotSupported); } - if (col.IsExpression.HasTrue()) + // We have changed HierarchyId column to an expression so that it can be displayed properly + if (!col.IsHierarchyId && col.IsExpression.HasTrue()) { throw new InvalidOperationException(SR.EditDataExpressionsNotSupported); } @@ -480,6 +481,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData // Step 3) Setup the internal state associatedResultSet = ValidateQueryForSession(state.Query); + UpdateColumnInformationWithMetadata(associatedResultSet.Columns); CheckResultsForInvalidColumns(associatedResultSet, initParams.ObjectName); NextRowId = associatedResultSet.RowCount; @@ -499,7 +501,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData public static string[] GetEditTargetName(EditInitializeParams initParams) { return initParams.SchemaName != null - ? new [] { initParams.SchemaName, initParams.ObjectName } + ? new[] { initParams.SchemaName, initParams.ObjectName } : FromSqlScript.DecodeMultipartIdentifier(initParams.ObjectName); } @@ -508,7 +510,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData try { // @TODO: Add support for transactional commits - + // Trust the RowEdit to sort itself appropriately var editOperations = EditCache.Values.ToList(); editOperations.Sort(); @@ -554,7 +556,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData } // Using the columns we know, add them to the query - var columns = metadata.Columns.Select(col => col.EscapedName); + var columns = metadata.Columns.Select(col => col.ExpressionForSelectStatement); var columnClause = string.Join(", ", columns); queryBuilder.Append(columnClause); @@ -590,6 +592,20 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData EditCache.TryRemove(rowId, out removedRow); } + internal void UpdateColumnInformationWithMetadata(DbColumnWrapper[] columns) + { + if (columns == null || this.objectMetadata == null) + { + return; + } + + foreach (DbColumnWrapper col in columns) + { + var columnMetadata = objectMetadata.Columns.FirstOrDefault(cm => { return cm.EscapedName == ToSqlScript.FormatIdentifier(col.ColumnName); }); + col.IsHierarchyId = columnMetadata != null && columnMetadata.IsHierarchyId; + } + } + #endregion /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/SmoEditMetadataFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/SmoEditMetadataFactory.cs index 319f1fcd..8f56c737 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/SmoEditMetadataFactory.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/SmoEditMetadataFactory.cs @@ -118,6 +118,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData DefaultValue = defaultValue, EscapedName = ToSqlScript.FormatIdentifier(smoColumn.Name), Ordinal = i, + IsHierarchyId = smoColumn.DataType.SqlDataType == SqlDataType.HierarchyId, }; editColumns.Add(column); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs index a1ebd2d4..7fc9eac2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs @@ -43,7 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement : base(rowId, associatedResultSet, associatedMetadata) { newCells = new CellUpdate[AssociatedResultSet.Columns.Length]; - + // Process the default cell values. If the column is calculated, then the value is a placeholder DefaultValues = AssociatedObjectMetadata.Columns.Select((col, index) => col.IsCalculated.HasTrue() ? SR.EditDataComputedColumnPlaceholder @@ -61,7 +61,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// provided cell update during commit /// public string[] DefaultValues { get; } - + #region Public Methods /// @@ -96,14 +96,21 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement List inValues = new List(); List inParameters = new List(); List selectColumns = new List(); - for(int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++) + for (int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++) { DbColumnWrapper column = AssociatedResultSet.Columns[i]; EditColumnMetadata metadata = AssociatedObjectMetadata.Columns[i]; CellUpdate cell = newCells[i]; - + // Add the output columns regardless of whether the column is read only - outClauseColumnNames.Add($"inserted.{metadata.EscapedName}"); + if (metadata.IsHierarchyId) + { + outClauseColumnNames.Add($"inserted.{metadata.EscapedName}.ToString() {metadata.EscapedName}"); + } + else + { + outClauseColumnNames.Add($"inserted.{metadata.EscapedName}"); + } declareColumns.Add($"{metadata.EscapedName} {ToSqlScript.FormatColumnType(column, useSemanticEquivalent: true)}"); selectColumns.Add(metadata.EscapedName); @@ -112,16 +119,25 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { continue; } - + // Add the input column inColumnNames.Add(metadata.EscapedName); - + // Add the input values as parameters string paramName = $"@Value{RowId}_{i}"; - inValues.Add(paramName); - inParameters.Add(new SqlParameter(paramName, column.SqlDbType) {Value = cell.Value}); + + if (metadata.IsHierarchyId) + { + inValues.Add($"CONVERT(hierarchyid,{paramName})"); + } + else + { + inValues.Add(paramName); + } + + inParameters.Add(new SqlParameter(paramName, column.SqlDbType) { Value = cell.Value }); } - + // Put everything together into a single query // Step 1) Build a temp table for inserting output values into string tempTableName = $"@Insert{RowId}Output"; @@ -130,32 +146,32 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement // Step 2) Build the insert statement string joinedOutClauseNames = string.Join(", ", outClauseColumnNames); string insertStatement = inValues.Count > 0 - ? string.Format(InsertOutputValuesStatement, + ? string.Format(InsertOutputValuesStatement, AssociatedObjectMetadata.EscapedMultipartName, - string.Join(", ", inColumnNames), + string.Join(", ", inColumnNames), joinedOutClauseNames, tempTableName, string.Join(", ", inValues)) - : string.Format(InsertOutputDefaultStatement, + : string.Format(InsertOutputDefaultStatement, AssociatedObjectMetadata.EscapedMultipartName, joinedOutClauseNames, tempTableName); // Step 3) Build the select statement string selectStatement = string.Format(SelectStatement, string.Join(", ", selectColumns), tempTableName); - + // Step 4) Put it all together into a results object StringBuilder query = new StringBuilder(); query.AppendLine(declareStatement); query.AppendLine(insertStatement); query.Append(selectStatement); - + // Build the command DbCommand command = connection.CreateCommand(); command.CommandText = query.ToString(); command.CommandType = CommandType.Text; command.Parameters.AddRange(inParameters.ToArray()); - + return command; } @@ -168,7 +184,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { // Get edit cells for each EditCell[] editCells = newCells.Select(GetEditCell).ToArray(); - + return new EditRow { Id = RowId, @@ -190,23 +206,23 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { DbColumnWrapper column = AssociatedResultSet.Columns[i]; CellUpdate cell = newCells[i]; - + // Continue if we're not inserting a value for this column if (!IsCellValueProvided(column, cell, DefaultValues[i])) { continue; } - + // Column is provided inColumns.Add(AssociatedObjectMetadata.Columns[i].EscapedName); inValues.Add(ToSqlScript.FormatValue(cell.AsDbCellValue, column)); } - + // Build the insert statement return inValues.Count > 0 - ? string.Format(InsertScriptValuesStatement, + ? string.Format(InsertScriptValuesStatement, AssociatedObjectMetadata.EscapedMultipartName, - string.Join(", ", inColumns), + string.Join(", ", inColumns), string.Join(", ", inValues)) : string.Format(InsertScriptDefaultStatement, AssociatedObjectMetadata.EscapedMultipartName); } @@ -225,7 +241,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement newCells[columnId] = null; return new EditRevertCellResult { - IsRowDirty = true, + IsRowDirty = true, Cell = GetEditCell(null, columnId) }; } @@ -277,7 +293,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { return false; } - + // Make sure a value was provided for the cell if (cell == null) { @@ -286,14 +302,14 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue(column.ColumnName)); } - + // There is a default value (or omitting the value is fine), so trust the db will apply it correctly return false; } return true; } - + private EditCell GetEditCell(CellUpdate cell, int index) { DbCellValue dbCell; diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs index c5e5888e..073e574b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs @@ -97,32 +97,46 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement for (int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++) { EditColumnMetadata metadata = AssociatedObjectMetadata.Columns[i]; - + // Add the output columns regardless of whether the column is read only declareColumns.Add($"{metadata.EscapedName} {ToSqlScript.FormatColumnType(metadata.DbColumn, useSemanticEquivalent: true)}"); - outClauseColumns.Add($"inserted.{metadata.EscapedName}"); + if (metadata.IsHierarchyId) + { + outClauseColumns.Add($"inserted.{metadata.EscapedName}.ToString() {metadata.EscapedName}"); + } + else + { + outClauseColumns.Add($"inserted.{metadata.EscapedName}"); + } selectColumns.Add(metadata.EscapedName); - + // If we have a new value for the column, proccess it now CellUpdate cellUpdate; if (cellUpdates.TryGetValue(i, out cellUpdate)) { string paramName = $"@Value{RowId}_{i}"; - setComponents.Add($"{metadata.EscapedName} = {paramName}"); - inParameters.Add(new SqlParameter(paramName, AssociatedResultSet.Columns[i].SqlDbType) {Value = cellUpdate.Value}); + if (metadata.IsHierarchyId) + { + setComponents.Add($"{metadata.EscapedName} = CONVERT(hierarchyid,{paramName})"); + } + else + { + setComponents.Add($"{metadata.EscapedName} = {paramName}"); + } + inParameters.Add(new SqlParameter(paramName, AssociatedResultSet.Columns[i].SqlDbType) { Value = cellUpdate.Value }); } } - + // Put everything together into a single query // Step 1) Build a temp table for inserting output values into string tempTableName = $"@Update{RowId}Output"; string declareStatement = string.Format(DeclareStatement, tempTableName, string.Join(", ", declareColumns)); - + // Step 2) Build the update statement WhereClause whereClause = GetWhereClause(true); - - string updateStatementFormat = AssociatedObjectMetadata.IsMemoryOptimized - ? UpdateOutputMemOptimized + + string updateStatementFormat = AssociatedObjectMetadata.IsMemoryOptimized + ? UpdateOutputMemOptimized : UpdateOutput; string updateStatement = string.Format(updateStatementFormat, AssociatedObjectMetadata.EscapedMultipartName, @@ -135,10 +149,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement string validateScript = string.Format(CultureInfo.InvariantCulture, validateUpdateOnlyOneRow, AssociatedObjectMetadata.EscapedMultipartName, whereClause.CommandText); - + // Step 3) Build the select statement string selectStatement = string.Format(SelectStatement, string.Join(", ", selectColumns), tempTableName); - + // Step 4) Put it all together into a results object StringBuilder query = new StringBuilder(); query.AppendLine(declareStatement); @@ -146,7 +160,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement query.AppendLine(updateStatement); query.AppendLine(selectStatement); query.Append("END"); - + // Build the command DbCommand command = connection.CreateCommand(); command.CommandText = query.ToString(); @@ -198,7 +212,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement return $"{formattedColumnName} = {formattedValue}"; }); string setClause = string.Join(", ", setComponents); - + // Put everything together into a single query string whereClause = GetWhereClause(false).CommandText; string updateStatementFormat = AssociatedObjectMetadata.IsMemoryOptimized @@ -247,7 +261,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement public override EditUpdateCellResult SetCell(int columnId, string newValue) { // Validate the value and convert to object - ValidateColumnIsUpdatable(columnId); + ValidateColumnIsUpdatable(columnId); CellUpdate update = new CellUpdate(AssociatedResultSet.Columns[columnId], newValue); // If the value is the same as the old value, we shouldn't make changes diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs index dd60564e..6389b8ce 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs @@ -199,8 +199,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts object assemblyQualifiedName = column.UdtAssemblyQualifiedName; const string hierarchyId = "MICROSOFT.SQLSERVER.TYPES.SQLHIERARCHYID"; - if (assemblyQualifiedName != null && - string.Equals(assemblyQualifiedName.ToString(), hierarchyId, StringComparison.OrdinalIgnoreCase)) + if (assemblyQualifiedName != null + && assemblyQualifiedName.ToString().StartsWith(hierarchyId, StringComparison.OrdinalIgnoreCase)) { DataType = typeof(SqlBinary); } @@ -259,6 +259,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public SqlDbType SqlDbType { get; private set; } + /// + /// Whther this is a HierarchyId column + /// + public bool IsHierarchyId { get; set; } + /// /// Whether or not the column is an XML Reader type. /// @@ -286,10 +291,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// /// /// Logic taken from SSDT determination of updatable columns + /// Special treatment for HierarchyId since we are using an Expression for HierarchyId column and expression column is readonly. /// - public bool IsUpdatable => !IsAutoIncrement.HasTrue() && - !IsReadOnly.HasTrue() && - !IsSqlXmlType; + public bool IsUpdatable => (!IsAutoIncrement.HasTrue() && + !IsReadOnly.HasTrue() && + !IsSqlXmlType) || IsHierarchyId; #endregion