diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index b68799855..3e96d255f 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -405,7 +405,10 @@ protected override void Generate(AlterColumnOperation operation, IModel model, M .AppendLine($"ALTER SEQUENCE {sequence} RENAME TO {oldSequenceWithoutSchema};") .AppendLine($"{alterBase}DROP DEFAULT;") .AppendLine($"{alterBase}ADD GENERATED {identityTypeClause} AS IDENTITY;") - .AppendLine($"SELECT * FROM setval('{sequence}', nextval('{oldSequence}'), false);") + // When generating idempotent scripts, migration DDL is enclosed in anonymous DO blocks, + // where PERFORM must be used instead of SELECT + .Append(Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) ? "PERFORM" : "SELECT") + .AppendLine($" * FROM setval('{sequence}', nextval('{oldSequence}'), false);") .AppendLine($"DROP SEQUENCE {oldSequence};"); break; default: diff --git a/test/EFCore.PG.Tests/Migrations/MigrationSqlGeneratorTestBase.cs b/test/EFCore.PG.Tests/Migrations/MigrationSqlGeneratorTestBase.cs index d145e99d7..902dc4e42 100644 --- a/test/EFCore.PG.Tests/Migrations/MigrationSqlGeneratorTestBase.cs +++ b/test/EFCore.PG.Tests/Migrations/MigrationSqlGeneratorTestBase.cs @@ -5,7 +5,10 @@ using System; using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; @@ -14,16 +17,31 @@ using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; +using NetTopologySuite.Geometries; using Xunit; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations { - public abstract class MigrationSqlGeneratorTestBase + public abstract class MigrationsSqlGeneratorTestBase { - protected static string EOL => Environment.NewLine; + protected static string EOL + => Environment.NewLine; protected virtual string Sql { get; set; } + [ConditionalFact] + public void All_tests_must_be_overriden() + { + var baseTests = GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(method => method.IsVirtual && !method.IsFinal && method.DeclaringType == typeof(MigrationsSqlGeneratorTestBase)) + .ToList(); + + Assert.True( + baseTests.Count == 0, $"{GetType().ShortDisplayName()} should override the following methods to assert the generated SQL:" + + EOL + + string.Join(EOL, baseTests.Select(m => m.Name))); + } + [ConditionalFact] public virtual void AddColumnOperation_without_column_type() => Generate( @@ -98,6 +116,31 @@ public virtual void AddColumnOperation_with_maxLength_no_model() IsNullable = true }); + [ConditionalFact] + public virtual void AddColumnOperation_with_precision_and_scale_overridden() + => Generate( + modelBuilder => modelBuilder.Entity().Property("Pi").HasPrecision(30, 17), + new AddColumnOperation + { + Table = "Person", + Name = "Pi", + ClrType = typeof(decimal), + Precision = 15, + Scale = 10 + }); + + [ConditionalFact] + public virtual void AddColumnOperation_with_precision_and_scale_no_model() + => Generate( + new AddColumnOperation + { + Table = "Person", + Name = "Pi", + ClrType = typeof(decimal), + Precision = 20, + Scale = 7 + }); + [ConditionalFact] public virtual void AddForeignKeyOperation_without_principal_columns() => Generate( @@ -145,6 +188,508 @@ public virtual void SqlOperation() => Generate( new SqlOperation { Sql = "-- I <3 DDL" }); + private static readonly LineString _lineString1 = new LineString( + new[] { new Coordinate(1.1, 2.2), new Coordinate(2.2, 2.2), new Coordinate(2.2, 1.1), new Coordinate(7.1, 7.2) }) + { + SRID = 4326 + }; + + private static readonly LineString _lineString2 = new LineString( + new[] { new Coordinate(7.1, 7.2), new Coordinate(20.2, 20.2), new Coordinate(20.20, 1.1), new Coordinate(70.1, 70.2) }) + { + SRID = 4326 + }; + + private static readonly MultiPoint _multiPoint = new MultiPoint( + new[] { new Point(1.1, 2.2), new Point(2.2, 2.2), new Point(2.2, 1.1) }) { SRID = 4326 }; + + private static readonly Polygon _polygon1 = new Polygon( + new LinearRing( + new[] { new Coordinate(1.1, 2.2), new Coordinate(2.2, 2.2), new Coordinate(2.2, 1.1), new Coordinate(1.1, 2.2) })) + { + SRID = 4326 + }; + + private static readonly Polygon _polygon2 = new Polygon( + new LinearRing( + new[] { new Coordinate(10.1, 20.2), new Coordinate(20.2, 20.2), new Coordinate(20.2, 10.1), new Coordinate(10.1, 20.2) })) + { + SRID = 4326 + }; + + private static readonly Point _point1 = new Point(1.1, 2.2, 3.3) { SRID = 4326 }; + + private static readonly MultiLineString _multiLineString = new MultiLineString( + new[] { _lineString1, _lineString2 }) { SRID = 4326 }; + + private static readonly MultiPolygon _multiPolygon = new MultiPolygon( + new[] { _polygon2, _polygon1 }) { SRID = 4326 }; + + private static readonly GeometryCollection _geometryCollection = new GeometryCollection( + new Geometry[] { _lineString1, _lineString2, _multiPoint, _polygon1, _polygon2, _point1, _multiLineString, _multiPolygon }) + { + SRID = 4326 + }; + + [ConditionalFact] + public virtual void InsertDataOperation_all_args_spatial() + => Generate( + new InsertDataOperation + { + Schema = "dbo", + Table = "People", + Columns = new[] { "Id", "Full Name", "Geometry" }, + ColumnTypes = new[] { "int", "varchar(40)", GetGeometryCollectionStoreType() }, + Values = new object[,] + { + { 0, null, null }, + { 1, "Daenerys Targaryen", null }, + { 2, "John Snow", null }, + { 3, "Arya Stark", null }, + { 4, "Harry Strickland", null }, + { 5, "The Imp", null }, + { 6, "The Kingslayer", null }, + { 7, "Aemon Targaryen", _geometryCollection } + } + }); + + protected abstract string GetGeometryCollectionStoreType(); + + [ConditionalFact] + public virtual void InsertDataOperation_required_args() + => Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Columns = new[] { "First Name" }, + Values = new object[,] { { "John" } } + }); + + [ConditionalFact] + public virtual void InsertDataOperation_required_args_composite() + => Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Columns = new[] { "First Name", "Last Name" }, + Values = new object[,] { { "John", "Snow" } } + }); + + [ConditionalFact] + public virtual void InsertDataOperation_required_args_multiple_rows() + => Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Columns = new[] { "First Name" }, + Values = new object[,] { { "John" }, { "Daenerys" } } + }); + + [ConditionalFact] + public void InsertDataOperation_throws_for_missing_column_types() + => Assert.Equal( + RelationalStrings.InsertDataOperationNoModel("dbo.People"), + Assert.Throws( + () => + Generate( + new InsertDataOperation + { + Table = "People", + Schema = "dbo", + Columns = new[] { "First Name" }, + Values = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public virtual void InsertDataOperation_throws_for_unsupported_column_types() + => Assert.Equal( + RelationalStrings.UnsupportedDataOperationStoreType("char[]", "dbo.People.First Name"), + Assert.Throws( + () => + Generate( + new InsertDataOperation + { + Table = "People", + Schema = "dbo", + Columns = new[] { "First Name" }, + ColumnTypes = new[] { "char[]" }, + Values = new object[,] { { null } } + })).Message); + + [ConditionalFact] + public void InsertDataOperation_throws_for_values_count_mismatch() + => Assert.Equal( + RelationalStrings.InsertDataOperationValuesCountMismatch(1, 2, "People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Columns = new[] { "First Name", "Last Name" }, + Values = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public void InsertDataOperation_throws_for_types_count_mismatch() + => Assert.Equal( + RelationalStrings.InsertDataOperationTypesCountMismatch(2, 1, "People"), + Assert.Throws( + () => + Generate( + new InsertDataOperation + { + Table = "People", + Columns = new[] { "First Name" }, + ColumnTypes = new[] { "string", "string" }, + Values = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public void InsertDataOperation_throws_for_missing_entity_type() + => Assert.Equal( + RelationalStrings.DataOperationNoTable("dbo.People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Schema = "dbo", + Columns = new[] { "First Name" }, + Values = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public void InsertDataOperation_throws_for_missing_property() + => Assert.Equal( + RelationalStrings.DataOperationNoProperty("People", "Name"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new InsertDataOperation + { + Table = "People", + Columns = new[] { "Name" }, + Values = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public virtual void DeleteDataOperation_all_args() + => Generate( + CreateGotModel, + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Hodor" }, { "Daenerys" }, { "John" }, { "Arya" }, { "Harry" } } + }); + + [ConditionalFact] + public virtual void DeleteDataOperation_all_args_composite() + => Generate( + CreateGotModel, + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] + { + { "Hodor", null }, { "Daenerys", "Targaryen" }, { "John", "Snow" }, { "Arya", "Stark" }, { "Harry", "Strickland" } + } + }); + + [ConditionalFact] + public virtual void DeleteDataOperation_required_args() + => Generate( + CreateGotModel, + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "Last Name" }, + KeyValues = new object[,] { { "Snow" } } + }); + + [ConditionalFact] + public virtual void DeleteDataOperation_required_args_composite() + => Generate( + CreateGotModel, + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "John", "Snow" } } + }); + + [ConditionalFact] + public void DeleteDataOperation_throws_for_missing_column_types() + => Assert.Equal( + RelationalStrings.DeleteDataOperationNoModel("People"), + Assert.Throws( + () => + Generate( + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public void DeleteDataOperation_throws_for_values_count_mismatch() + => Assert.Equal( + RelationalStrings.DeleteDataOperationValuesCountMismatch(1, 2, "People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public void DeleteDataOperation_throws_for_types_count_mismatch() + => Assert.Equal( + RelationalStrings.DeleteDataOperationTypesCountMismatch(2, 1, "People"), + Assert.Throws( + () => + Generate( + new DeleteDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyColumnTypes = new[] { "string", "string" }, + KeyValues = new object[,] { { "John" } } + })).Message); + + [ConditionalFact] + public virtual void UpdateDataOperation_all_args() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Hodor" }, { "Daenerys" } }, + Columns = new[] { "Birthplace", "House Allegiance", "Culture" }, + Values = new object[,] { { "Winterfell", "Stark", "Northmen" }, { "Dragonstone", "Targaryen", "Valyrian" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_all_args_composite() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "Hodor", null }, { "Daenerys", "Targaryen" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Stark" }, { "Targaryen" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_all_args_composite_multi() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "Hodor", null }, { "Daenerys", "Targaryen" } }, + Columns = new[] { "Birthplace", "House Allegiance", "Culture" }, + Values = new object[,] { { "Winterfell", "Stark", "Northmen" }, { "Dragonstone", "Targaryen", "Valyrian" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_all_args_multi() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "Birthplace", "House Allegiance", "Culture" }, + Values = new object[,] { { "Dragonstone", "Targaryen", "Valyrian" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_required_args() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_required_args_multiple_rows() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Hodor" }, { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Stark" }, { "Targaryen" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_required_args_composite() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "Daenerys", "Targaryen" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_required_args_composite_multi() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "Daenerys", "Targaryen" } }, + Columns = new[] { "Birthplace", "House Allegiance", "Culture" }, + Values = new object[,] { { "Dragonstone", "Targaryen", "Valyrian" } } + }); + + [ConditionalFact] + public virtual void UpdateDataOperation_required_args_multi() + => Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "Birthplace", "House Allegiance", "Culture" }, + Values = new object[,] { { "Dragonstone", "Targaryen", "Valyrian" } } + }); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_missing_column_types() + => Assert.Equal( + RelationalStrings.UpdateDataOperationNoModel("People"), + Assert.Throws( + () => + Generate( + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_row_count_mismatch() + => Assert.Equal( + RelationalStrings.UpdateDataOperationRowCountMismatch(1, 2, "People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyColumnTypes = new[] { "string" }, + KeyValues = new object[,] { { "Daenerys" }, { "John" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_key_values_count_mismatch() + => Assert.Equal( + RelationalStrings.UpdateDataOperationKeyValuesCountMismatch(1, 2, "People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name", "Last Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_key_types_count_mismatch() + => Assert.Equal( + RelationalStrings.UpdateDataOperationKeyTypesCountMismatch(2, 1, "People"), + Assert.Throws( + () => + Generate( + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyColumnTypes = new[] { "string", "string" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_values_count_mismatch() + => Assert.Equal( + RelationalStrings.UpdateDataOperationValuesCountMismatch(1, 2, "People"), + Assert.Throws( + () => + Generate( + CreateGotModel, + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance", "Culture" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + + [ConditionalFact] + public void UpdateDataOperation_throws_for_types_count_mismatch() + => Assert.Equal( + RelationalStrings.UpdateDataOperationTypesCountMismatch(2, 1, "People"), + Assert.Throws( + () => + Generate( + new UpdateDataOperation + { + Table = "People", + KeyColumns = new[] { "First Name" }, + KeyValues = new object[,] { { "Daenerys" } }, + Columns = new[] { "House Allegiance" }, + ColumnTypes = new[] { "string", "string" }, + Values = new object[,] { { "Targaryen" } } + })).Message); + [ConditionalTheory] [InlineData(false)] [InlineData(true)] @@ -168,31 +713,79 @@ public virtual void DefaultValue_with_line_breaks(bool isUnicode) } }); + private static void CreateGotModel(ModelBuilder b) + { + b.Entity( + "Person", pb => + { + pb.ToTable("People"); + pb.Property("FirstName").HasColumnName("First Name"); + pb.Property("LastName").HasColumnName("Last Name"); + pb.Property("Birthplace").HasColumnName("Birthplace"); + pb.Property("Allegiance").HasColumnName("House Allegiance"); + pb.Property("Culture").HasColumnName("Culture"); + pb.HasKey("FirstName", "LastName"); + }); + } + protected TestHelpers TestHelpers { get; } + protected DbContextOptions ContextOptions { get; } + protected IServiceCollection CustomServices { get; } - protected MigrationSqlGeneratorTestBase(TestHelpers testHelpers) + protected MigrationsSqlGeneratorTestBase( + TestHelpers testHelpers, + IServiceCollection customServices = null, + DbContextOptions options = null) { TestHelpers = testHelpers; + CustomServices = customServices; + ContextOptions = options; } protected virtual void Generate(params MigrationOperation[] operation) - => Generate(_ => { }, operation); + => Generate(null, operation); + + protected virtual void Generate( + Action buildAction, + Action migrateAction, + MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) + { + var migrationBuilder = new MigrationBuilder(activeProvider: null); + migrateAction(migrationBuilder); + + Generate(buildAction, migrationBuilder.Operations.ToArray(), options); + } protected virtual void Generate(Action buildAction, params MigrationOperation[] operation) + => Generate(buildAction, operation, MigrationsSqlGenerationOptions.Default); + + protected virtual void Generate( + Action buildAction, + MigrationOperation[] operation, + MigrationsSqlGenerationOptions options) { - var modelBuilder = TestHelpers.CreateConventionBuilder(); - modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); - buildAction(modelBuilder); + var services = ContextOptions != null + ? TestHelpers.CreateContextServices(CustomServices, ContextOptions) + : TestHelpers.CreateContextServices(CustomServices); + + IModel model = null; + if (buildAction != null) + { + var modelBuilder = TestHelpers.CreateConventionBuilder(); + modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); + buildAction(modelBuilder); + + model = modelBuilder.Model; + var conventionSet = services.GetRequiredService().CreateConventionSet(); - var services = TestHelpers.CreateContextServices(); + var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType().FirstOrDefault(); + typeMappingConvention.ProcessModelFinalizing(((IConventionModel)model).Builder, null); - IModel model = modelBuilder.Model; - var conventionSet = services.GetRequiredService().CreateConventionSet(); - var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType().First(); - model = relationalModelConvention.ProcessModelFinalized((IConventionModel)model); - model = ((IMutableModel)model).FinalizeModel(); + var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType().First(); + model = relationalModelConvention.ProcessModelFinalized((IConventionModel)model); + } - var batch = services.GetRequiredService().Generate(operation, modelBuilder.Model); + var batch = services.GetRequiredService().Generate(operation, model, options); Sql = string.Join( "GO" + EOL + EOL, @@ -206,6 +799,7 @@ protected class Person { public int Id { get; set; } public string Name { get; set; } + public decimal Pi { get; set; } } } } diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationSqlGeneratorTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationSqlGeneratorTest.cs deleted file mode 100644 index 7c8ffb785..000000000 --- a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationSqlGeneratorTest.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using Xunit; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations -{ - public class NpgsqlMigrationSqlGeneratorTest : MigrationSqlGeneratorTestBase - { - #region Database - - [Fact] - public virtual void CreateDatabaseOperation() - { - Generate(new NpgsqlCreateDatabaseOperation { Name = "Northwind" }); - - AssertSql( - @"CREATE DATABASE ""Northwind""; -"); - } - - [ConditionalFact] - public virtual void CreateDatabaseOperation_with_collation() - { - Generate( - new NpgsqlCreateDatabaseOperation { Name = "Northwind", Collation = "POSIX" }); - - AssertSql( - @"CREATE DATABASE ""Northwind"" -LC_COLLATE ""POSIX""; -"); - } - - [Fact] - public virtual void CreateDatabaseOperation_with_template() - { - Generate(new NpgsqlCreateDatabaseOperation - { - Name = "Northwind", - Template = "MyTemplate" - }); - - AssertSql( - @"CREATE DATABASE ""Northwind"" -TEMPLATE ""MyTemplate""; -"); - } - - [Fact] - public virtual void CreateDatabaseOperation_with_tablespace() - { - Generate(new NpgsqlCreateDatabaseOperation - { - Name = "some_db", - Tablespace = "MyTablespace" - }); - - AssertSql( - @"CREATE DATABASE some_db -TABLESPACE ""MyTablespace""; -"); - } - - #endregion - - // Which index collations are available on a given PostgreSQL varies (e.g. Linux vs. Windows) - // so we test support for this on the generated SQL only, and not against the database in MigrationsNpsqlTest. - [Fact] - public void CreateIndexOperation_collation() - { - Generate(new CreateIndexOperation - { - Name = "IX_People_Name", - Table = "People", - Schema = "dbo", - Columns = new[] { "FirstName", "LastName" }, - [RelationalAnnotationNames.Collation] = new[] { null, "de_DE" } - }); - - AssertSql( - @"CREATE INDEX ""IX_People_Name"" ON dbo.""People"" (""FirstName"", ""LastName"" COLLATE ""de_DE""); -"); - } - - #region CockroachDB interleave-in-parent - - // Note that we don't run tests against actual CockroachDB instances, so these are unit tests asserting on SQL - // only - - [Fact] - public void CreateTableOperation_with_cockroach_interleave_in_parent() - { - var op = - new CreateTableOperation - { - Name = "People", - Schema = "dbo", - Columns = - { - new AddColumnOperation - { - Name = "Id", - Table = "People", - Schema = "dbo", - ClrType = typeof(int), - IsNullable = false - }, - }, - PrimaryKey = new AddPrimaryKeyOperation - { - Columns = new[] { "Id" } - } - }; - - var interleaveInParent = new CockroachDbInterleaveInParent(op); - interleaveInParent.ParentTableSchema = "my_schema"; - interleaveInParent.ParentTableName = "my_parent"; - interleaveInParent.InterleavePrefix = new List { "col_a", "col_b" }; - - Generate(op); - - AssertSql( - @"CREATE TABLE dbo.""People"" ( - ""Id"" integer NOT NULL, - PRIMARY KEY (""Id"") -) -INTERLEAVE IN PARENT my_schema.my_parent (col_a, col_b); -"); - } - - #endregion CockroachDB interleave-in-parent - -#pragma warning disable 618 - [Fact] - public virtual void AddColumnOperation_serial_old_annotation_throws() - { - Assert.Throws(() => - Generate(new AddColumnOperation - { - Table = "People", - Name = "foo", - ClrType = typeof(int), - ColumnType = "int", - IsNullable = false, - [NpgsqlAnnotationNames.ValueGeneratedOnAdd] = true - })); - } -#pragma warning restore 618 - - public NpgsqlMigrationSqlGeneratorTest() : base(NpgsqlTestHelpers.Instance) - { - } - } -} diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs new file mode 100644 index 000000000..daa08f842 --- /dev/null +++ b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs @@ -0,0 +1,563 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations +{ + public class NpgsqlMigrationsSqlGeneratorTest : MigrationsSqlGeneratorTestBase + { + #region Database + + [Fact] + public virtual void CreateDatabaseOperation() + { + Generate(new NpgsqlCreateDatabaseOperation { Name = "Northwind" }); + + AssertSql( + @"CREATE DATABASE ""Northwind""; +"); + } + + [ConditionalFact] + public virtual void CreateDatabaseOperation_with_collation() + { + Generate( + new NpgsqlCreateDatabaseOperation { Name = "Northwind", Collation = "POSIX" }); + + AssertSql( + @"CREATE DATABASE ""Northwind"" +LC_COLLATE ""POSIX""; +"); + } + + [Fact] + public virtual void CreateDatabaseOperation_with_template() + { + Generate(new NpgsqlCreateDatabaseOperation + { + Name = "Northwind", + Template = "MyTemplate" + }); + + AssertSql( + @"CREATE DATABASE ""Northwind"" +TEMPLATE ""MyTemplate""; +"); + } + + [Fact] + public virtual void CreateDatabaseOperation_with_tablespace() + { + Generate(new NpgsqlCreateDatabaseOperation + { + Name = "some_db", + Tablespace = "MyTablespace" + }); + + AssertSql( + @"CREATE DATABASE some_db +TABLESPACE ""MyTablespace""; +"); + } + + #endregion + + public override void AddColumnOperation_without_column_type() + { + base.AddColumnOperation_without_column_type(); + + AssertSql( + @"ALTER TABLE ""People"" ADD ""Alias"" text NOT NULL; +"); + } + + public override void AddColumnOperation_with_unicode_overridden() + { + base.AddColumnOperation_with_unicode_overridden(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Name"" text NULL; +"); + } + + public override void AddColumnOperation_with_unicode_no_model() + { + base.AddColumnOperation_with_unicode_no_model(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Name"" text NULL; +"); + } + + public override void AddColumnOperation_with_fixed_length_no_model() + { + base.AddColumnOperation_with_fixed_length_no_model(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Name"" character(100) NULL; +"); + } + + public override void AddColumnOperation_with_maxLength_overridden() + { + base.AddColumnOperation_with_maxLength_overridden(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Name"" character varying(32) NULL; +"); + } + + public override void AddColumnOperation_with_maxLength_no_model() + { + base.AddColumnOperation_with_maxLength_no_model(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Name"" character varying(30) NULL; +"); + } + + public override void AddColumnOperation_with_precision_and_scale_overridden() + { + base.AddColumnOperation_with_precision_and_scale_overridden(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Pi"" numeric(15,10) NOT NULL; +"); + } + + public override void AddColumnOperation_with_precision_and_scale_no_model() + { + base.AddColumnOperation_with_precision_and_scale_no_model(); + + AssertSql( + @"ALTER TABLE ""Person"" ADD ""Pi"" numeric(20,7) NOT NULL; +"); + } + + public override void AddForeignKeyOperation_without_principal_columns() + { + base.AddForeignKeyOperation_without_principal_columns(); + + AssertSql( + @"ALTER TABLE ""People"" ADD FOREIGN KEY (""SpouseId"") REFERENCES ""People""; +"); + } + + public override void AlterColumnOperation_without_column_type() + { + base.AlterColumnOperation_without_column_type(); + + AssertSql( + @"ALTER TABLE ""People"" ALTER COLUMN ""LuckyNumber"" TYPE integer; +ALTER TABLE ""People"" ALTER COLUMN ""LuckyNumber"" SET NOT NULL; +ALTER TABLE ""People"" ALTER COLUMN ""LuckyNumber"" DROP DEFAULT; +"); + } + + public override void RenameTableOperation_legacy() + { + base.RenameTableOperation_legacy(); + + AssertSql( + @"ALTER TABLE dbo.""People"" RENAME TO ""Person""; +"); + } + + public override void RenameTableOperation() + { + base.RenameTableOperation(); + + AssertSql( + @"ALTER TABLE dbo.""People"" RENAME TO ""Person""; +"); + } + + public override void SqlOperation() + { + base.SqlOperation(); + + AssertSql( + @"-- I <3 DDL +"); + } + + public override void InsertDataOperation_all_args_spatial() + { + base.InsertDataOperation_all_args_spatial(); + + AssertSql( + @"INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (0, NULL, NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (1, 'Daenerys Targaryen', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (2, 'John Snow', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (3, 'Arya Stark', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (4, 'Harry Strickland', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (5, 'The Imp', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (6, 'The Kingslayer', NULL); +INSERT INTO dbo.""People"" (""Id"", ""Full Name"", ""Geometry"") +VALUES (7, 'Aemon Targaryen', GEOMETRY 'SRID=4326;GEOMETRYCOLLECTION Z(LINESTRING Z(1.1 2.2 NaN, 2.2 2.2 NaN, 2.2 1.1 NaN, 7.1 7.2 NaN), LINESTRING Z(7.1 7.2 NaN, 20.2 20.2 NaN, 20.2 1.1 NaN, 70.1 70.2 NaN), MULTIPOINT Z((1.1 2.2 NaN), (2.2 2.2 NaN), (2.2 1.1 NaN)), POLYGON Z((1.1 2.2 NaN, 2.2 2.2 NaN, 2.2 1.1 NaN, 1.1 2.2 NaN)), POLYGON Z((10.1 20.2 NaN, 20.2 20.2 NaN, 20.2 10.1 NaN, 10.1 20.2 NaN)), POINT Z(1.1 2.2 3.3), MULTILINESTRING Z((1.1 2.2 NaN, 2.2 2.2 NaN, 2.2 1.1 NaN, 7.1 7.2 NaN), (7.1 7.2 NaN, 20.2 20.2 NaN, 20.2 1.1 NaN, 70.1 70.2 NaN)), MULTIPOLYGON Z(((10.1 20.2 NaN, 20.2 20.2 NaN, 20.2 10.1 NaN, 10.1 20.2 NaN)), ((1.1 2.2 NaN, 2.2 2.2 NaN, 2.2 1.1 NaN, 1.1 2.2 NaN))))'); +"); + } + + public override void InsertDataOperation_required_args() + { + base.InsertDataOperation_required_args(); + + AssertSql( + @"INSERT INTO ""People"" (""First Name"") +VALUES ('John'); +"); + } + + public override void InsertDataOperation_required_args_composite() + { + base.InsertDataOperation_required_args_composite(); + + AssertSql( + @"INSERT INTO ""People"" (""First Name"", ""Last Name"") +VALUES ('John', 'Snow'); +"); + } + + public override void InsertDataOperation_required_args_multiple_rows() + { + base.InsertDataOperation_required_args_multiple_rows(); + + AssertSql( + @"INSERT INTO ""People"" (""First Name"") +VALUES ('John'); +INSERT INTO ""People"" (""First Name"") +VALUES ('Daenerys'); +"); + } + + public override void DeleteDataOperation_all_args() + { + base.DeleteDataOperation_all_args(); + + AssertSql( + @"DELETE FROM ""People"" +WHERE ""First Name"" = 'Hodor'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Daenerys'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'John'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Arya'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Harry'; +"); + } + + public override void DeleteDataOperation_all_args_composite() + { + base.DeleteDataOperation_all_args_composite(); + + AssertSql( + @"DELETE FROM ""People"" +WHERE ""First Name"" = 'Hodor' AND ""Last Name"" IS NULL; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Daenerys' AND ""Last Name"" = 'Targaryen'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'John' AND ""Last Name"" = 'Snow'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Arya' AND ""Last Name"" = 'Stark'; +DELETE FROM ""People"" +WHERE ""First Name"" = 'Harry' AND ""Last Name"" = 'Strickland'; +"); + } + + public override void DeleteDataOperation_required_args() + { + base.DeleteDataOperation_required_args(); + + AssertSql( + @"DELETE FROM ""People"" +WHERE ""Last Name"" = 'Snow'; +"); + } + + public override void DeleteDataOperation_required_args_composite() + { + base.DeleteDataOperation_required_args_composite(); + + AssertSql( + @"DELETE FROM ""People"" +WHERE ""First Name"" = 'John' AND ""Last Name"" = 'Snow'; +"); + } + + public override void UpdateDataOperation_all_args() + { + base.UpdateDataOperation_all_args(); + + AssertSql( + @"UPDATE ""People"" SET ""Birthplace"" = 'Winterfell', ""House Allegiance"" = 'Stark', ""Culture"" = 'Northmen' +WHERE ""First Name"" = 'Hodor'; +UPDATE ""People"" SET ""Birthplace"" = 'Dragonstone', ""House Allegiance"" = 'Targaryen', ""Culture"" = 'Valyrian' +WHERE ""First Name"" = 'Daenerys'; +"); + } + + public override void UpdateDataOperation_all_args_composite() + { + base.UpdateDataOperation_all_args_composite(); + + AssertSql( + @"UPDATE ""People"" SET ""House Allegiance"" = 'Stark' +WHERE ""First Name"" = 'Hodor' AND ""Last Name"" IS NULL; +UPDATE ""People"" SET ""House Allegiance"" = 'Targaryen' +WHERE ""First Name"" = 'Daenerys' AND ""Last Name"" = 'Targaryen'; +"); + } + + public override void UpdateDataOperation_all_args_composite_multi() + { + base.UpdateDataOperation_all_args_composite_multi(); + + AssertSql( + @"UPDATE ""People"" SET ""Birthplace"" = 'Winterfell', ""House Allegiance"" = 'Stark', ""Culture"" = 'Northmen' +WHERE ""First Name"" = 'Hodor' AND ""Last Name"" IS NULL; +UPDATE ""People"" SET ""Birthplace"" = 'Dragonstone', ""House Allegiance"" = 'Targaryen', ""Culture"" = 'Valyrian' +WHERE ""First Name"" = 'Daenerys' AND ""Last Name"" = 'Targaryen'; +"); + } + + public override void UpdateDataOperation_all_args_multi() + { + base.UpdateDataOperation_all_args_multi(); + + AssertSql( + @"UPDATE ""People"" SET ""Birthplace"" = 'Dragonstone', ""House Allegiance"" = 'Targaryen', ""Culture"" = 'Valyrian' +WHERE ""First Name"" = 'Daenerys'; +"); + } + + public override void UpdateDataOperation_required_args() + { + base.UpdateDataOperation_required_args(); + + AssertSql( + @"UPDATE ""People"" SET ""House Allegiance"" = 'Targaryen' +WHERE ""First Name"" = 'Daenerys'; +"); + } + + public override void UpdateDataOperation_required_args_multiple_rows() + { + base.UpdateDataOperation_required_args_multiple_rows(); + + AssertSql( + @"UPDATE ""People"" SET ""House Allegiance"" = 'Stark' +WHERE ""First Name"" = 'Hodor'; +UPDATE ""People"" SET ""House Allegiance"" = 'Targaryen' +WHERE ""First Name"" = 'Daenerys'; +"); + } + + public override void UpdateDataOperation_required_args_composite() + { + base.UpdateDataOperation_required_args_composite(); + + AssertSql( + @"UPDATE ""People"" SET ""House Allegiance"" = 'Targaryen' +WHERE ""First Name"" = 'Daenerys' AND ""Last Name"" = 'Targaryen'; +"); + } + + public override void UpdateDataOperation_required_args_composite_multi() + { + base.UpdateDataOperation_required_args_composite_multi(); + + AssertSql( + @"UPDATE ""People"" SET ""Birthplace"" = 'Dragonstone', ""House Allegiance"" = 'Targaryen', ""Culture"" = 'Valyrian' +WHERE ""First Name"" = 'Daenerys' AND ""Last Name"" = 'Targaryen'; +"); + } + + public override void UpdateDataOperation_required_args_multi() + { + base.UpdateDataOperation_required_args_multi(); + + AssertSql( + @"UPDATE ""People"" SET ""Birthplace"" = 'Dragonstone', ""House Allegiance"" = 'Targaryen', ""Culture"" = 'Valyrian' +WHERE ""First Name"" = 'Daenerys'; +"); + } + + [ConditionalTheory(Skip = "https://github.com/npgsql/efcore.pg/issues/1478")] + public override void DefaultValue_with_line_breaks(bool isUnicode) + => base.DefaultValue_with_line_breaks(isUnicode); + + // Which index collations are available on a given PostgreSQL varies (e.g. Linux vs. Windows) + // so we test support for this on the generated SQL only, and not against the database in MigrationsNpsqlTest. + [Fact] + public void CreateIndexOperation_collation() + { + Generate(new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName", "LastName" }, + [RelationalAnnotationNames.Collation] = new[] { null, "de_DE" } + }); + + AssertSql( + @"CREATE INDEX ""IX_People_Name"" ON dbo.""People"" (""FirstName"", ""LastName"" COLLATE ""de_DE""); +"); + } + + [Theory] + [InlineData(MigrationsSqlGenerationOptions.Default)] + [InlineData(MigrationsSqlGenerationOptions.Idempotent)] + public void Alter_column_change_serial_to_identity_idempotent(MigrationsSqlGenerationOptions options) + { + Generate( + modelBuilder => modelBuilder.Entity().Property("Id").UseSerialColumn(), + new[] + { + new AlterColumnOperation + { + Table = "Person", + Name = "Id", + ClrType = typeof(int), + [NpgsqlAnnotationNames.ValueGenerationStrategy] = + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, + + OldColumn = new AddColumnOperation + { + Table = "Person", + Name = "Id", + ClrType = typeof(int), + [NpgsqlAnnotationNames.ValueGenerationStrategy] = + NpgsqlValueGenerationStrategy.SerialColumn, + } + } + }, + options); + + AssertSql( + $@"ALTER TABLE ""Person"" ALTER COLUMN ""Id"" TYPE integer; +ALTER TABLE ""Person"" ALTER COLUMN ""Id"" SET NOT NULL; +ALTER SEQUENCE ""Person_Id_seq"" RENAME TO ""Person_Id_old_seq""; +ALTER TABLE ""Person"" ALTER COLUMN ""Id"" DROP DEFAULT; +ALTER TABLE ""Person"" ALTER COLUMN ""Id"" ADD GENERATED BY DEFAULT AS IDENTITY; +{(options == MigrationsSqlGenerationOptions.Idempotent ? "PERFORM" : "SELECT")} * FROM setval('""Person_Id_seq""', nextval('""Person_Id_old_seq""'), false); +DROP SEQUENCE ""Person_Id_old_seq""; +"); + } + + #region CockroachDB interleave-in-parent + + // Note that we don't run tests against actual CockroachDB instances, so these are unit tests asserting on SQL + // only + + [Fact] + public void CreateTableOperation_with_cockroach_interleave_in_parent() + { + var op = + new CreateTableOperation + { + Name = "People", + Schema = "dbo", + Columns = + { + new AddColumnOperation + { + Name = "Id", + Table = "People", + Schema = "dbo", + ClrType = typeof(int), + IsNullable = false + }, + }, + PrimaryKey = new AddPrimaryKeyOperation + { + Columns = new[] { "Id" } + } + }; + + var interleaveInParent = new CockroachDbInterleaveInParent(op); + interleaveInParent.ParentTableSchema = "my_schema"; + interleaveInParent.ParentTableName = "my_parent"; + interleaveInParent.InterleavePrefix = new List { "col_a", "col_b" }; + + Generate(op); + + AssertSql( + @"CREATE TABLE dbo.""People"" ( + ""Id"" integer NOT NULL, + PRIMARY KEY (""Id"") +) +INTERLEAVE IN PARENT my_schema.my_parent (col_a, col_b); +"); + } + + #endregion CockroachDB interleave-in-parent + +#pragma warning disable 618 + [Fact] + public virtual void AddColumnOperation_serial_old_annotation_throws() + { + Assert.Throws(() => + Generate(new AddColumnOperation + { + Table = "People", + Name = "foo", + ClrType = typeof(int), + ColumnType = "int", + IsNullable = false, + [NpgsqlAnnotationNames.ValueGeneratedOnAdd] = true + })); + } + + public override void InsertDataOperation_throws_for_unsupported_column_types() + => Assert.Equal( + RelationalStrings.UnsupportedDataOperationStoreType("foo", "dbo.People.First Name"), + Assert.Throws( + () => + Generate( + new InsertDataOperation + { + Table = "People", + Schema = "dbo", + Columns = new[] { "First Name" }, + ColumnTypes = new[] { "foo" }, + Values = new object[,] { { null } } + })).Message); + +#pragma warning restore 618 + + public NpgsqlMigrationsSqlGeneratorTest() + : base( + NpgsqlTestHelpers.Instance, + new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite(), + NpgsqlTestHelpers.Instance.AddProviderOptions( + ((IRelationalDbContextOptionsBuilderInfrastructure) + new NpgsqlDbContextOptionsBuilder(new DbContextOptionsBuilder()).UseNetTopologySuite()) + .OptionsBuilder).Options) + { + } + + protected override string GetGeometryCollectionStoreType() => "GEOMETRY(GEOMETRYCOLLECTION)"; + } +}