From 2542df35026d15dfee7090573a2075fe2fef5f24 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Thu, 17 Feb 2022 11:12:27 -0800 Subject: [PATCH] add validation for table designer (#1411) * add validation for table designer * comments --- .../ProcessTableDesignerEditRequest.cs | 2 +- .../Contracts/TableDesignerValidationError.cs | 4 +- .../TableDesigner/TableDesignerService.cs | 5 +- .../TableDesigner/TableDesignerValidator.cs | 256 ++++++++++++++++++ 4 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs index d1433dc2..4d539d14 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/Requests/ProcessTableDesignerEditRequest.cs @@ -21,7 +21,7 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts public bool IsValid { get; set; } - public TableDesignerValidationError[] errors { get; set; } + public TableDesignerValidationError[] Errors { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/TableDesignerValidationError.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/TableDesignerValidationError.cs index eaeadd8d..9cf37f51 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/TableDesignerValidationError.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/Contracts/TableDesignerValidationError.cs @@ -16,8 +16,8 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts public string Message { get; set; } /// - /// The property associated with the message, could be a string or TableDesignerPropertyIdentifier + /// The property path associated with the message /// - public object Property { get; set; } + public object[] PropertyPath { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs index 05436fc5..f038dc36 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerService.cs @@ -113,10 +113,13 @@ namespace Microsoft.SqlTools.ServiceLayer.TableDesigner default: break; } + var designer = this.GetTableDesigner(requestParams.TableInfo); + var errors = TableDesignerValidator.Validate(designer.TableViewModel); await requestContext.SendResult(new ProcessTableDesignerEditResponse() { ViewModel = this.GetTableViewModel(requestParams.TableInfo), - IsValid = true + IsValid = errors.Count == 0, + Errors = errors.ToArray() }); }); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs new file mode 100644 index 00000000..186362d2 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/TableDesigner/TableDesignerValidator.cs @@ -0,0 +1,256 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.Data.Tools.Sql.DesignServices.TableDesigner; +using ValidationError = Microsoft.SqlTools.ServiceLayer.TableDesigner.Contracts.TableDesignerValidationError; +namespace Microsoft.SqlTools.ServiceLayer.TableDesigner +{ + public static class TableDesignerValidator + { + private static List Rules = new List() { + new IndexMustHaveColumnsRule(), + new ForeignKeyMustHaveColumnsRule(), + new ColumnCanOnlyAppearOnceInForeignKeyRule(), + new ColumnCanOnlyAppearOnceInIndexRule(), + new NoDuplicateColumnNameRule(), + new NoDuplicateConstraintNameRule(), + new NoDuplicateIndexNameRule() + }; + + /// + /// Validate the table and return the validation errors. + /// + public static List Validate(TableViewModel table) + { + var errors = new List(); + foreach (var rule in Rules) + { + errors.AddRange(rule.Run(table)); + } + return errors; + } + } + + public interface ITableDesignerValidationRule + { + List Run(TableViewModel table); + } + + public class IndexMustHaveColumnsRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + for (int i = 0; i < table.Indexes.Items.Count; i++) + { + var index = table.Indexes.Items[i]; + if (index.Columns.Count == 0) + { + errors.Add(new ValidationError() + { + Message = string.Format("Index '{0}' does not have any columns associated with it.", index.Name), + PropertyPath = new object[] { TablePropertyNames.Indexes, i } + }); + } + } + return errors; + } + } + + public class ForeignKeyMustHaveColumnsRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + for (int i = 0; i < table.ForeignKeys.Items.Count; i++) + { + var foreignKey = table.ForeignKeys.Items[i]; + if (foreignKey.Columns.Count == 0) + { + errors.Add(new ValidationError() + { + Message = string.Format("Forgien key '{0}' does not have any column mapping specified.", foreignKey.Name), + PropertyPath = new object[] { TablePropertyNames.ForeignKeys, i } + }); + } + } + return errors; + } + } + + public class ColumnCanOnlyAppearOnceInIndexRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + for (int i = 0; i < table.Indexes.Items.Count; i++) + { + var index = table.Indexes.Items[i]; + var existingColumns = new HashSet(); + for (int j = 0; j < index.Columns.Count; j++) + { + var columnSpec = index.Columns[j]; + if (existingColumns.Contains(columnSpec.Column)) + { + errors.Add(new ValidationError() + { + Message = string.Format("Column with name '{0}' has already been added to the index '{1}'. Row number: {2}.", columnSpec.Column, index.Name, j + 1), + PropertyPath = new object[] { TablePropertyNames.Indexes, i, IndexPropertyNames.Columns, j } + }); + } + else + { + existingColumns.Add(columnSpec.Column); + } + } + } + return errors; + } + } + + public class ColumnCanOnlyAppearOnceInForeignKeyRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + for (int i = 0; i < table.ForeignKeys.Items.Count; i++) + { + var foreignKey = table.ForeignKeys.Items[i]; + var existingColumns = new HashSet(); + for (int j = 0; j < foreignKey.Columns.Count; j++) + { + var column = foreignKey.Columns[j]; + if (existingColumns.Contains(column)) + { + errors.Add(new ValidationError() + { + Message = string.Format("Column with name '{0}' has already been added to the foreign key '{1}'. Row number: {2}.", column, foreignKey.Name, j + 1), + PropertyPath = new object[] { TablePropertyNames.ForeignKeys, i, ForeignKeyPropertyNames.ColumnMapping, j, ForeignKeyColumnMappingPropertyNames.Column } + }); + } + else + { + existingColumns.Add(column); + } + } + + var existingForeignColumns = new HashSet(); + for (int j = 0; j < foreignKey.ForeignColumns.Count; j++) + { + var foreignColumn = foreignKey.ForeignColumns[j]; + if (existingForeignColumns.Contains(foreignColumn)) + { + errors.Add(new ValidationError() + { + Message = string.Format("Foreign column with name '{0}' has already been added to the foreign key '{1}'. Row number: {2}.", foreignColumn, foreignKey.Name, j + 1), + PropertyPath = new object[] { TablePropertyNames.ForeignKeys, i, ForeignKeyPropertyNames.ColumnMapping, j, ForeignKeyColumnMappingPropertyNames.ForeignColumn } + }); + } + else + { + existingForeignColumns.Add(foreignColumn); + } + } + } + return errors; + } + } + + public class NoDuplicateConstraintNameRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + var existingNames = new HashSet(); + for (int i = 0; i < table.ForeignKeys.Items.Count; i++) + { + var foreignKey = table.ForeignKeys.Items[i]; + if (existingNames.Contains(foreignKey.Name)) + { + errors.Add(new ValidationError() + { + Message = 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)) + { + errors.Add(new ValidationError() + { + Message = 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); + } + } + return errors; + } + } + + public class NoDuplicateColumnNameRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + var existingNames = new HashSet(); + for (int i = 0; i < table.Columns.Items.Count; i++) + { + var column = table.Columns.Items[i]; + if (existingNames.Contains(column.Name)) + { + errors.Add(new ValidationError() + { + Message = string.Format("The name '{0}' is already used by another column. Row number: {1}.", column.Name, i + 1), + PropertyPath = new object[] { TablePropertyNames.Columns, i, TableColumnPropertyNames.Name } + }); + } + else + { + existingNames.Add(column.Name); + } + } + return errors; + } + } + + public class NoDuplicateIndexNameRule : ITableDesignerValidationRule + { + public List Run(TableViewModel table) + { + var errors = new List(); + var existingNames = new HashSet(); + for (int i = 0; i < table.Indexes.Items.Count; i++) + { + var index = table.Indexes.Items[i]; + if (existingNames.Contains(index.Name)) + { + errors.Add(new ValidationError() + { + Message = string.Format("The name '{0}' is already used by another index. Row number: {1}.", index.Name, i + 1), + PropertyPath = new object[] { TablePropertyNames.Indexes, i, IndexPropertyNames.Name } + }); + } + else + { + existingNames.Add(index.Name); + } + } + return errors; + } + } +} \ No newline at end of file