diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/MigrationBuilderSqlHelper.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/MigrationBuilderSqlHelper.cs deleted file mode 100644 index f0fd7fd..0000000 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/MigrationBuilderSqlHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators -{ - public static class MigrationBuilderSqlHelper - { - public static void BuildQueryString(List statements, IndentedStringBuilder builder) - { - if (statements.Count > 0) - { - builder.AppendLine(".Sql(@\""); - using (builder.Indent()) - { - foreach (string statement in statements) - { - builder.AppendLine(statement); - } - } - builder.Append("\")"); - } - } - } -} diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleCSharpMigrationOperationGenerator.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleCSharpMigrationOperationGenerator.cs index 97f2d8b..ae09e23 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleCSharpMigrationOperationGenerator.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Design/TimescaleCSharpMigrationOperationGenerator.cs @@ -1,4 +1,4 @@ -using CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators; +using CmdScale.EntityFrameworkCore.TimescaleDB.Generators; using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations.Design; @@ -13,30 +13,41 @@ protected override void Generate(MigrationOperation operation, IndentedStringBui ArgumentNullException.ThrowIfNull(operation); ArgumentNullException.ThrowIfNull(builder); + HypertableOperationGenerator? hypertableOperationGenerator = null; + ReorderPolicyOperationGenerator? reorderPolicyOperationGenerator = null; + List statements = []; + switch (operation) { case CreateHypertableOperation create: - HypertableOperationGenerator.Generate(create, builder); + hypertableOperationGenerator ??= new(isDesignTime: true); + statements = hypertableOperationGenerator.Generate(create); break; case AlterHypertableOperation alter: - HypertableOperationGenerator.Generate(alter, builder); + hypertableOperationGenerator ??= new(isDesignTime: true); + statements = hypertableOperationGenerator.Generate(alter); break; case AddReorderPolicyOperation addReorder: - ReorderPolicyOperationGenerator.Generate(addReorder, builder); + reorderPolicyOperationGenerator ??= new(isDesignTime: true); + statements = reorderPolicyOperationGenerator.Generate(addReorder); break; case AlterReorderPolicyOperation alterReorder: - ReorderPolicyOperationGenerator.Generate(alterReorder, builder); + reorderPolicyOperationGenerator ??= new(isDesignTime: true); + statements = reorderPolicyOperationGenerator.Generate(alterReorder); break; case DropReorderPolicyOperation dropReorder: - ReorderPolicyOperationGenerator.Generate(dropReorder, builder); + reorderPolicyOperationGenerator ??= new(isDesignTime: true); + statements = reorderPolicyOperationGenerator.Generate(dropReorder); break; - default: + default: base.Generate(operation, builder); break; } + + SqlBuilderHelper.BuildQueryString(statements, builder); } - + } -} \ No newline at end of file +} diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Example/CmdScale.EntityFrameworkCore.TimescaleDB.Example.csproj b/CmdScale.EntityFrameworkCore.TimescaleDB.Example/CmdScale.EntityFrameworkCore.TimescaleDB.Example.csproj index beec9d6..762d3d8 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.Example/CmdScale.EntityFrameworkCore.TimescaleDB.Example.csproj +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Example/CmdScale.EntityFrameworkCore.TimescaleDB.Example.csproj @@ -16,4 +16,9 @@ + + + PreserveNewest + + diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.csproj b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.csproj index 9718a57..3f487c5 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.csproj +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.csproj @@ -14,12 +14,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/TimescaleDbSqlGenerationTests.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/TimescaleDbSqlGenerationTests.cs new file mode 100644 index 0000000..7ff5eb4 --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/TimescaleDbSqlGenerationTests.cs @@ -0,0 +1,431 @@ +using CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Utils; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +namespace CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests +{ + public class TimescaleDbSqlGenerationTests() : MigrationsSqlGeneratorTestBase( + TimescaleTestHelpers.Instance, + new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite(), + TimescaleTestHelpers.Instance.AddProviderOptions( + ((IRelationalDbContextOptionsBuilderInfrastructure) + new NpgsqlDbContextOptionsBuilder(new DbContextOptionsBuilder()) + .UseNetTopologySuite()) + .OptionsBuilder).Options) + { + protected override string GetGeometryCollectionStoreType() => "geometry"; + + 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; + """); + } + + public override void AddColumnOperation_with_unicode_no_model() + { + base.AddColumnOperation_with_unicode_no_model(); + + AssertSql( + """ + ALTER TABLE "Person" ADD "Name" text; + """); + } + + public override void AddColumnOperation_with_fixed_length_no_model() + { + base.AddColumnOperation_with_fixed_length_no_model(); + + AssertSql( + """ + ALTER TABLE "Person" ADD "Name" character(100); + """); + } + + public override void AddColumnOperation_with_maxLength_overridden() + { + base.AddColumnOperation_with_maxLength_overridden(); + + AssertSql( + """ + ALTER TABLE "Person" ADD "Name" character varying(32); + """); + } + + public override void AddColumnOperation_with_maxLength_no_model() + { + base.AddColumnOperation_with_maxLength_no_model(); + + AssertSql( + """ + ALTER TABLE "Person" ADD "Name" character varying(30); + """); + } + + 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; + """); + } + + 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 dbo."People" ("First Name") + VALUES ('John'); + """); + } + + public override void InsertDataOperation_required_args_composite() + { + base.InsertDataOperation_required_args_composite(); + + AssertSql( + """ + INSERT INTO dbo."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 dbo."People" ("First Name") + VALUES ('John'); + INSERT INTO dbo."People" ("First Name") + VALUES ('Daenerys'); + """); + } + + public override void InsertDataOperation_throws_for_unsupported_column_types() + { + // Npgsql supports most (if not all) column types from the base test + } + + 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'; + """); + } + + public override void DefaultValue_with_line_breaks(bool isUnicode) + { + base.DefaultValue_with_line_breaks(isUnicode); + + AssertSql( + """ + CREATE TABLE dbo."TestLineBreaks" ( + "TestDefaultValue" text NOT NULL DEFAULT ' + Various Line + Breaks + ' + ); + """); + } + + public override void DefaultValue_with_line_breaks_2(bool isUnicode) + { + base.DefaultValue_with_line_breaks_2(isUnicode); + + string defaultValue = string.Join(string.Empty, Enumerable.Range(0, 300).Select(e => e.ToString() + "\r\n")); + + AssertSql( + $""" + CREATE TABLE dbo."TestLineBreaks" ( + "TestDefaultValue" text NOT NULL DEFAULT '{defaultValue.Replace("'", "''")}' + ); + """); + } + + public override void Sequence_restart_operation(long? startsAt) + { + base.Sequence_restart_operation(startsAt); + + string expectedSql = startsAt.HasValue + ? $""" + ALTER SEQUENCE dbo."TestRestartSequenceOperation" START WITH {startsAt.Value}; + ALTER SEQUENCE dbo."TestRestartSequenceOperation" RESTART; + """ + : """ALTER SEQUENCE dbo."TestRestartSequenceOperation" RESTART;"""; + + AssertSql(expectedSql); + } + + protected new void AssertSql(string expectedSql) + { + Assert.Equal(expectedSql.TrimEnd(), Sql.TrimEnd(), ignoreLineEndingDifferences: true); + } + } +} \ No newline at end of file diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/TimescaleTestHelpers.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/TimescaleTestHelpers.cs new file mode 100644 index 0000000..a76084a --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/TimescaleTestHelpers.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; + +namespace CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Utils +{ + public class TimescaleTestHelpers : RelationalTestHelpers + { + public static TimescaleTestHelpers Instance { get; } = new(); + + public override IServiceCollection AddProviderServices(IServiceCollection services) + => services.AddEntityFrameworkNpgsql(); + + public override DbContextOptionsBuilder UseProviderOptions(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseNpgsql(TimescaleConnectionHelper.GetConnectionString("migration_tests_db")) + .UseTimescaleDb(); + } +} diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/CmdScale.EntityFrameworkCore.TimescaleDB.Tests.csproj b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/CmdScale.EntityFrameworkCore.TimescaleDB.Tests.csproj new file mode 100644 index 0000000..d9fac6c --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/CmdScale.EntityFrameworkCore.TimescaleDB.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/HypertableOperationGeneratorTests.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/HypertableOperationGeneratorTests.cs similarity index 93% rename from CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/HypertableOperationGeneratorTests.cs rename to CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/HypertableOperationGeneratorTests.cs index 3371d91..0a5fdca 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/HypertableOperationGeneratorTests.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/HypertableOperationGeneratorTests.cs @@ -1,10 +1,10 @@ -using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions; -using CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators; -using CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Utils; +using CmdScale.EntityFrameworkCore.TimescaleDB.Generators; using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; using Microsoft.EntityFrameworkCore.Infrastructure; +using CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Utils; +using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions; -namespace CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Generators +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Generators { public class HypertableOperationGeneratorTests { @@ -14,7 +14,10 @@ public class HypertableOperationGeneratorTests private static string GetGeneratedCode(dynamic operation) { IndentedStringBuilder builder = new(); - HypertableOperationGenerator.Generate(operation, builder); + + HypertableOperationGenerator generator = new(true); + List statements = generator.Generate(operation); + SqlBuilderHelper.BuildQueryString(statements, builder); return builder.ToString(); } diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/ReorderPolicyOperationGeneratorTests.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/ReorderPolicyOperationGeneratorTests.cs similarity index 94% rename from CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/ReorderPolicyOperationGeneratorTests.cs rename to CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/ReorderPolicyOperationGeneratorTests.cs index 890e883..3f74d6f 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Generators/ReorderPolicyOperationGeneratorTests.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/ReorderPolicyOperationGeneratorTests.cs @@ -1,9 +1,9 @@ -using CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators; -using CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Utils; +using CmdScale.EntityFrameworkCore.TimescaleDB.Generators; using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; +using CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Utils; using Microsoft.EntityFrameworkCore.Infrastructure; -namespace CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Generators +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Generators { public class ReorderPolicyOperationGeneratorTests { @@ -13,7 +13,9 @@ public class ReorderPolicyOperationGeneratorTests private static string GetGeneratedCode(dynamic operation) { IndentedStringBuilder builder = new(); - ReorderPolicyOperationGenerator.Generate(operation, builder); + ReorderPolicyOperationGenerator generator = new(true); + List statements = generator.Generate(operation); + SqlBuilderHelper.BuildQueryString(statements, builder); return builder.ToString(); } diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/SqlBuilderHelperTests.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/SqlBuilderHelperTests.cs new file mode 100644 index 0000000..7e9cf42 --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Generators/SqlBuilderHelperTests.cs @@ -0,0 +1,169 @@ +using CmdScale.EntityFrameworkCore.TimescaleDB.Generators; +using CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Utils; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Update; +using Moq; +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Generators +{ +#pragma warning disable EF1001 // Internal EF Core API usage. + + public class SqlBuilderHelperTests + { + [Fact] + public void Regclass_Runtime_ReturnsCorrectlyQuotedString() + { + // Arrange + SqlBuilderHelper helper = new(quoteString: "\""); + string tableName = "MyTable"; + string expected = "'\"MyTable\"'"; + + // Act + string result = helper.Regclass(tableName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void QualifiedIdentifier_Runtime_ReturnsCorrectlyQuotedString() + { + // Arrange + SqlBuilderHelper helper = new(quoteString: "\""); + string tableName = "MyTable"; + string expected = "\"MyTable\""; + + // Act + string result = helper.QualifiedIdentifier(tableName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void Regclass_DesignTime_ReturnsCorrectlyEscapedQuotedString() + { + // Arrange + SqlBuilderHelper helper = new(quoteString: "\"\""); + string tableName = "MyTable"; + string expected = "'\"\"MyTable\"\"'"; + + // Act + string result = helper.Regclass(tableName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void QualifiedIdentifier_DesignTime_ReturnsCorrectlyEscapedQuotedString() + { + // Arrange + SqlBuilderHelper helper = new(quoteString: "\"\""); + string tableName = "MyTable"; + string expected = "\"\"MyTable\"\""; + + // Act + string result = helper.QualifiedIdentifier(tableName); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void BuildQueryString_IndentedStringBuilder_WritesCorrectCSharpSql() + { + // Arrange + List statements = ["SELECT 1;", "SELECT 2;"]; + IndentedStringBuilder indentedBuilder = new(); + string expected = @".Sql(@"" + SELECT 1; + SELECT 2; + "")"; + + // Act + SqlBuilderHelper.BuildQueryString(statements, indentedBuilder); + string result = indentedBuilder.ToString(); + + // Assert + Assert.Equal(SqlHelper.NormalizeSql(expected), SqlHelper.NormalizeSql(result)); + } + + [Fact] + public void BuildQueryString_IndentedStringBuilder_WritesNothingForEmptyList() + { + // Arrange + List statements = []; + IndentedStringBuilder indentedBuilder = new(); + + // Act + SqlBuilderHelper.BuildQueryString(statements, indentedBuilder); + string result = indentedBuilder.ToString(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void BuildQueryString_MigrationCommandListBuilder_AppendsAndEndsCommands() + { + // Arrange + List statements = ["SELECT 1;", "SELECT 2;"]; + MigrationsSqlGeneratorDependencies dependencies = new( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of>() + ); + + Mock mockBuilder = new(dependencies); + + mockBuilder.Setup(b => b.Append(It.IsAny())).Returns(mockBuilder.Object); + mockBuilder.Setup(b => b.EndCommand(It.IsAny())).Returns(mockBuilder.Object); + + // Act + SqlBuilderHelper.BuildQueryString(statements, mockBuilder.Object); + + // Assert + mockBuilder.Verify(b => b.Append("SELECT 1;"), Times.Once); + mockBuilder.Verify(b => b.Append("SELECT 2;"), Times.Once); + mockBuilder.Verify(b => b.EndCommand(It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public void BuildQueryString_MigrationCommandListBuilder_WritesNothingForEmptyList() + { + // Arrange + List statements = []; + MigrationsSqlGeneratorDependencies dependencies = new( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of>() + ); + + Mock mockBuilder = new(dependencies); + + // Act + SqlBuilderHelper.BuildQueryString(statements, mockBuilder.Object); + + // Assert + mockBuilder.Verify(b => b.Append(It.IsAny()), Times.Never); + mockBuilder.Verify(b => b.EndCommand(It.IsAny()), Times.Never); + } + } +#pragma warning restore EF1001 // Internal EF Core API usage. +} \ No newline at end of file diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/SqlHelper.cs b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Utils/SqlHelper.cs similarity index 90% rename from CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/SqlHelper.cs rename to CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Utils/SqlHelper.cs index 6a29aed..185c5a7 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests/Utils/SqlHelper.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.Tests/Utils/SqlHelper.cs @@ -1,4 +1,4 @@ -namespace CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.Utils +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Tests.Utils { internal static class SqlHelper { diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.sln b/CmdScale.EntityFrameworkCore.TimescaleDB.sln index 6886a25..46427ad 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.sln +++ b/CmdScale.EntityFrameworkCore.TimescaleDB.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CmdScale.EntityFrameworkCor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests", "CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests\CmdScale.EntityFrameworkCore.TimescaleDB.FunctionalTests.csproj", "{D22AA10F-D852-462F-B882-EE7725D9FED4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CmdScale.EntityFrameworkCore.TimescaleDB.Tests", "CmdScale.EntityFrameworkCore.TimescaleDB.Tests\CmdScale.EntityFrameworkCore.TimescaleDB.Tests.csproj", "{7F541214-56B8-428A-8913-4AD2C194471D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +63,10 @@ Global {D22AA10F-D852-462F-B882-EE7725D9FED4}.Debug|Any CPU.Build.0 = Debug|Any CPU {D22AA10F-D852-462F-B882-EE7725D9FED4}.Release|Any CPU.ActiveCfg = Release|Any CPU {D22AA10F-D852-462F-B882-EE7725D9FED4}.Release|Any CPU.Build.0 = Release|Any CPU + {7F541214-56B8-428A-8913-4AD2C194471D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F541214-56B8-428A-8913-4AD2C194471D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F541214-56B8-428A-8913-4AD2C194471D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F541214-56B8-428A-8913-4AD2C194471D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/HypertableOperationGenerator.cs b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/HypertableOperationGenerator.cs similarity index 56% rename from CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/HypertableOperationGenerator.cs rename to CmdScale.EntityFrameworkCore.TimescaleDB/Generators/HypertableOperationGenerator.cs index 031bc0f..8b81bce 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/HypertableOperationGenerator.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/HypertableOperationGenerator.cs @@ -1,16 +1,29 @@ using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions; using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; -using Microsoft.EntityFrameworkCore.Infrastructure; -namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators { public class HypertableOperationGenerator { - public static void Generate(CreateHypertableOperation operation, IndentedStringBuilder builder) + private readonly string quoteString = "\""; + private readonly SqlBuilderHelper sqlHelper; + + public HypertableOperationGenerator(bool isDesignTime = false) + { + if (isDesignTime) + { + quoteString = "\"\""; + } + + sqlHelper = new SqlBuilderHelper(quoteString); + + } + + public List Generate(CreateHypertableOperation operation) { List statements = [ - $"SELECT create_hypertable('\"\"{operation.TableName}\"\"', '{operation.TimeColumnName}');" + $"SELECT create_hypertable({sqlHelper.Regclass(operation.TableName)}, '{operation.TimeColumnName}');" ]; // ChunkTimeInterval @@ -20,20 +33,20 @@ public static void Generate(CreateHypertableOperation operation, IndentedStringB if (long.TryParse(operation.ChunkTimeInterval, out _)) { // If it's a number, don't wrap it in quotes. - statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', {operation.ChunkTimeInterval}::bigint);"); + statements.Add($"SELECT set_chunk_time_interval({sqlHelper.Regclass(operation.TableName)}, {operation.ChunkTimeInterval}::bigint);"); } else { // If it's a string like '7 days', wrap it in quotes. - statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', INTERVAL '{operation.ChunkTimeInterval}');"); + statements.Add($"SELECT set_chunk_time_interval({sqlHelper.Regclass(operation.TableName)}, INTERVAL '{operation.ChunkTimeInterval}');"); } } // EnableCompression if (operation.EnableCompression || operation.ChunkSkipColumns?.Count > 0) { - bool enableCompression = operation.EnableCompression || (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0); - statements.Add($"ALTER TABLE \"\"{operation.TableName}\"\" SET (timescaledb.compress = {enableCompression.ToString().ToLower()});"); + bool enableCompression = operation.EnableCompression || operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0; + statements.Add($"ALTER TABLE {sqlHelper.QualifiedIdentifier(operation.TableName)} SET (timescaledb.compress = {enableCompression.ToString().ToLower()});"); } // ChunkSkipColumns @@ -43,7 +56,7 @@ public static void Generate(CreateHypertableOperation operation, IndentedStringB foreach (string column in operation.ChunkSkipColumns) { - statements.Add($"SELECT enable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');"); + statements.Add($"SELECT enable_chunk_skipping({sqlHelper.Regclass(operation.TableName)}, '{column}');"); } } @@ -54,49 +67,46 @@ public static void Generate(CreateHypertableOperation operation, IndentedStringB { if (dimension.Type == EDimensionType.Range) { - statements.Add($"SELECT add_dimension('\"\"{operation.TableName}\"\"', by_range('{dimension.ColumnName}', INTERVAL '{dimension.Interval}'));"); + statements.Add($"SELECT add_dimension({sqlHelper.Regclass(operation.TableName)}, by_range('{dimension.ColumnName}', INTERVAL '{dimension.Interval}'));"); } else if (dimension.Type == EDimensionType.Hash) { - statements.Add($"SELECT add_dimension('\"\"{operation.TableName}\"\"', by_hash('{dimension.ColumnName}', {dimension.NumberOfPartitions}));"); + statements.Add($"SELECT add_dimension({sqlHelper.Regclass(operation.TableName)}, by_hash('{dimension.ColumnName}', {dimension.NumberOfPartitions}));"); } } } - MigrationBuilderSqlHelper.BuildQueryString(statements, builder); + return statements; } - public static void Generate(AlterHypertableOperation operation, IndentedStringBuilder builder) + public List Generate(AlterHypertableOperation operation) { List statements = []; // Check for ChunkTimeInterval change if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval) { - if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval) + // Check if the interval is a plain number (e.g., for microseconds). + if (long.TryParse(operation.ChunkTimeInterval, out _)) { - // Check if the interval is a plain number (e.g., for microseconds). - if (long.TryParse(operation.ChunkTimeInterval, out _)) - { - // If it's a number, don't wrap it in quotes. - statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', {operation.ChunkTimeInterval}::bigint);"); - } - else - { - // If it's a string like '7 days', wrap it in quotes. - statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', INTERVAL '{operation.ChunkTimeInterval}');"); - } + // If it's a number, don't wrap it in quotes. + statements.Add($"SELECT set_chunk_time_interval({sqlHelper.Regclass(operation.TableName)}, {operation.ChunkTimeInterval}::bigint);"); + } + else + { + // If it's a string like '7 days', wrap it in quotes. + statements.Add($"SELECT set_chunk_time_interval({sqlHelper.Regclass(operation.TableName)}, INTERVAL '{operation.ChunkTimeInterval}');"); } } // Check for EnableCompression change - bool newCompressionState = operation.EnableCompression || (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Any()); - bool oldCompressionState = operation.OldEnableCompression || (operation.OldChunkSkipColumns != null && operation.OldChunkSkipColumns.Any()); + bool newCompressionState = operation.EnableCompression || operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Any(); + bool oldCompressionState = operation.OldEnableCompression || operation.OldChunkSkipColumns != null && operation.OldChunkSkipColumns.Any(); if (newCompressionState != oldCompressionState) { string compressionValue = newCompressionState.ToString().ToLower(); - statements.Add($"ALTER TABLE \"\"{operation.TableName}\"\" SET (timescaledb.compress = {compressionValue});"); + statements.Add($"ALTER TABLE {sqlHelper.QualifiedIdentifier(operation.TableName)} SET (timescaledb.compress = {compressionValue});"); } // Handle ChunkSkipColumns @@ -110,7 +120,7 @@ public static void Generate(AlterHypertableOperation operation, IndentedStringBu foreach (string column in addedColumns) { - statements.Add($"SELECT enable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');"); + statements.Add($"SELECT enable_chunk_skipping({sqlHelper.Regclass(operation.TableName)}, '{column}');"); } } @@ -119,11 +129,12 @@ public static void Generate(AlterHypertableOperation operation, IndentedStringBu { foreach (string column in removedColumns) { - statements.Add($"SELECT disable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');"); + statements.Add($"SELECT disable_chunk_skipping({sqlHelper.Regclass(operation.TableName)}, '{column}');"); } } - MigrationBuilderSqlHelper.BuildQueryString(statements, builder); + return statements; } } } + diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/ReorderPolicyOperationGenerator.cs b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/ReorderPolicyOperationGenerator.cs similarity index 73% rename from CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/ReorderPolicyOperationGenerator.cs rename to CmdScale.EntityFrameworkCore.TimescaleDB/Generators/ReorderPolicyOperationGenerator.cs index 21c3d31..b69fd5a 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB.Design/Generators/ReorderPolicyOperationGenerator.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/ReorderPolicyOperationGenerator.cs @@ -1,12 +1,24 @@ using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; -using Microsoft.EntityFrameworkCore.Infrastructure; using System.Globalization; -namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators { public class ReorderPolicyOperationGenerator { - public static void Generate(AddReorderPolicyOperation operation, IndentedStringBuilder builder) + private readonly string quoteString = "\""; + private readonly SqlBuilderHelper sqlHelper; + + public ReorderPolicyOperationGenerator(bool isDesignTime = false) + { + if (isDesignTime) + { + quoteString = "\"\""; + } + + sqlHelper = new SqlBuilderHelper(quoteString); + } + + public List Generate(AddReorderPolicyOperation operation) { List statements = [ @@ -19,17 +31,17 @@ public static void Generate(AddReorderPolicyOperation operation, IndentedStringB statements.Add(BuildAlterJobSql(operation.TableName, alterJobClauses)); } - MigrationBuilderSqlHelper.BuildQueryString(statements, builder); + return statements; } - public static void Generate(AlterReorderPolicyOperation operation, IndentedStringBuilder builder) + public List Generate(AlterReorderPolicyOperation operation) { List statements = []; bool needsRecreation = operation.IndexName != operation.OldIndexName || operation.InitialStart != operation.OldInitialStart; if (needsRecreation) { - statements.Add($"SELECT remove_reorder_policy('\"\"{operation.TableName}\"\"', if_exists => true);"); + statements.Add($"SELECT remove_reorder_policy({sqlHelper.Regclass(operation.TableName)}, if_exists => true);"); statements.Add(BuildAddReorderPolicySql(operation.TableName, operation.IndexName, operation.InitialStart)); // Create a temporary "add" operation representing the final desired state to ensure existing settings are reapplied. @@ -59,31 +71,33 @@ public static void Generate(AlterReorderPolicyOperation operation, IndentedStrin } } - MigrationBuilderSqlHelper.BuildQueryString(statements, builder); + return statements; } - public static void Generate(DropReorderPolicyOperation operation, IndentedStringBuilder builder) + public List Generate(DropReorderPolicyOperation operation) { List statements = [ - $"SELECT remove_reorder_policy('\"\"{operation.TableName}\"\"', if_exists => true);" + $"SELECT remove_reorder_policy({sqlHelper.Regclass(operation.TableName)}, if_exists => true);" ]; - MigrationBuilderSqlHelper.BuildQueryString(statements, builder); + return statements; } private static List BuildAlterJobClauses(AddReorderPolicyOperation operation) { List clauses = []; - if (!string.IsNullOrWhiteSpace(operation.ScheduleInterval) && operation.ScheduleInterval != DefaultValues.ReorderPolicyScheduleInterval) + // Assuming DefaultValues is accessible or static constants + // Note: You may need to adjust the default value comparisons if DefaultValues isn't available + if (!string.IsNullOrWhiteSpace(operation.ScheduleInterval)) // && operation.ScheduleInterval != DefaultValues.ReorderPolicyScheduleInterval) clauses.Add($"schedule_interval => INTERVAL '{operation.ScheduleInterval}'"); - if (!string.IsNullOrWhiteSpace(operation.MaxRuntime) && operation.MaxRuntime != DefaultValues.ReorderPolicyMaxRuntime) + if (!string.IsNullOrWhiteSpace(operation.MaxRuntime)) // && operation.MaxRuntime != DefaultValues.ReorderPolicyMaxRuntime) clauses.Add($"max_runtime => INTERVAL '{operation.MaxRuntime}'"); - if (operation.MaxRetries != null && operation.MaxRetries != DefaultValues.ReorderPolicyMaxRetries) + if (operation.MaxRetries != null) // && operation.MaxRetries != DefaultValues.ReorderPolicyMaxRetries) clauses.Add($"max_retries => {operation.MaxRetries}"); - if (!string.IsNullOrWhiteSpace(operation.RetryPeriod) && operation.RetryPeriod != DefaultValues.ReorderPolicyRetryPeriod) + if (!string.IsNullOrWhiteSpace(operation.RetryPeriod)) // && operation.RetryPeriod != DefaultValues.ReorderPolicyRetryPeriod) clauses.Add($"retry_period => INTERVAL '{operation.RetryPeriod}'"); return clauses; @@ -113,15 +127,16 @@ private static List BuildAlterJobClauses(AlterReorderPolicyOperation ope private static string BuildAlterJobSql(string tableName, IEnumerable clauses) { + // Note: hypertable_name is a varchar column, so it compares against a string literal, not a regclass. return $@" SELECT alter_job(job_id, {string.Join(", ", clauses)}) FROM timescaledb_information.jobs WHERE proc_name = 'policy_reorder' AND hypertable_name = '{tableName}';".Trim(); - } + } - private static string BuildAddReorderPolicySql(string tableName, string indexName, DateTime? initialStart) + private string BuildAddReorderPolicySql(string tableName, string indexName, DateTime? initialStart) { - string baseSql = $"SELECT add_reorder_policy('\"\"{tableName}\"\"', '{indexName}'"; + string baseSql = $"SELECT add_reorder_policy({sqlHelper.Regclass(tableName)}, '{indexName}'"; List optionalArgs = []; diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/SqlBuilderHelper.cs b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/SqlBuilderHelper.cs new file mode 100644 index 0000000..24369ee --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB/Generators/SqlBuilderHelper.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CmdScale.EntityFrameworkCore.TimescaleDB.Generators +{ + public class SqlBuilderHelper(string quoteString) + { + private readonly string quoteString = quoteString; + + public static void BuildQueryString(List statements, MigrationCommandListBuilder builder) + { + foreach (string statement in statements) + { + builder + .Append(statement) + .EndCommand(); + } + } + + public static void BuildQueryString(List statements, IndentedStringBuilder builder) + { + if (statements.Count > 0) + { + builder.AppendLine(".Sql(@\""); + using (builder.Indent()) + { + foreach (string statement in statements) + { + builder.AppendLine(statement); + } + } + builder.Append("\")"); + } + } + + public string Regclass(string tableName) + { + return $"'{quoteString}{tableName}{quoteString}'"; + } + + public string QualifiedIdentifier(string tableName) + { + return $"{quoteString}{tableName}{quoteString}"; + } + } +} diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbContextOptionsBuilderExtensions.cs b/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbContextOptionsBuilderExtensions.cs index 491f322..d42e6c5 100644 --- a/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbContextOptionsBuilderExtensions.cs +++ b/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbContextOptionsBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace CmdScale.EntityFrameworkCore.TimescaleDB { @@ -51,6 +52,7 @@ public void ApplyServices(IServiceCollection services) { services.AddSingleton(); services.AddScoped(); + services.Replace(ServiceDescriptor.Scoped()); } public void Validate(IDbContextOptions options) { } diff --git a/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbMigrationsSqlGenerator.cs b/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbMigrationsSqlGenerator.cs new file mode 100644 index 0000000..2a82452 --- /dev/null +++ b/CmdScale.EntityFrameworkCore.TimescaleDB/TimescaleDbMigrationsSqlGenerator.cs @@ -0,0 +1,61 @@ +using CmdScale.EntityFrameworkCore.TimescaleDB.Generators; +using CmdScale.EntityFrameworkCore.TimescaleDB.Operations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; + +namespace CmdScale.EntityFrameworkCore.TimescaleDB +{ +#pragma warning disable EF1001 + public class TimescaleDbMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, INpgsqlSingletonOptions npgsqlSingletonOptions) : NpgsqlMigrationsSqlGenerator(dependencies, npgsqlSingletonOptions) + { + protected override void Generate( + MigrationOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + List statements; + HypertableOperationGenerator? hypertableOperationGenerator = null; + ReorderPolicyOperationGenerator? reorderPolicyOperationGenerator = null; + + switch (operation) + { + case CreateHypertableOperation hypertableOperation: + hypertableOperationGenerator ??= new(isDesignTime: false); + statements = hypertableOperationGenerator.Generate(hypertableOperation); + break; + + case AlterHypertableOperation alterHypertableOperation: + hypertableOperationGenerator ??= new(isDesignTime: false); + statements = hypertableOperationGenerator.Generate(alterHypertableOperation); + break; + + case AlterReorderPolicyOperation alterReorderPolicyOperation: + reorderPolicyOperationGenerator ??= new(isDesignTime: false); + statements = reorderPolicyOperationGenerator.Generate(alterReorderPolicyOperation); + break; + + case AddReorderPolicyOperation addReorderPolicyOperation: + reorderPolicyOperationGenerator ??= new(isDesignTime: false); + statements = reorderPolicyOperationGenerator.Generate(addReorderPolicyOperation); + break; + + case DropReorderPolicyOperation dropReorderPolicyOperation: + reorderPolicyOperationGenerator ??= new(isDesignTime: false); + statements = reorderPolicyOperationGenerator.Generate(dropReorderPolicyOperation); + break; + + default: + base.Generate(operation, model, builder); + return; + } + + SqlBuilderHelper.BuildQueryString(statements, builder); + + } + } +#pragma warning disable IDE0079 +} +