diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs index d7dd7a589..6e7a7dd70 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs @@ -8,7 +8,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Queries; using Squidex.Providers.Postgres; using Squidex.Providers.Postgres.App; using Testcontainers.PostgreSql; @@ -19,7 +21,7 @@ namespace Squidex.EntityFramework.Migrations; public class PostgresMigrationTests : IAsyncLifetime { private readonly PostgreSqlContainer postgreSql = - new PostgreSqlBuilder("postgis/postgis") + new PostgreSqlBuilder("imresamu/postgis:16-3.4") .Build(); public async ValueTask InitializeAsync() @@ -59,4 +61,79 @@ public class PostgresMigrationTests : IAsyncLifetime var migrations = await dbContext.Database.GetAppliedMigrationsAsync(); Assert.NotEmpty(migrations); } + + [Fact] + public async Task Should_migrate_idempotent_and_functions_callable() + { + var services = + new ServiceCollection() + .AddDbContextFactory(b => + { + b.UseNpgsql(postgreSql.GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); + }) + .AddSingleton() + .AddSingleton(TestUtils.DefaultSerializer) + .AddSingleton>() + .BuildServiceProvider(); + + var databaseMigrator = services.GetRequiredService>(); + var databaseFactory = services.GetRequiredService>(); + + // Run migrations twice to verify idempotency. + await databaseMigrator.InitializeAsync(default); + await databaseMigrator.InitializeAsync(default); + + // Verify the jsonb_exists function was created and is callable. + // Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser. + await using var dbContext = await databaseFactory.CreateDbContextAsync(); + var result = await dbContext.Database.ExecuteSqlRawAsync( + "SELECT jsonb_exists('{{\"a\":1}}'::jsonb)"); + Assert.Equal(-1, result); + } + + [Fact] + public async Task Should_migrate_when_functions_already_exist() + { + var services = + new ServiceCollection() + .AddDbContextFactory(b => + { + b.UseNpgsql(postgreSql.GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); + }) + .AddSingleton() + .AddSingleton(TestUtils.DefaultSerializer) + .AddSingleton>() + .BuildServiceProvider(); + + var databaseMigrator = services.GetRequiredService>(); + var databaseFactory = services.GetRequiredService>(); + + // Simulate a pre-migration database: apply all migrations up to (but not including) + // AddJsonFunctions, then create the functions via the old SqlDialectInitializer path. + await using (var dbContext = await databaseFactory.CreateDbContextAsync()) + { + await dbContext.Database.MigrateAsync("20260323155026_MigrateToNet10"); + + if (dbContext is IDbContextWithDialect withDialect) + { + await withDialect.Dialect.InitializeAsync(dbContext, default); + } + } + + // Run the full migration — should apply AddJsonFunctions on top of already-existing functions. + await databaseMigrator.InitializeAsync(default); + + // Verify the functions are still callable after migration. + // Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser. + await using var verifyContext = await databaseFactory.CreateDbContextAsync(); + var result = await verifyContext.Database.ExecuteSqlRawAsync( + "SELECT jsonb_exists('{{\"a\":1}}'::jsonb)"); + Assert.Equal(-1, result); + } } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs index 4f613fd46..db5d7e09c 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs @@ -23,7 +23,7 @@ namespace Squidex.EntityFramework.TestHelpers; public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixture { private readonly PostgreSqlContainer postgreSql = - new PostgreSqlBuilder("postgis/postgis") + new PostgreSqlBuilder("imresamu/postgis:16-3.4") .WithReuse(true) .WithLabel("reuse-id", reuseId) .Build();