From a1aa3f8b4c347df32f75ab94dc3f32269469509f Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 28 Mar 2017 16:30:46 +0000 Subject: [PATCH] Add table and view metadata messges (#296) * Add SMO table property lookup event * Fix a couple bugs in column metadata * Add GetView metadata message --- .../Metadata/Contracts/ColumnMetadata.cs | 112 ++++++++++++++++ .../Metadata/Contracts/TableMetadata.cs | 85 ++++++++++++ .../Contracts/TableMetadataRequest.cs | 33 +++++ .../Metadata/Contracts/ViewMetadataRequest.cs | 21 +++ .../Metadata/MetadataService.cs | 60 ++++++++- .../Metadata/SmoMetadataFactory.cs | 122 ++++++++++++++++++ .../Metadata/MetadataServiceTests.cs | 53 +++++++- 7 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ColumnMetadata.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadata.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadataRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ViewMetadataRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/SmoMetadataFactory.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ColumnMetadata.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ColumnMetadata.cs new file mode 100644 index 00000000..c3a7f553 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ColumnMetadata.cs @@ -0,0 +1,112 @@ +// +// 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.Metadata +{ + /// + /// ColumnMetadata class + /// + public class ColumnMetadata + { + /// + /// Constructs a simple edit column metadata provider + /// + public ColumnMetadata() + { + HasExtendedProperties = false; + } + + #region Basic Properties (properties provided by SMO) + + /// + /// If set, this is a string representation of the default value. If set to null, then the + /// column does not have a default value. + /// + public string DefaultValue { get; set; } + + /// + /// Escaped identifier for the name of the column + /// + public string EscapedName { get; set; } + + /// + /// Whether or not the column is computed + /// + public bool IsComputed { get; set; } + + /// + /// Whether or not the column is deterministically computed + /// + public bool IsDeterministic { get; set; } + + /// + /// Whether or not the column is an identity column + /// + public bool IsIdentity { get; set; } + + /// + /// The ordinal ID of the column + /// + public int Ordinal { get; set; } + + #endregion + + #region Extended Properties (properties provided by SqlClient) + + // public DbColumnWrapper DbColumn { get; private set; } + + /// + /// Whether or not the column has extended properties + /// + public bool HasExtendedProperties { get; private set; } + + /// + /// Whether or not the column is calculated on the server side. This could be a computed + /// column or a identity column. + /// + public bool? IsCalculated { get; private set; } + + /// + /// Whether or not the column is used in a key to uniquely identify a row + /// + public bool? IsKey { get; private set; } + + /// + /// Whether or not the column can be trusted for uniqueness + /// + public bool? IsTrustworthyForUniqueness { get; private set; } + + #endregion + + /// + /// Extracts extended column properties from the database columns from SQL Client + /// + /// The column information provided by SQL Client + 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; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadata.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadata.cs new file mode 100644 index 00000000..5d019345 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadata.cs @@ -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.Metadata +{ + /// + /// Provides metadata about the table or view being edited + /// + public class TableMetadata + { + /// + /// Constructs a simple edit table metadata provider + /// + public TableMetadata() + { + HasExtendedProperties = false; + } + + #region Basic Properties (properties provided by SMO) + + /// + /// List of columns in the object being edited + /// + public ColumnMetadata[] Columns { get; set; } + + /// + /// Full escaped multipart identifier for the object being edited + /// + public string EscapedMultipartName { get; set; } + + /// + /// Whether or not the object being edited is memory optimized + /// + public bool IsMemoryOptimized { get; set; } + + #endregion + + #region Extended Properties (properties provided by SqlClient) + + /// + /// Whether or not the table has had extended properties added to it + /// + public bool HasExtendedProperties { get; private set; } + + /// + /// List of columns that are used to uniquely identify a row + /// + public ColumnMetadata[] KeyColumns { get; private set; } + + #endregion + + /// + /// Extracts extended column properties from the database columns from SQL Client + /// + /// The column information provided by SQL Client + 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; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadataRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadataRequest.cs new file mode 100644 index 00000000..eee9917c --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/TableMetadataRequest.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Metadata.Contracts +{ + public class TableMetadataParams + { + public string OwnerUri { get; set; } + + public string Schema { get; set; } + + public string ObjectName { get; set; } + } + + public class TableMetadataResult + { + public ColumnMetadata[] Columns { get; set; } + } + + /// + /// Retreive metadata for the table described in the TableMetadataParams value + /// + public class TableMetadataRequest + { + public static readonly + RequestType Type = + RequestType.Create("metadata/table"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ViewMetadataRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ViewMetadataRequest.cs new file mode 100644 index 00000000..78c9b98b --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ViewMetadataRequest.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Metadata.Contracts +{ + /// + /// Retreive metadata for the view described in the TableMetadataParams value. + /// This message reuses the table metadata params and result since the exchanged + /// data is the same. + /// + public class ViewMetadataRequest + { + public static readonly + RequestType Type = + RequestType.Create("metadata/view"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs index 9295fd6e..531816d3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs @@ -53,6 +53,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata public void InitializeService(ServiceHost serviceHost) { serviceHost.SetRequestHandler(MetadataListRequest.Type, HandleMetadataListRequest); + serviceHost.SetRequestHandler(TableMetadataRequest.Type, HandleGetTableRequest); + serviceHost.SetRequestHandler(ViewMetadataRequest.Type, HandleGetViewRequest); } /// @@ -87,6 +89,62 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata } } + /// + /// Handle a table metadata query request + /// + internal static async Task HandleGetTableRequest( + TableMetadataParams metadataParams, + RequestContext requestContext) + { + await HandleGetTableOrViewRequest(metadataParams, "table", requestContext); + } + + /// + /// Handle a view metadata query request + /// + internal static async Task HandleGetViewRequest( + TableMetadataParams metadataParams, + RequestContext requestContext) + { + await HandleGetTableOrViewRequest(metadataParams, "view", requestContext); + } + + /// + /// Handle a table pr view metadata query request + /// + private static async Task HandleGetTableOrViewRequest( + TableMetadataParams metadataParams, + string objectType, + RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + MetadataService.ConnectionServiceInstance.TryFindConnection( + metadataParams.OwnerUri, + out connInfo); + + ColumnMetadata[] metadata = null; + if (connInfo != null) + { + SqlConnection sqlConn = OpenMetadataConnection(connInfo); + TableMetadata table = new SmoMetadataFactory().GetObjectMetadata( + sqlConn, metadataParams.Schema, + metadataParams.ObjectName, objectType); + metadata = table.Columns; + } + + await requestContext.SendResult(new TableMetadataResult + { + Columns = metadata + }); + } + catch (Exception ex) + { + await requestContext.SendError(ex.ToString()); + } + } + /// /// Create a SqlConnection to use for querying metadata /// @@ -162,6 +220,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata } } } - } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/SmoMetadataFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/SmoMetadataFactory.cs new file mode 100644 index 00000000..96b5c427 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/SmoMetadataFactory.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +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.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Metadata +{ + /// + /// Interface for a factory that generates metadata for an object to edit + /// + public interface IMetadataFactory + { + /// + /// Generates a edit-ready metadata object + /// + /// Connection to use for getting metadata + /// Name of the object to return metadata for + /// Type of the object to return metadata for + /// Metadata about the object requested + TableMetadata GetObjectMetadata(DbConnection connection, string schemaName, string objectName, string objectType); + } + + /// + /// Factory that generates metadata using a combination of SMO and SqlClient metadata + /// + public class SmoMetadataFactory : IMetadataFactory + { + /// + /// Generates a edit-ready metadata object using SMO + /// + /// Connection to use for getting metadata + /// Name of the object to return metadata for + /// Type of the object to return metadata for + /// Metadata about the object requested + public TableMetadata GetObjectMetadata(DbConnection connection, string schemaName, string objectName, string objectType) + { + // Get a connection to the database for SMO purposes + SqlConnection sqlConn = connection as SqlConnection; + if (sqlConn == null) + { + // It's not actually a SqlConnection, so let's try a reliable SQL connection + ReliableSqlConnection reliableConn = connection as ReliableSqlConnection; + if (reliableConn == null) + { + // If we don't have connection we can use with SMO, just give up on using SMO + return null; + } + + // We have a reliable connection, use the underlying connection + sqlConn = reliableConn.GetUnderlyingConnection(); + } + + // Connect with SMO and get the metadata for the table + Server server = new Server(new ServerConnection(sqlConn)); + Database database = server.Databases[sqlConn.Database]; + TableViewTableTypeBase smoResult; + switch (objectType.ToLowerInvariant()) + { + case "table": + Table table = string.IsNullOrEmpty(schemaName) ? new Table(database, objectName) : new Table(database, objectName, schemaName); + table.Refresh(); + smoResult = table; + break; + case "view": + View view = string.IsNullOrEmpty(schemaName) ? new View(database, objectName) : new View(database, objectName, schemaName); + view.Refresh(); + smoResult = view; + break; + default: + throw new ArgumentOutOfRangeException(nameof(objectType), SR.EditDataUnsupportedObjectType(objectType)); + } + if (smoResult == null) + { + throw new ArgumentOutOfRangeException(nameof(objectName), SR.EditDataObjectMetadataNotFound); + } + + // Generate the edit column metadata + List editColumns = new List(); + 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); + + ColumnMetadata column = new ColumnMetadata + { + 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 TableMetadata + { + Columns = editColumns.ToArray(), + EscapedMultipartName = escapedMultipartName, + IsMemoryOptimized = isMemoryOptimized, + }; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs index aa5def9b..535b22d8 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs @@ -5,13 +5,15 @@ using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Xunit; -using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.Metadata; using System.Collections.Generic; using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; using System.Data.SqlClient; using System; +using Moq; +using Microsoft.SqlTools.Hosting.Protocol; +using System.Threading.Tasks; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata { @@ -27,7 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata { var textDocument = new TextDocumentPosition { - TextDocument = new TextDocumentIdentifier { Uri = Constants.OwnerUri }, + TextDocument = new TextDocumentIdentifier { Uri = Test.Common.Constants.OwnerUri }, Position = new Position { Line = 0, @@ -92,5 +94,52 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata DeleteTestTable(sqlConn); } + + [Fact] + public async void GetTableInfoReturnsValidResults() + { + this.testTableName += new Random().Next(1000000, 9999999).ToString(); + + var result = GetLiveAutoCompleteTestObjects(); + var sqlConn = MetadataService.OpenMetadataConnection(result.ConnectionInfo); + + CreateTestTable(sqlConn); + + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var metadataParmas = new TableMetadataParams + { + OwnerUri = result.ConnectionInfo.OwnerUri, + Schema = this.testTableSchema, + ObjectName = this.testTableName + }; + + await MetadataService.HandleGetTableRequest(metadataParmas, requestContext.Object); + + DeleteTestTable(sqlConn); + + requestContext.VerifyAll(); + } + + [Fact] + public async void GetViewInfoReturnsValidResults() + { + var result = GetLiveAutoCompleteTestObjects(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var metadataParmas = new TableMetadataParams + { + OwnerUri = result.ConnectionInfo.OwnerUri, + Schema = "sys", + ObjectName = "all_objects" + }; + + await MetadataService.HandleGetViewRequest(metadataParmas, requestContext.Object); + + requestContext.VerifyAll(); + } + } }