From b1f494d04de90828ef1fe956e4496b3cada67977 Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Fri, 8 Apr 2022 12:52:24 -0700 Subject: [PATCH] Added uniqueness check for constraints (#1454) * added rule for checking constraints * added constraint validation rule * merge PR's * remove unused code * update comment * remove unused import --- .../Requests/GeneratePreviewReportRequest.cs | 5 + .../ProcessTableDesignerEditRequest.cs | 2 + .../TableDesigner/TableDesignerService.cs | 85 ++++++++---- .../TableDesigner/TableDesignerValidator.cs | 127 +++++++++++------- 4 files changed, 143 insertions(+), 76 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/GeneratePreviewReportRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/GeneratePreviewReportRequest.cs index c2bcd866..8788f7f1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/GeneratePreviewReportRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/GeneratePreviewReportRequest.cs @@ -28,6 +28,11 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts /// Metadata about the table /// public Dictionary Metadata { get; set; } + + /// + /// The table schema validation error + /// + public string SchemaValidationError { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs index c523f91f..25dc171b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs @@ -27,6 +27,8 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts public TableDesignerIssue[] Issues { get; set; } public Dictionary Metadata { get; set; } + + public string InputValidationError { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs index 66d3baeb..5515ec17 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs @@ -10,6 +10,7 @@ using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.Data.Tools.Sql.DesignServices; using Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts; using Dac = Microsoft.Data.Tools.Sql.DesignServices.TableDesigner; using STSHost = Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost; @@ -98,30 +99,44 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner { var refreshViewRequired = false; DesignerPathUtils.Validate(requestParams.TableChangeInfo.Path, requestParams.TableChangeInfo.Type); - switch (requestParams.TableChangeInfo.Type) + try { - case DesignerEditType.Add: - this.HandleAddItemRequest(requestParams); - break; - case DesignerEditType.Remove: - this.HandleRemoveItemRequest(requestParams); - break; - case DesignerEditType.Update: - refreshViewRequired = this.HandleUpdateItemRequest(requestParams); - break; - default: - break; + switch (requestParams.TableChangeInfo.Type) + { + case DesignerEditType.Add: + this.HandleAddItemRequest(requestParams); + break; + case DesignerEditType.Remove: + this.HandleRemoveItemRequest(requestParams); + break; + case DesignerEditType.Update: + refreshViewRequired = this.HandleUpdateItemRequest(requestParams); + break; + default: + break; + } + await SendProcessTableEditResponse(requestParams.TableInfo, requestContext, refreshViewRequired); } - var designer = this.GetTableDesigner(requestParams.TableInfo); - var issues = TableDesignerValidator.Validate(designer.TableViewModel); - await requestContext.SendResult(new ProcessTableDesignerEditResponse() + catch (DesignerValidationException e) { - ViewModel = this.GetTableViewModel(requestParams.TableInfo), - IsValid = issues.Where(i => i.Severity == IssueSeverity.Error).Count() == 0, - Issues = issues.ToArray(), - View = refreshViewRequired ? this.GetDesignerViewInfo(requestParams.TableInfo) : null, - Metadata = this.GetMetadata(requestParams.TableInfo) - }); + await SendProcessTableEditResponse(requestParams.TableInfo, requestContext, refreshViewRequired, e.Message); + } + }); + } + + private async Task SendProcessTableEditResponse(TableInfo tableInfo, RequestContext requestContext, + bool refreshViewRequired, string inputValidationError = null) + { + var designer = this.GetTableDesigner(tableInfo); + var issues = TableDesignerValidator.Validate(designer); + await requestContext.SendResult(new ProcessTableDesignerEditResponse() + { + ViewModel = this.GetTableViewModel(tableInfo), + IsValid = issues.Where(i => i.Severity == IssueSeverity.Error).Count() == 0, + Issues = issues.ToArray(), + View = refreshViewRequired ? this.GetDesignerViewInfo(tableInfo) : null, + Metadata = this.GetMetadata(tableInfo), + InputValidationError = inputValidationError }); } @@ -168,16 +183,30 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner { return this.HandleRequest(requestContext, async () => { - var table = this.GetTableDesigner(tableInfo); - var report = table.GenerateReport(); - var generatePreviewReportResult = new GeneratePreviewReportResult(); - generatePreviewReportResult.Report = report; - generatePreviewReportResult.MimeType = "text/markdown"; - generatePreviewReportResult.Metadata = this.GetMetadata(tableInfo); - await requestContext.SendResult(generatePreviewReportResult); + try + { + await SendGeneratePreviewReportResult(tableInfo, requestContext); + } + catch (DesignerValidationException e) + { + await SendGeneratePreviewReportResult(tableInfo, requestContext, e.Message); + } }); } + private async Task SendGeneratePreviewReportResult(TableInfo tableInfo, RequestContext requestContext, + string schemaValidationError = null) + { + var table = this.GetTableDesigner(tableInfo); + var report = table.GenerateReport(); + var generatePreviewReportResult = new GeneratePreviewReportResult(); + generatePreviewReportResult.Report = report; + generatePreviewReportResult.MimeType = "text/markdown"; + generatePreviewReportResult.Metadata = this.GetMetadata(tableInfo); + generatePreviewReportResult.SchemaValidationError = schemaValidationError; + await requestContext.SendResult(generatePreviewReportResult); + } + private Task HandleDisposeTableDesignerRequest(TableInfo tableInfo, RequestContext requestContext) { return this.HandleRequest(requestContext, async () => diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs index 2abc42fc..172f9d9f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.Data.Tools.Sql.DesignServices.TableDesigner; +using Dac = Microsoft.Data.Tools.Sql.DesignServices.TableDesigner; using TableDesignerIssue = Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts.TableDesignerIssue; using System.Linq; namespace Microsoft.SqlTools.ServiceLayer.TableDesigner @@ -35,12 +36,12 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner /// /// Validate the table and return the validation errors. /// - public static List Validate(TableViewModel table) + public static List Validate(Dac.TableDesigner designer) { var errors = new List(); foreach (var rule in Rules) { - errors.AddRange(rule.Run(table)); + errors.AddRange(rule.Run(designer)); } return errors; } @@ -48,13 +49,14 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public interface ITableDesignerValidationRule { - List Run(TableViewModel table); + List Run(Dac.TableDesigner designer); } public class IndexMustHaveColumnsRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.Indexes.Items.Count; i++) { @@ -74,8 +76,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class ForeignKeyMustHaveColumnsRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.ForeignKeys.Items.Count; i++) { @@ -95,8 +98,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class ColumnCanOnlyAppearOnceInIndexRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.Indexes.Items.Count; i++) { @@ -125,8 +129,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class ColumnCanOnlyAppearOnceInForeignKeyRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.ForeignKeys.Items.Count; i++) { @@ -173,58 +178,71 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class NoDuplicateConstraintNameRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { var errors = new List(); + var table = designer.TableViewModel; var existingNames = new HashSet(); + Dictionary currentSchemaConstraints = designer.AllConstraintsNamesCounts; for (int i = 0; i < table.ForeignKeys.Items.Count; i++) { var foreignKey = table.ForeignKeys.Items[i]; - if (existingNames.Contains(foreignKey.Name)) + if (currentSchemaConstraints.ContainsKey(foreignKey.Name) && currentSchemaConstraints[foreignKey.Name] > 1) { - errors.Add(new TableDesignerIssue() + if (existingNames.Contains(foreignKey.Name)) { - Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", foreignKey.Name, i + 1), - PropertyPath = new object[] { TablePropertyNames.ForeignKeys, i, ForeignKeyPropertyNames.Name } - }); - } - else - { - existingNames.Add(foreignKey.Name); + errors.Add(new TableDesignerIssue() + { + Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", foreignKey.Name, i + 1), + PropertyPath = new object[] { TablePropertyNames.ForeignKeys, i, ForeignKeyPropertyNames.Name } + }); + } + else + { + existingNames.Add(foreignKey.Name); + } + } } for (int i = 0; i < table.CheckConstraints.Items.Count; i++) { var checkConstraint = table.CheckConstraints.Items[i]; - if (existingNames.Contains(checkConstraint.Name)) + if (currentSchemaConstraints.ContainsKey(checkConstraint.Name) && currentSchemaConstraints[checkConstraint.Name] > 1) { - errors.Add(new TableDesignerIssue() + if (existingNames.Contains(checkConstraint.Name)) { - Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", checkConstraint.Name, i + 1), - PropertyPath = new object[] { TablePropertyNames.CheckConstraints, i, CheckConstraintPropertyNames.Name } - }); - } - else - { - existingNames.Add(checkConstraint.Name); + errors.Add(new TableDesignerIssue() + { + Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", checkConstraint.Name, i + 1), + PropertyPath = new object[] { TablePropertyNames.CheckConstraints, i, CheckConstraintPropertyNames.Name } + }); + } + else + { + existingNames.Add(checkConstraint.Name); + } } } for (int i = 0; i < table.EdgeConstraints.Items.Count; i++) { var edgeConstraint = table.EdgeConstraints.Items[i]; - if (existingNames.Contains(edgeConstraint.Name)) + if (currentSchemaConstraints.ContainsKey(edgeConstraint.Name) && currentSchemaConstraints[edgeConstraint.Name] > 1) { - errors.Add(new TableDesignerIssue() + if (existingNames.Contains(edgeConstraint.Name)) { - Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", edgeConstraint.Name, i + 1), - PropertyPath = new object[] { TablePropertyNames.EdgeConstraints, i, EdgeConstraintPropertyNames.Name } - }); - } - else - { - existingNames.Add(edgeConstraint.Name); + errors.Add(new TableDesignerIssue() + { + Description = string.Format("The name '{0}' is already used by another constraint. Row number: {1}.", edgeConstraint.Name, i + 1), + PropertyPath = new object[] { TablePropertyNames.EdgeConstraints, i, EdgeConstraintPropertyNames.Name } + }); + } + else + { + existingNames.Add(edgeConstraint.Name); + } + } } return errors; @@ -233,8 +251,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class NoDuplicateColumnNameRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); var existingNames = new HashSet(); for (int i = 0; i < table.Columns.Items.Count; i++) @@ -259,8 +278,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class NoDuplicateIndexNameRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); var existingNames = new HashSet(); for (int i = 0; i < table.Indexes.Items.Count; i++) @@ -285,8 +305,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class EdgeConstraintMustHaveClausesRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.EdgeConstraints.Items.Count; i++) { @@ -306,8 +327,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class EdgeConstraintNoRepeatingClausesRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.EdgeConstraints.Items.Count; i++) { @@ -337,8 +359,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class MemoryOptimizedTableMustHaveNonClusteredPrimaryKeyRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.IsMemoryOptimized && (table.PrimaryKey == null || table.PrimaryKey.IsClustered)) { @@ -354,8 +377,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class TemporalTableMustHavePrimaryKeyRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.SystemVersioningHistoryTable != null && table.PrimaryKey == null) { @@ -370,8 +394,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class TemporalTableMustHavePeriodColumns : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.SystemVersioningHistoryTable != null && !table.PeriodColumnsDefined) { @@ -386,8 +411,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class PeriodColumnsRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); var rowStart = table.Columns.Items.Where(c => c.GeneratedAlwaysAs == ColumnGeneratedAlwaysAsType.GeneratedAlwaysAsRowStart); var rowEnd = table.Columns.Items.Where(c => c.GeneratedAlwaysAs == ColumnGeneratedAlwaysAsType.GeneratedAlwaysAsRowEnd); @@ -411,8 +437,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class ColumnsInPrimaryKeyCannotBeNullableRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); for (int i = 0; i < table.Columns.Items.Count; i++) { @@ -432,8 +459,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class OnlyDurableMemoryOptimizedTableCanBeSystemVersionedRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.Durability == TableDurability.SchemaOnly && table.IsMemoryOptimized && table.IsSystemVersioningEnabled) { @@ -448,8 +476,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class TableMustHaveAtLeastOneColumnRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (!table.IsEdge && table.Columns.Items.Count == 0) { @@ -464,8 +493,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class MemoryOptimizedTableIdentityColumnRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.IsMemoryOptimized) { @@ -489,8 +519,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner public class TableShouldAvoidHavingMultipleEdgeConstraintsRule : ITableDesignerValidationRule { - public List Run(TableViewModel table) + public List Run(Dac.TableDesigner designer) { + var table = designer.TableViewModel; var errors = new List(); if (table.EdgeConstraints.Items.Count > 1) {