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();
+ }
+
}
}