From c5a07e05b7ed282a18e4a0af121d2ef5cbb8e0b7 Mon Sep 17 00:00:00 2001 From: Neil Bostrom Date: Sun, 10 Feb 2019 22:19:38 +0000 Subject: [PATCH] Add CHECK constraint support Improved encapsulation for CheckConstraint annotations --- .../CSharpMigrationOperationGenerator.cs | 92 +++++++ .../Metadata/ICheckConstraint.cs | 36 +++ .../Metadata/IRelationalModelAnnotations.cs | 17 ++ .../Metadata/Internal/CheckConstraint.cs | 235 ++++++++++++++++++ .../Metadata/RelationalAnnotationNames.cs | 5 + .../Metadata/RelationalModelAnnotations.cs | 66 ++++- .../IMigrationsAnnotationProvider.cs | 15 ++ .../Internal/MigrationsModelDiffer.cs | 82 +++++- .../Migrations/MigrationBuilder.cs | 53 ++++ .../MigrationsAnnotationProvider.cs | 25 ++ .../Migrations/MigrationsSqlGenerator.cs | 103 ++++++++ .../Operations/Builders/CreateTableBuilder.cs | 25 ++ .../CreateCheckConstraintOperation.cs | 37 +++ .../Operations/CreateTableOperation.cs | 5 + .../DropCheckConstraintOperation.cs | 28 +++ .../Properties/RelationalStrings.Designer.cs | 6 + .../Properties/RelationalStrings.resx | 3 + .../RelationalEntityTypeBuilderExtensions.cs | 26 ++ .../SqliteMigrationsSqlGenerator.cs | 22 ++ .../CSharpMigrationOperationGeneratorTest.cs | 194 +++++++++++++++ .../Design/CSharpMigrationsGeneratorTest.cs | 2 + .../MigrationSqlGeneratorTestBase.cs | 28 +++ .../RelationalBuilderExtensionsTest.cs | 39 ++- .../Internal/MigrationsModelDifferTest.cs | 145 +++++++++++ .../Migrations/MigrationSqlGeneratorTest.cs | 20 ++ .../SqliteMigrationSqlGeneratorTest.cs | 12 + 26 files changed, 1315 insertions(+), 6 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/ICheckConstraint.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs create mode 100644 src/EFCore.Relational/Migrations/Operations/CreateCheckConstraintOperation.cs create mode 100644 src/EFCore.Relational/Migrations/Operations/DropCheckConstraintOperation.cs diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs index 1c9f452e9cb..f41ded5dd1e 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs @@ -384,6 +384,45 @@ protected virtual void Generate([NotNull] AddUniqueConstraintOperation operation } } + /// + /// Generates code for an . + /// + /// The operation. + /// The builder code is added to. + protected virtual void Generate([NotNull] CreateCheckConstraintOperation operation, [NotNull] IndentedStringBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder.AppendLine(".CreateCheckConstraint("); + + using (builder.Indent()) + { + builder + .Append("name: ") + .Append(Code.Literal(operation.Name)); + + if (operation.Schema != null) + { + builder + .AppendLine(",") + .Append("schema: ") + .Append(Code.Literal(operation.Schema)); + } + + builder + .AppendLine(",") + .Append("table: ") + .Append(Code.Literal(operation.Table)) + .AppendLine(",") + .Append("constraintSql: ") + .Append(Code.Literal(operation.ConstraintSql)) + .Append(")"); + + Annotations(operation.GetAnnotations(), builder); + } + } + /// /// Generates code for an . /// @@ -1039,6 +1078,23 @@ protected virtual void Generate([NotNull] CreateTableOperation operation, [NotNu builder.AppendLine(";"); } + foreach (var checkConstraints in operation.CheckConstraints) + { + builder + .Append("table.CheckConstraint(") + .Append(Code.Literal(checkConstraints.Name)) + .Append(", ") + .Append(Code.Literal(checkConstraints.ConstraintSql)) + .Append(")"); + + using (builder.Indent()) + { + Annotations(checkConstraints.GetAnnotations(), builder); + } + + builder.AppendLine(";"); + } + foreach (var foreignKey in operation.ForeignKeys) { builder.AppendLine("table.ForeignKey("); @@ -1380,6 +1436,42 @@ protected virtual void Generate([NotNull] DropUniqueConstraintOperation operatio } } + /// + /// Generates code for a . + /// + /// The operation. + /// The builder code is added to. + protected virtual void Generate([NotNull] DropCheckConstraintOperation operation, [NotNull] IndentedStringBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder.AppendLine(".DropCheckConstraint("); + + using (builder.Indent()) + { + builder + .Append("name: ") + .Append(Code.Literal(operation.Name)); + + if (operation.Schema != null) + { + builder + .AppendLine(",") + .Append("schema: ") + .Append(Code.Literal(operation.Schema)); + } + + builder + .AppendLine(",") + .Append("table: ") + .Append(Code.Literal(operation.Table)) + .Append(")"); + + Annotations(operation.GetAnnotations(), builder); + } + } + /// /// Generates code for a . /// diff --git a/src/EFCore.Relational/Metadata/ICheckConstraint.cs b/src/EFCore.Relational/Metadata/ICheckConstraint.cs new file mode 100644 index 00000000000..d236bb4906f --- /dev/null +++ b/src/EFCore.Relational/Metadata/ICheckConstraint.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a check constraint in the . + /// + public interface ICheckConstraint + { + /// + /// Gets the name of the check constraint in the database. + /// + string Name { get; } + + /// + /// The database table that contains the check constraint. + /// + string Table { get; } + + /// + /// The database table schema that contains the check constraint. + /// + string Schema { get; } + + /// + /// The in which this check constraint is defined. + /// + IModel Model { get; } + + /// + /// Gets the constraint sql used in a check constraint in the database. + /// + string ConstraintSql { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs b/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs index d79a14f8929..dfe400bd909 100644 --- a/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs +++ b/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs @@ -24,6 +24,18 @@ public interface IRelationalModelAnnotations /// ISequence FindSequence([NotNull] string name, [CanBeNull] string schema = null); + /// + /// Finds an with the given name. + /// + /// The check constraint name. + /// The table that contains the check constraint. + /// The table schema that contains the check constraint. + /// + /// The or null if no check constraint with the given name in + /// the given schema was found. + /// + ICheckConstraint FindCheckConstraint([NotNull] string name, [NotNull] string table, [CanBeNull] string schema = null); + /// /// Finds a that is mapped to the method represented by the given . /// @@ -36,6 +48,11 @@ public interface IRelationalModelAnnotations /// IReadOnlyList Sequences { get; } + /// + /// All s contained in the model. + /// + IReadOnlyList CheckConstraints { get; } + /// /// All s contained in the model. /// diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs new file mode 100644 index 00000000000..88fe0300f13 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class CheckConstraint : ICheckConstraint + { + private readonly IModel _model; + private readonly string _annotationName; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CheckConstraint( + [NotNull] IMutableModel model, + [NotNull] string name, + [NotNull] string constraintSql, + [NotNull] string table, + [CanBeNull] string schema = null) + : this(model, GetAnnotationKey(name, table, schema)) + { + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(constraintSql, nameof(constraintSql)); + Check.NotEmpty(table, nameof(table)); + Check.NullButNotEmpty(schema, nameof(schema)); + + SetData( + new CheckContraintData + { + Name = name, + ConstraintSql = constraintSql, + Table = table, + Schema = schema + }); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CheckConstraint([NotNull] IModel model, [NotNull] string annotationName) + { + Check.NotNull(model, nameof(model)); + + _model = model; + _annotationName = annotationName; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static IEnumerable GetCheckConstraints([NotNull] IModel model) + { + Check.NotNull(model, nameof(model)); + + return GetAnnotationsDictionary(model)? + .Select(a => new CheckConstraint(model, a.Key)) ?? Enumerable.Empty(); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static ICheckConstraint FindCheckConstraint([NotNull] IModel model, [NotNull] string name, [NotNull] string table, [CanBeNull] string schema = null) + { + Check.NotNull(model, nameof(model)); + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(table, nameof(table)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var dataDictionary = GetAnnotationsDictionary(model); + var annotationKey = GetAnnotationKey(name, table, schema); + + return dataDictionary?.ContainsKey(annotationKey) == true ? new CheckConstraint(model, annotationKey) : null; + } + + private static string GetAnnotationKey(string name, string table, string schema = null) + => $"{schema}.{table}:{name}"; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Model Model => (Model)_model; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string Name => GetData().Name; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string Table => GetData().Table; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string Schema => GetData().Schema ?? Model.Relational().DefaultSchema; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string ConstraintSql + { + get => GetData().ConstraintSql; + set + { + var data = GetData(); + data.ConstraintSql = value; + SetData(data); + } + } + + private CheckContraintData GetData() + { + var dataDictionary = GetAnnotationsDictionary(Model); + return CheckContraintData.Deserialize(dataDictionary?[_annotationName]); + } + + private void SetData(CheckContraintData data) + { + var dataDictionary = GetAnnotationsDictionary(Model); + if (dataDictionary == null) + { + dataDictionary = new Dictionary(); + Model[RelationalAnnotationNames.CheckConstraints] = dataDictionary; + } + dataDictionary[_annotationName] = data.Serialize(); + } + + internal static Dictionary GetAnnotationsDictionary(IModel model) => + (Dictionary)model[RelationalAnnotationNames.CheckConstraints]; + + IModel ICheckConstraint.Model => _model; + + private class CheckContraintData + { + public string Name { get; set; } + + public string Table { get; set; } + + public string Schema { get; set; } + + public string ConstraintSql { get; set; } + + public string Serialize() + { + var builder = new StringBuilder(); + + EscapeAndQuote(builder, Name); + builder.Append(", "); + EscapeAndQuote(builder, Table); + builder.Append(", "); + EscapeAndQuote(builder, Schema); + builder.Append(", "); + EscapeAndQuote(builder, ConstraintSql); + + return builder.ToString(); + } + + public static CheckContraintData Deserialize([NotNull] string value) + { + Check.NotEmpty(value, nameof(value)); + + try + { + var data = new CheckContraintData(); + + // ReSharper disable PossibleInvalidOperationException + var position = 0; + data.Name = ExtractValue(value, ref position); + data.Table = ExtractValue(value, ref position); + data.Schema = ExtractValue(value, ref position); + data.ConstraintSql = ExtractValue(value, ref position); + // ReSharper restore PossibleInvalidOperationException + + return data; + } + catch (Exception ex) + { + throw new ArgumentException(RelationalStrings.BadCheckConstraintString, ex); + } + } + + private static string ExtractValue(string value, ref int position) + { + position = value.IndexOf('\'', position) + 1; + + var end = value.IndexOf('\'', position); + + while (end + 1 < value.Length + && value[end + 1] == '\'') + { + end = value.IndexOf('\'', end + 2); + } + + var extracted = value.Substring(position, end - position).Replace("''", "'"); + position = end + 1; + + return extracted.Length == 0 ? null : extracted; + } + + private static void EscapeAndQuote(StringBuilder builder, object value) + { + builder.Append("'"); + + if (value != null) + { + builder.Append(value.ToString().Replace("'", "''")); + } + + builder.Append("'"); + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 91b582cffe4..c3d16f6e2f2 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -65,6 +65,11 @@ public static class RelationalAnnotationNames /// public const string SequencePrefix = Prefix + "Sequence:"; + /// + /// The prefix for serialized check constraint annotations. + /// + public const string CheckConstraints = Prefix + "CheckConstraints"; + /// /// The name for discriminator property annotations. /// diff --git a/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs b/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs index 9a49e126d1b..5bc240d6f6d 100644 --- a/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs +++ b/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs @@ -65,7 +65,7 @@ public virtual IMutableSequence FindSequence([NotNull] string name, [CanBeNull] Check.NotEmpty(name, nameof(name)); Check.NullButNotEmpty(schema, nameof(schema)); - var annotationName = BuildAnnotationName(RelationalAnnotationNames.SequencePrefix, name, schema); + var annotationName = BuildSequenceAnnotationName(RelationalAnnotationNames.SequencePrefix, name, schema); return Model[annotationName] == null ? null : new Sequence(Model, annotationName); } @@ -79,9 +79,9 @@ public virtual IMutableSequence FindSequence([NotNull] string name, [CanBeNull] /// The sequence. public virtual IMutableSequence GetOrAddSequence([NotNull] string name, [CanBeNull] string schema = null) => FindSequence(name, schema) - ?? new Sequence((IMutableModel)Model, BuildAnnotationName(RelationalAnnotationNames.SequencePrefix, name, schema), name, schema); + ?? new Sequence((IMutableModel)Model, BuildSequenceAnnotationName(RelationalAnnotationNames.SequencePrefix, name, schema), name, schema); - private static string BuildAnnotationName(string annotationPrefix, string name, string schema) + private static string BuildSequenceAnnotationName(string annotationPrefix, string name, string schema) => annotationPrefix + schema + "." + name; /// @@ -166,5 +166,65 @@ protected virtual bool SetMaxIdentifierLength(int? value) /// All s contained in the model. /// IReadOnlyList IRelationalModelAnnotations.Sequences => Sequences; + + /// + /// All s contained in the model. + /// + public virtual IReadOnlyList CheckConstraints + => CheckConstraint.GetCheckConstraints(Model).ToList(); + + /// + /// Finds an with the given name. + /// + /// The table schema that contains the check constraint. + /// The table that contains the check constraint. + /// The check constraint name. + /// + /// The or null if no check constraint with the given name in + /// the given schema was found. + /// + public virtual ICheckConstraint FindCheckConstraint([NotNull] string name, [NotNull] string table, [CanBeNull] string schema = null) + { + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(table, nameof(table)); + Check.NullButNotEmpty(schema, nameof(schema)); + + return CheckConstraint.FindCheckConstraint(Model, name, table, schema); + } + + /// + /// Either returns the existing with the given name in the given schema + /// or creates a new check constraint with the given name and schema. + /// + /// The table schema that contains the check constraint. + /// The table that contains the check constraint. + /// The check constraint name. + /// The logical constraint sql used in the check constraint. + /// The check constraint. + public virtual ICheckConstraint GetOrAddCheckConstraint( + [NotNull] string name, + [NotNull] string constraintSql, + [NotNull] string table, + [CanBeNull] string schema = null) + => FindCheckConstraint(name, table, schema) + ?? new CheckConstraint((IMutableModel)Model, name, constraintSql, table, schema); + + /// + /// Finds an with the given name. + /// + /// The table schema that contains the check constraint. + /// The table that contains the check constraint. + /// The check constraint name. + /// + /// The or null if no check constraint with the given name in + /// the given schema was found. + /// + ICheckConstraint IRelationalModelAnnotations.FindCheckConstraint([NotNull] string name, [NotNull] string table, [CanBeNull] string schema) + => FindCheckConstraint(name, table, schema); + + /// + /// All s contained in the model. + /// + IReadOnlyList IRelationalModelAnnotations.CheckConstraints => CheckConstraints; } } diff --git a/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs b/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs index dd1b20d4cb4..9977668efa0 100644 --- a/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs +++ b/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs @@ -71,6 +71,13 @@ public interface IMigrationsAnnotationProvider /// The annotations. IEnumerable For([NotNull] ISequence sequence); + /// + /// Gets provider-specific Migrations annotations for the given . + /// + /// The check constraint. + /// The annotations. + IEnumerable For([NotNull] ICheckConstraint checkConstraint); + /// /// Gets provider-specific Migrations annotations for the given /// when it is being removed/altered. @@ -126,5 +133,13 @@ public interface IMigrationsAnnotationProvider /// The sequence. /// The annotations. IEnumerable ForRemove([NotNull] ISequence sequence); + + /// + /// Gets provider-specific Migrations annotations for the given + /// when it is being removed/altered. + /// + /// The check constraint. + /// The annotations. + IEnumerable ForRemove([NotNull] ICheckConstraint sequence); } } diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 856e571d560..20820b593ec 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -39,7 +39,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal /// public class MigrationsModelDiffer : IMigrationsModelDiffer { - private static readonly Type[] _dropOperationTypes = { typeof(DropIndexOperation), typeof(DropPrimaryKeyOperation), typeof(DropSequenceOperation), typeof(DropUniqueConstraintOperation) }; + private static readonly Type[] _dropOperationTypes = { typeof(DropIndexOperation), typeof(DropPrimaryKeyOperation), typeof(DropSequenceOperation), typeof(DropUniqueConstraintOperation), typeof(DropCheckConstraintOperation) }; private static readonly Type[] _alterOperationTypes = { typeof(AddPrimaryKeyOperation), typeof(AddUniqueConstraintOperation), typeof(AlterSequenceOperation) }; @@ -156,6 +156,7 @@ protected virtual IReadOnlyList Sort( var dropTableOperations = new List(); var ensureSchemaOperations = new List(); var createSequenceOperations = new List(); + var createCheckConstraintOperations = new List(); var createTableOperations = new List(); var alterDatabaseOperations = new List(); var alterTableOperations = new List(); @@ -197,6 +198,10 @@ protected virtual IReadOnlyList Sort( { createSequenceOperations.Add(operation); } + else if (type == typeof(CreateCheckConstraintOperation)) + { + createCheckConstraintOperations.Add(operation); + } else if (type == typeof(CreateTableOperation)) { createTableOperations.Add((CreateTableOperation)operation); @@ -326,6 +331,7 @@ protected virtual IReadOnlyList Sort( .Concat(renameOperations) .Concat(alterDatabaseOperations) .Concat(createSequenceOperations) + .Concat(createCheckConstraintOperations) .Concat(alterTableOperations) .Concat(columnOperations) .Concat(computedColumnOperations) @@ -358,6 +364,7 @@ protected virtual IEnumerable Diff( .Concat(Diff(GetSchemas(source), GetSchemas(target), diffContext)) .Concat(Diff(diffContext.GetSourceTables(), diffContext.GetTargetTables(), diffContext)) .Concat(Diff(source.Relational().Sequences, target.Relational().Sequences, diffContext)) + .Concat(Diff(source.Relational().CheckConstraints, target.Relational().CheckConstraints, diffContext)) .Concat( Diff( diffContext.GetSourceTables().SelectMany(s => s.GetForeignKeys()), @@ -424,6 +431,7 @@ protected virtual IEnumerable Add([NotNull] IModel target, [ .Concat(GetSchemas(target).SelectMany(t => Add(t, diffContext))) .Concat(diffContext.GetTargetTables().SelectMany(t => Add(t, diffContext))) .Concat(target.Relational().Sequences.SelectMany(t => Add(t, diffContext))) + .Concat(target.Relational().CheckConstraints.SelectMany(t => Add(t, diffContext))) .Concat(diffContext.GetTargetTables().SelectMany(t => t.GetForeignKeys()).SelectMany(k => Add(k, diffContext))); /// @@ -435,7 +443,8 @@ protected virtual IEnumerable Add([NotNull] IModel target, [ protected virtual IEnumerable Remove([NotNull] IModel source, [NotNull] DiffContext diffContext) => DiffAnnotations(source, null) .Concat(diffContext.GetSourceTables().SelectMany(t => Remove(t, diffContext))) - .Concat(source.Relational().Sequences.SelectMany(t => Remove(t, diffContext))); + .Concat(source.Relational().Sequences.SelectMany(t => Remove(t, diffContext))) + .Concat(source.Relational().CheckConstraints.SelectMany(t => Remove(t, diffContext))); #endregion @@ -1369,6 +1378,75 @@ protected virtual IEnumerable Remove([NotNull] IIndex source #endregion + #region ICheckConstraint + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual IEnumerable Diff( + [NotNull] IEnumerable source, + [NotNull] IEnumerable target, + [NotNull] DiffContext diffContext) + => DiffCollection( + source, + target, + diffContext, + Diff, + Add, + Remove, + (s, t, c) => string.Equals(s.Schema, t.Schema, StringComparison.OrdinalIgnoreCase) + && string.Equals(s.Name, t.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(s.Table, t.Table, StringComparison.OrdinalIgnoreCase) + && string.Equals(s.ConstraintSql, t.ConstraintSql, StringComparison.OrdinalIgnoreCase)); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual IEnumerable Diff( + [NotNull] ICheckConstraint source, [NotNull] ICheckConstraint target, [NotNull] DiffContext diffContext) + => Enumerable.Empty(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual IEnumerable Add([NotNull] ICheckConstraint target, [NotNull] DiffContext diffContext) + { + var operation = new CreateCheckConstraintOperation + { + Schema = target.Schema, + Name = target.Name, + Table = target.Table, + ConstraintSql = target.ConstraintSql + }; + + operation.ConstraintSql = target.ConstraintSql; + operation.AddAnnotations(MigrationsAnnotations.For(target)); + + yield return operation; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual IEnumerable Remove([NotNull] ICheckConstraint source, [NotNull] DiffContext diffContext) + { + var operation = new DropCheckConstraintOperation + { + Schema = source.Schema, + Name = source.Name, + Table = source.Table + }; + operation.AddAnnotations(MigrationsAnnotations.ForRemove(source)); + + yield return operation; + } + + #endregion + #region ISequence /// diff --git a/src/EFCore.Relational/Migrations/MigrationBuilder.cs b/src/EFCore.Relational/Migrations/MigrationBuilder.cs index ca1e737a631..55e18d50617 100644 --- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs +++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs @@ -627,6 +627,34 @@ public virtual OperationBuilder CreateSequence( return new OperationBuilder(operation); } + /// + /// Builds an to create a new check constraint. + /// + /// The check constraint name. + /// The name of the table for the check constraint. + /// The constraint sql for the check constraint. + /// The schema that contains the check constraint, or null to use the default schema. + /// A builder to allow annotations to be added to the operation. + public virtual OperationBuilder CreateCheckConstraint( + [NotNull] string name, + [NotNull] string table, + [NotNull] string constraintSql, + [CanBeNull] string schema = null) + { + Check.NotEmpty(name, nameof(name)); + + var operation = new CreateCheckConstraintOperation + { + Schema = schema, + Name = name, + Table = table, + ConstraintSql = constraintSql + }; + Operations.Add(operation); + + return new OperationBuilder(operation); + } + /// /// Builds an to create a new table. /// @@ -821,6 +849,31 @@ public virtual OperationBuilder DropSequence( return new OperationBuilder(operation); } + /// + /// Builds an to drop an existing check constraint. + /// + /// The name of the check constraint to drop. + /// The name of the table for the check constraint to drop. + /// The schema that contains the check constraint, or null to use the default schema. + /// A builder to allow annotations to be added to the operation. + public virtual OperationBuilder DropCheckConstraint( + [NotNull] string name, + [NotNull] string table, + [CanBeNull] string schema = null) + { + Check.NotEmpty(name, nameof(name)); + + var operation = new DropCheckConstraintOperation + { + Name = name, + Table = table, + Schema = schema + }; + Operations.Add(operation); + + return new OperationBuilder(operation); + } + /// /// Builds an to drop an existing table. /// diff --git a/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs b/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs index 7419a106066..2f4ea41f4e7 100644 --- a/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs +++ b/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs @@ -117,6 +117,18 @@ public MigrationsAnnotationProvider([NotNull] MigrationsAnnotationProviderDepend /// The annotations. public virtual IEnumerable For(ISequence sequence) => Enumerable.Empty(); + /// + /// + /// Gets provider-specific Migrations annotations for the given . + /// + /// + /// The default implementation returns an empty collection. + /// + /// + /// The check constraint. + /// The annotations. + public virtual IEnumerable For(ICheckConstraint checkConstraint) => Enumerable.Empty(); + /// /// /// Gets provider-specific Migrations annotations for the given @@ -207,5 +219,18 @@ public MigrationsAnnotationProvider([NotNull] MigrationsAnnotationProviderDepend /// The sequence. /// The annotations. public virtual IEnumerable ForRemove(ISequence sequence) => Enumerable.Empty(); + + /// + /// + /// Gets provider-specific Migrations annotations for the given + /// when it is being removed/altered. + /// + /// + /// The default implementation returns an empty collection. + /// + /// + /// The check constraint. + /// The annotations. + public virtual IEnumerable ForRemove(ICheckConstraint checkConstraint) => Enumerable.Empty(); } } diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs index c9355258ffb..4b02a862eb1 100644 --- a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs +++ b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs @@ -47,6 +47,7 @@ public class MigrationsSqlGenerator : IMigrationsSqlGenerator { typeof(AlterDatabaseOperation), (g, o, m, b) => g.Generate((AlterDatabaseOperation)o, m, b) }, { typeof(AlterSequenceOperation), (g, o, m, b) => g.Generate((AlterSequenceOperation)o, m, b) }, { typeof(AlterTableOperation), (g, o, m, b) => g.Generate((AlterTableOperation)o, m, b) }, + { typeof(CreateCheckConstraintOperation), (g, o, m, b) => g.Generate((CreateCheckConstraintOperation)o, m, b) }, { typeof(CreateIndexOperation), (g, o, m, b) => g.Generate((CreateIndexOperation)o, m, b) }, { typeof(CreateSequenceOperation), (g, o, m, b) => g.Generate((CreateSequenceOperation)o, m, b) }, { typeof(CreateTableOperation), (g, o, m, b) => g.Generate((CreateTableOperation)o, m, b) }, @@ -58,6 +59,7 @@ public class MigrationsSqlGenerator : IMigrationsSqlGenerator { typeof(DropSequenceOperation), (g, o, m, b) => g.Generate((DropSequenceOperation)o, m, b) }, { typeof(DropTableOperation), (g, o, m, b) => g.Generate((DropTableOperation)o, m, b) }, { typeof(DropUniqueConstraintOperation), (g, o, m, b) => g.Generate((DropUniqueConstraintOperation)o, m, b) }, + { typeof(DropCheckConstraintOperation), (g, o, m, b) => g.Generate((DropCheckConstraintOperation)o, m, b) }, { typeof(EnsureSchemaOperation), (g, o, m, b) => g.Generate((EnsureSchemaOperation)o, m, b) }, { typeof(RenameColumnOperation), (g, o, m, b) => g.Generate((RenameColumnOperation)o, m, b) }, { typeof(RenameIndexOperation), (g, o, m, b) => g.Generate((RenameIndexOperation)o, m, b) }, @@ -309,6 +311,30 @@ protected virtual void Generate( EndStatement(builder); } + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + [NotNull] CreateCheckConstraintOperation operation, + [CanBeNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" ADD "); + CheckConstraint(operation, model, builder); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + EndStatement(builder); + } + /// /// /// Can be overridden by database providers to build commands for the given @@ -893,6 +919,31 @@ protected virtual void Generate( EndStatement(builder); } + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + [NotNull] DropCheckConstraintOperation operation, + [CanBeNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" DROP CONSTRAINT ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + EndStatement(builder); + } + /// /// /// Can be overridden by database providers to build commands for the given @@ -1365,6 +1416,7 @@ protected virtual void CreateTableConstraints( CreateTablePrimaryKeyContstraint(operation, model, builder); CreateTableUniqueConstraints(operation, model, builder); + CreateTableCheckConstraints(operation, model, builder); CreateTableForeignKeys(operation, model, builder); } @@ -1544,6 +1596,57 @@ protected virtual void UniqueConstraint( .Append(")"); } + /// + /// Generates a SQL fragment for the check constraints of a . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected virtual void CreateTableCheckConstraints( + [NotNull] CreateTableOperation operation, + [CanBeNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + foreach (var checkConstraint in operation.CheckConstraints) + { + builder.AppendLine(","); + CheckConstraint(checkConstraint, model, builder); + } + } + + /// + /// Generates a SQL fragment for a check constraint of an . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected virtual void CheckConstraint( + [NotNull] CreateCheckConstraintOperation operation, + [CanBeNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.Name != null) + { + builder + .Append("CONSTRAINT ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" "); + } + + builder + .Append("CHECK "); + + builder.Append("(") + .Append(operation.ConstraintSql) + .Append(")"); + } + /// /// Generates a SQL fragment for traits of an index from a , /// , or . diff --git a/src/EFCore.Relational/Migrations/Operations/Builders/CreateTableBuilder.cs b/src/EFCore.Relational/Migrations/Operations/Builders/CreateTableBuilder.cs index 0476d15e829..2ea806e22f7 100644 --- a/src/EFCore.Relational/Migrations/Operations/Builders/CreateTableBuilder.cs +++ b/src/EFCore.Relational/Migrations/Operations/Builders/CreateTableBuilder.cs @@ -157,6 +157,31 @@ public virtual OperationBuilder UniqueConstraint( return new OperationBuilder(operation); } + /// + /// Configures a check constraint on the table. + /// + /// The constraint name. + /// The sql expression used in the CHECK constraint. + /// The same builder so that multiple calls can be chained. + public virtual OperationBuilder CheckConstraint( + [NotNull] string name, + [NotNull] string constraintSql) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(constraintSql, nameof(constraintSql)); + + var operation = new CreateCheckConstraintOperation + { + Schema = Operation.Schema, + Table = Operation.Name, + Name = name, + ConstraintSql = constraintSql + }; + Operation.CheckConstraints.Add(operation); + + return new OperationBuilder(operation); + } + /// /// Annotates the operation with the given name/value pair. /// diff --git a/src/EFCore.Relational/Migrations/Operations/CreateCheckConstraintOperation.cs b/src/EFCore.Relational/Migrations/Operations/CreateCheckConstraintOperation.cs new file mode 100644 index 00000000000..686297c64d8 --- /dev/null +++ b/src/EFCore.Relational/Migrations/Operations/CreateCheckConstraintOperation.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Migrations.Operations +{ + /// + /// A for creating a new check constraint. + /// + public class CreateCheckConstraintOperation : MigrationOperation + { + /// + /// The name of the check constraint. + /// + public virtual string Name { get; [param: NotNull] set; } + + /// + /// The table of the check constraint. + /// + public virtual string Table { get; [param: NotNull] set; } + + /// + /// The table schema that contains the check constraint, or null if the default schema should be used. + /// + public virtual string Schema { get; [param: CanBeNull] set; } + + /// + /// The logical sql expression used in a CHECK constraint and returns TRUE or FALSE. + /// Sql used with CHECK constraints cannot reference another table + /// but can reference other columns in the same table for the same row. + /// The expression cannot reference an alias data type. + /// + public virtual string ConstraintSql { get; [param: NotNull] set; } + } +} diff --git a/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs b/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs index bb1413b1312..0b17f09789a 100644 --- a/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs @@ -40,5 +40,10 @@ public class CreateTableOperation : MigrationOperation /// A list of for creating unique constraints in the table. /// public virtual List UniqueConstraints { get; } = new List(); + + /// + /// A list of for creating check constraints in the table. + /// + public virtual List CheckConstraints { get; } = new List(); } } diff --git a/src/EFCore.Relational/Migrations/Operations/DropCheckConstraintOperation.cs b/src/EFCore.Relational/Migrations/Operations/DropCheckConstraintOperation.cs new file mode 100644 index 00000000000..a8da9ade91b --- /dev/null +++ b/src/EFCore.Relational/Migrations/Operations/DropCheckConstraintOperation.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Migrations.Operations +{ + /// + /// A for dropping an existing check constraint. + /// + public class DropCheckConstraintOperation : MigrationOperation + { + /// + /// The name of the constraint. + /// + public virtual string Name { get; [param: NotNull] set; } + + /// + /// The schema that contains the table, or null if the default schema should be used. + /// + public virtual string Schema { get; [param: CanBeNull] set; } + + /// + /// The table that contains the constraint. + /// + public virtual string Table { get; [param: NotNull] set; } + } +} diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 955825e8f18..a19b657d637 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -116,6 +116,12 @@ public static string BadSequenceType public static string BadSequenceString => GetString("BadSequenceString"); + /// + /// Unable to deserialize check constraint from model metadata. See inner exception for details. + /// + public static string BadCheckConstraintString + => GetString("BadCheckConstraintString"); + /// /// The migration '{migrationName}' was not found. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index cb5f22438c6..4bd97c54adf 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -211,6 +211,9 @@ Unable to deserialize sequence from model metadata. See inner exception for details. + + Unable to deserialize check constraint from model metadata. See inner exception for details. + The migration '{migrationName}' was not found. diff --git a/src/EFCore.Relational/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/RelationalEntityTypeBuilderExtensions.cs index 0f56c1ddd56..eb77ca1e9c7 100644 --- a/src/EFCore.Relational/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/RelationalEntityTypeBuilderExtensions.cs @@ -244,5 +244,31 @@ public static DiscriminatorBuilder HasDiscriminator() .Relational(ConfigurationSource.Explicit).HasDiscriminator(propertyExpression.GetPropertyAccess())); } + + /// + /// Configures a database check constraint when targeting a relational database. + /// + /// The entity type builder. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// A builder to further configure the check constraint. + public static EntityTypeBuilder HasCheckConstraint( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [NotNull] string name, + [NotNull] string constraintSql) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + Check.NotEmpty(name, nameof(name)); + + var relaionalEntityTypeBuilder = entityTypeBuilder.GetInfrastructure() + .Relational(ConfigurationSource.Explicit); + + var tableName = relaionalEntityTypeBuilder.TableName; + var schema = relaionalEntityTypeBuilder.Schema; + + entityTypeBuilder.Metadata.Model.Relational().GetOrAddCheckConstraint(name, constraintSql, tableName, schema); + + return entityTypeBuilder; + } } } diff --git a/src/EFCore.Sqlite.Core/Migrations/SqliteMigrationsSqlGenerator.cs b/src/EFCore.Sqlite.Core/Migrations/SqliteMigrationsSqlGenerator.cs index 75f90aac9c6..ad52dea9462 100644 --- a/src/EFCore.Sqlite.Core/Migrations/SqliteMigrationsSqlGenerator.cs +++ b/src/EFCore.Sqlite.Core/Migrations/SqliteMigrationsSqlGenerator.cs @@ -424,6 +424,17 @@ protected override void Generate(AddUniqueConstraintOperation operation, IModel => throw new NotSupportedException( SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName())); + /// + /// Throws since this operation requires table rebuilds, which + /// are not yet supported. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(CreateCheckConstraintOperation operation, IModel model, MigrationCommandListBuilder builder) + => throw new NotSupportedException( + SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName())); + /// /// Throws since this operation requires table rebuilds, which /// are not yet supported. @@ -468,6 +479,17 @@ protected override void Generate(DropUniqueConstraintOperation operation, IModel => throw new NotSupportedException( SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName())); + /// + /// Throws since this operation requires table rebuilds, which + /// are not yet supported. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(DropCheckConstraintOperation operation, IModel model, MigrationCommandListBuilder builder) + => throw new NotSupportedException( + SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName())); + /// /// Throws since this operation requires table rebuilds, which /// are not yet supported. diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs index 53e238ce47f..e1741662ea7 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs @@ -403,6 +403,53 @@ public void AddUniqueConstraint_composite() }); } + [Fact] + public void CreateCheckConstraint_required_args() + { + Test( + new CreateCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Table = "Post", + ConstraintSql = "AltId1 > AltId2" + }, + "mb.CreateCheckConstraint(" + _eol + + " name: \"CK_Post_AltId1_AltId2\"," + _eol + + " table: \"Post\"," + _eol + + " constraintSql: \"AltId1 > AltId2\");", + o => + { + Assert.Equal("CK_Post_AltId1_AltId2", o.Name); + Assert.Equal("Post", o.Table); + Assert.Equal("AltId1 > AltId2", o.ConstraintSql); + }); + } + + [Fact] + public void CreateCheckConstraint_all_args() + { + Test( + new CreateCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Schema = "dbo", + Table = "Post", + ConstraintSql = "AltId1 > AltId2" + }, + "mb.CreateCheckConstraint(" + _eol + + " name: \"CK_Post_AltId1_AltId2\"," + _eol + + " schema: \"dbo\"," + _eol + + " table: \"Post\"," + _eol + + " constraintSql: \"AltId1 > AltId2\");", + o => + { + Assert.Equal("CK_Post_AltId1_AltId2", o.Name); + Assert.Equal("dbo", o.Schema); + Assert.Equal("Post", o.Table); + Assert.Equal("AltId1 > AltId2", o.ConstraintSql); + }); + } + [Fact] public void AlterColumnOperation_required_args() { @@ -1541,6 +1588,112 @@ public void CreateTableOperation_UniqueConstraints_composite() }); } + [Fact] + public void CreateTableOperation_CheckConstraints_required_args() + { + Test( + new CreateTableOperation + { + Name = "Post", + Columns = + { + new AddColumnOperation + { + Name = "AltId1", + ClrType = typeof(int) + }, + new AddColumnOperation + { + Name = "AltId2", + ClrType = typeof(int) + } + }, + CheckConstraints = + { + new CreateCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Table = "Post", + ConstraintSql = "AltId1 > AltId2" + } + } + }, + "mb.CreateTable(" + _eol + + " name: \"Post\"," + _eol + + " columns: table => new" + _eol + + " {" + _eol + + " AltId1 = table.Column(nullable: false)," + _eol + + " AltId2 = table.Column(nullable: false)" + _eol + + " }," + _eol + + " constraints: table =>" + _eol + + " {" + _eol + + " table.CheckConstraint(\"CK_Post_AltId1_AltId2\", \"AltId1 > AltId2\");" + _eol + + " });", + o => + { + Assert.Equal(1, o.CheckConstraints.Count); + + Assert.Equal("CK_Post_AltId1_AltId2", o.CheckConstraints[0].Name); + Assert.Equal("Post", o.CheckConstraints[0].Table); + Assert.Equal("AltId1 > AltId2", o.CheckConstraints[0].ConstraintSql); + }); + } + + [Fact] + public void CreateTableOperation_ChecksConstraints_all_args() + { + Test( + new CreateTableOperation + { + Name = "Post", + Schema = "dbo", + Columns = + { + new AddColumnOperation + { + Name = "AltId1", + ClrType = typeof(int) + }, + new AddColumnOperation + { + Name = "AltId2", + ClrType = typeof(int) + } + }, + CheckConstraints = + { + new CreateCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Schema = "dbo", + Table = "Post", + ConstraintSql = "AltId1 > AltId2" + } + } + }, + "mb.CreateTable(" + _eol + + " name: \"Post\"," + _eol + + " schema: \"dbo\"," + _eol + + " columns: table => new" + _eol + + " {" + _eol + + " AltId1 = table.Column(nullable: false)," + _eol + + " AltId2 = table.Column(nullable: false)" + _eol + + " }," + _eol + + " constraints: table =>" + _eol + + " {" + _eol + + " table.CheckConstraint(\"CK_Post_AltId1_AltId2\", \"AltId1 > AltId2\");" + _eol + + " });", + o => + { + Assert.Equal(1, o.CheckConstraints.Count); + + Assert.Equal("CK_Post_AltId1_AltId2", o.CheckConstraints[0].Name); + Assert.Equal("dbo", o.CheckConstraints[0].Schema); + Assert.Equal("Post", o.CheckConstraints[0].Table); + Assert.Equal("AltId1 > AltId2", o.CheckConstraints[0].ConstraintSql); + }); + } + [Fact] public void DropColumnOperation_required_args() { @@ -1823,6 +1976,47 @@ public void DropUniqueConstraintOperation_all_args() }); } + [Fact] + public void DropCheckConstraintOperation_required_args() + { + Test( + new DropCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Table = "Post" + }, + "mb.DropCheckConstraint(" + _eol + + " name: \"CK_Post_AltId1_AltId2\"," + _eol + + " table: \"Post\");", + o => + { + Assert.Equal("CK_Post_AltId1_AltId2", o.Name); + Assert.Equal("Post", o.Table); + }); + } + + [Fact] + public void DropCheckConstraintOperation_all_args() + { + Test( + new DropCheckConstraintOperation + { + Name = "CK_Post_AltId1_AltId2", + Schema = "dbo", + Table = "Post" + }, + "mb.DropCheckConstraint(" + _eol + + " name: \"CK_Post_AltId1_AltId2\"," + _eol + + " schema: \"dbo\"," + _eol + + " table: \"Post\");", + o => + { + Assert.Equal("CK_Post_AltId1_AltId2", o.Name); + Assert.Equal("dbo", o.Schema); + Assert.Equal("Post", o.Table); + }); + } + [Fact] public void RenameColumnOperation_required_args() { diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 5651d13378a..0d9357970e0 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -66,6 +66,7 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.DefaultValue, RelationalAnnotationNames.Name, RelationalAnnotationNames.SequencePrefix, + RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.DefaultSchema, RelationalAnnotationNames.Filter, RelationalAnnotationNames.DbFunction, @@ -128,6 +129,7 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.DefaultSchema, RelationalAnnotationNames.Name, RelationalAnnotationNames.SequencePrefix, + RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.DiscriminatorProperty, RelationalAnnotationNames.DiscriminatorValue, RelationalAnnotationNames.Filter, diff --git a/test/EFCore.Relational.Specification.Tests/MigrationSqlGeneratorTestBase.cs b/test/EFCore.Relational.Specification.Tests/MigrationSqlGeneratorTestBase.cs index b0b89200b74..d812c283a2d 100644 --- a/test/EFCore.Relational.Specification.Tests/MigrationSqlGeneratorTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/MigrationSqlGeneratorTestBase.cs @@ -320,6 +320,17 @@ public virtual void AddUniqueConstraintOperation_without_name() Columns = new[] { "SSN" } }); + [Fact] + public virtual void CreateCheckConstraintOperation_with_name() + => Generate( + new CreateCheckConstraintOperation + { + Table = "People", + Schema = "dbo", + Name = "CK_People_DriverLicense", + ConstraintSql = "DriverLicense_Number > 0" + }); + [Fact] public virtual void AlterColumnOperation() => Generate( @@ -507,6 +518,13 @@ public virtual void CreateTableOperation() Columns = new[] { "SSN" } } }, + CheckConstraints = + { + new CreateCheckConstraintOperation + { + ConstraintSql = "SSN > 0" + } + }, ForeignKeys = { new AddForeignKeyOperation @@ -586,6 +604,16 @@ public virtual void DropUniqueConstraintOperation() Name = "AK_People_SSN" }); + [Fact] + public virtual void DropCheckConstraintOperation() + => Generate( + new DropCheckConstraintOperation + { + Table = "People", + Schema = "dbo", + Name = "CK_People_SSN" + }); + [Fact] public virtual void SqlOperation() => Generate( diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 8b2469c8842..c6d969195ad 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -545,6 +545,43 @@ public void Can_set_table_and_schema_name_non_generic() Assert.Equal("db0", entityType.Relational().Schema); } + [Fact] + public void Can_create_check_constraint() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .HasCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + + var checkConstraint = modelBuilder.Model.Relational().FindCheckConstraint("CK_Customer_AlternateId", "Customer"); + + Assert.NotNull(checkConstraint); + Assert.Equal("CK_Customer_AlternateId", checkConstraint.Name); + Assert.Equal("AlternateId > Id", checkConstraint.ConstraintSql); + Assert.Equal("Customer", checkConstraint.Table); + Assert.Equal(null, checkConstraint.Schema); + } + + [Fact] + public void Can_create_check_constraint_with_schema_and_none_default_table_name() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .ToTable("Customizer", "db0") + .HasCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + + var checkConstraint = modelBuilder.Model.Relational().FindCheckConstraint("CK_Customer_AlternateId", "Customizer", "db0"); + + Assert.NotNull(checkConstraint); + Assert.Equal("CK_Customer_AlternateId", checkConstraint.Name); + Assert.Equal("AlternateId > Id", checkConstraint.ConstraintSql); + Assert.Equal("Customizer", checkConstraint.Table); + Assert.Equal("db0", checkConstraint.Schema); + } + [Fact] public void Can_set_discriminator_value_using_property_expression() { @@ -926,7 +963,7 @@ public void Can_create_named_sequence_with_specific_facets_using_nested_closure_ ValidateNamedSpecificSequence(sequence); } - + private static void ValidateNamedSpecificSequence(ISequence sequence) { Assert.Equal("Snook", sequence.Name); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 3e3990a5d33..5e40aa3607d 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -255,6 +255,7 @@ public void Create_table() Assert.Null(createTableOperation.Columns.First(o => o.Name == "AltId").DefaultValue); Assert.NotNull(createTableOperation.PrimaryKey); Assert.Equal(1, createTableOperation.UniqueConstraints.Count); + Assert.Equal(0, createTableOperation.CheckConstraints.Count); Assert.Equal(1, createTableOperation.ForeignKeys.Count); Assert.IsType(upOps[2]); @@ -723,6 +724,7 @@ public void Create_shared_table_with_two_types() Assert.Equal(new[] { "Id", "MouseId", "BoneId" }, createTableOperation.Columns.Select(c => c.Name)); Assert.Equal(0, createTableOperation.ForeignKeys.Count); Assert.Equal(0, createTableOperation.UniqueConstraints.Count); + Assert.Equal(0, createTableOperation.CheckConstraints.Count); }, downOps => { @@ -2279,6 +2281,149 @@ public void Alter_unique_constraint_columns() }); } + [Fact] + public void Add_check_constraint() + { + Execute( + source => source.Entity( + "Flamingo", + x => + { + x.ToTable("Flamingo", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + }), + target => target.Entity( + "Flamingo", + x => + { + x.ToTable("Flamingo", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + }), + operations => + { + Assert.Equal(1, operations.Count); + + var operation = Assert.IsType(operations[0]); + Assert.Equal("dbo", operation.Schema); + Assert.Equal("Flamingo", operation.Table); + Assert.Equal("CK_Flamingo_AlternateId", operation.Name); + Assert.Equal("AlternateId > Id", operation.ConstraintSql); + }); + } + + [Fact] + public void Drop_check_constraint() + { + Execute( + source => source.Entity( + "Penguin", + x => + { + x.ToTable("Penguin", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + }), + target => target.Entity( + "Penguin", + x => + { + x.ToTable("Penguin", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + }), + operations => + { + Assert.Equal(1, operations.Count); + + var operation = Assert.IsType(operations[0]); + Assert.Equal("dbo", operation.Schema); + Assert.Equal("Penguin", operation.Table); + Assert.Equal("CK_Flamingo_AlternateId", operation.Name); + }); + } + + [Fact] + public void Rename_check_constraint() + { + Execute( + source => source.Entity( + "Pelican", + x => + { + x.ToTable("Pelican", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + }), + target => target.Entity( + "Pelican", + x => + { + x.ToTable("Pelican", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo", "AlternateId > Id"); + }), + operations => + { + Assert.Equal(2, operations.Count); + + var dropOperation = Assert.IsType(operations[0]); + Assert.Equal("dbo", dropOperation.Schema); + Assert.Equal("Pelican", dropOperation.Table); + Assert.Equal("CK_Flamingo_AlternateId", dropOperation.Name); + + var createOperation = Assert.IsType(operations[1]); + Assert.Equal("dbo", createOperation.Schema); + Assert.Equal("Pelican", createOperation.Table); + Assert.Equal("CK_Flamingo", createOperation.Name); + Assert.Equal("AlternateId > Id", createOperation.ConstraintSql); + }); + } + + [Fact] + public void Alter_check_constraint_expression() + { + Execute( + source => source.Entity( + "Rook", + x => + { + x.ToTable("Rook", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + }), + target => target.Entity( + "Rook", + x => + { + x.ToTable("Rook", "dbo"); + x.Property("Id"); + x.Property("AlternateId"); + x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId < Id"); + }), + operations => + { + Assert.Equal(2, operations.Count); + + var dropOperation = Assert.IsType(operations[0]); + Assert.Equal("dbo", dropOperation.Schema); + Assert.Equal("Rook", dropOperation.Table); + Assert.Equal("CK_Flamingo_AlternateId", dropOperation.Name); + + var createOperation = Assert.IsType(operations[1]); + Assert.Equal("dbo", createOperation.Schema); + Assert.Equal("Rook", createOperation.Table); + Assert.Equal("CK_Flamingo_AlternateId", createOperation.Name); + Assert.Equal("AlternateId < Id", createOperation.ConstraintSql); + }); + } + [Fact] public void Rename_primary_key() { diff --git a/test/EFCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs b/test/EFCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs index e61fdbd476d..878b506f49d 100644 --- a/test/EFCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs @@ -128,6 +128,16 @@ public override void AddUniqueConstraintOperation_without_name() Sql); } + public override void CreateCheckConstraintOperation_with_name() + { + base.CreateCheckConstraintOperation_with_name(); + + Assert.Equal( + "ALTER TABLE \"dbo\".\"People\" ADD CONSTRAINT \"CK_People_DriverLicense\" CHECK (DriverLicense_Number > 0);" + + EOL, + Sql); + } + public override void AlterSequenceOperation_with_minValue_and_maxValue() { base.AlterSequenceOperation_with_minValue_and_maxValue(); @@ -212,6 +222,7 @@ public override void CreateTableOperation() " \"SSN\" char(11) NULL," + EOL + " PRIMARY KEY (\"Id\")," + EOL + " UNIQUE (\"SSN\")," + EOL + + " CHECK (SSN > 0)," + EOL + " FOREIGN KEY (\"EmployerId\") REFERENCES \"Companies\" (\"Id\")" + EOL + ");" + EOL, Sql); @@ -271,6 +282,15 @@ public override void DropUniqueConstraintOperation() Sql); } + public override void DropCheckConstraintOperation() + { + base.DropCheckConstraintOperation(); + + Assert.Equal( + "ALTER TABLE \"dbo\".\"People\" DROP CONSTRAINT \"CK_People_SSN\";" + EOL, + Sql); + } + public override void SqlOperation() { base.SqlOperation(); diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteMigrationSqlGeneratorTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteMigrationSqlGeneratorTest.cs index 6f9671e156a..7b6f68055c2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/SqliteMigrationSqlGeneratorTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/SqliteMigrationSqlGeneratorTest.cs @@ -310,6 +310,12 @@ public override void AddUniqueConstraintOperation_without_name() Assert.Equal(SqliteStrings.InvalidMigrationOperation(nameof(AddUniqueConstraintOperation)), ex.Message); } + public override void CreateCheckConstraintOperation_with_name() + { + var ex = Assert.Throws(() => base.CreateCheckConstraintOperation_with_name()); + Assert.Equal(SqliteStrings.InvalidMigrationOperation(nameof(CreateCheckConstraintOperation)), ex.Message); + } + public override void AlterColumnOperation() { var ex = Assert.Throws(() => base.AlterColumnOperation()); @@ -481,6 +487,12 @@ public override void DropUniqueConstraintOperation() Assert.Equal(SqliteStrings.InvalidMigrationOperation(nameof(DropUniqueConstraintOperation)), ex.Message); } + public override void DropCheckConstraintOperation() + { + var ex = Assert.Throws(() => base.DropCheckConstraintOperation()); + Assert.Equal(SqliteStrings.InvalidMigrationOperation(nameof(DropCheckConstraintOperation)), ex.Message); + } + [Fact] public virtual void CreateTableOperation_old_autoincrement_annotation() {