Browse Source

Move JSON function creation into EF Core migrations

- Replace SqlDialectInitializer startup logic with proper EF Core migrations
  for all three providers (MySQL, SQL Server, Postgres)
- SQL Server: uses CREATE OR ALTER FUNCTION (idempotent, no DROP needed)
- MySQL: uses DROP FUNCTION IF EXISTS + CREATE FUNCTION in migration
- Remove SqlDialectInitializer registration from production ServiceExtensions
- Add migration tests: idempotency and upgrade-from-pre-migration-database
pull/1313/head
Joe de Ronde 5 months ago
parent
commit
14fcb6b289
  1. 1628
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.Designer.cs
  2. 67
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.cs
  3. 7
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/JsonFunction.cs
  4. 75
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/json_function.sql
  5. 1629
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.Designer.cs
  6. 59
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.cs
  7. 1631
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.Designer.cs
  8. 64
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.cs
  9. 1
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  10. 87
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs
  11. 77
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs

1628
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

67
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.cs

@ -0,0 +1,67 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.MySql;
#nullable disable
namespace Squidex.Providers.MySql.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(MySqlDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.MySql.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement, suppressTransaction: true);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
var functions = new[]
{
"json_empty",
"json_exists",
"json_null_equals",
"json_null_notequals",
"json_text_contains",
"json_text_endswith",
"json_text_equals",
"json_text_greaterthan",
"json_text_greaterthanorequal",
"json_text_in",
"json_text_lessthan",
"json_text_lessthanorequal",
"json_text_matchs",
"json_text_notequals",
"json_text_startswith",
"json_boolean_equals",
"json_boolean_in",
"json_boolean_notequals",
"json_number_equals",
"json_number_greaterthan",
"json_number_greaterthanorequal",
"json_number_in",
"json_number_lessthan",
"json_number_lessthanorequal",
"json_number_notequals",
};
foreach (var function in functions)
{
migrationBuilder.Sql($"DROP FUNCTION IF EXISTS {function}", suppressTransaction: true);
}
}
}
}

7
backend/src/Squidex.Data.EntityFramework/Providers/MySql/JsonFunction.cs

@ -57,15 +57,8 @@ public static class JsonFunction
sqlText = sqlText.Replace("}", "}}", StringComparison.Ordinal);
var statements = sqlText.Split(";;", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// We want to filter out the drop statements and multiple function creations are not supported.
foreach (var statement in statements)
{
#if RELEASE
if (statement.StartsWith("DROP", StringComparison.Ordinal))
{
continue;
}
#endif
await dbContext.Database.ExecuteSqlRawAsync(statement, ct);
}
}

75
backend/src/Squidex.Data.EntityFramework/Providers/MySql/json_function.sql

@ -1,7 +1,8 @@
-- =============================================================================
-- TYPE-AGNOSTIC
-- =============================================================================
CREATE OR REPLACE FUNCTION json_empty(col JSON, path VARCHAR(500))
DROP FUNCTION IF EXISTS json_empty;;
CREATE FUNCTION json_empty(col JSON, path VARCHAR(500))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -13,7 +14,8 @@ BEGIN
RETURN 0;
END;;
CREATE OR REPLACE FUNCTION json_exists(col JSON, path VARCHAR(500))
DROP FUNCTION IF EXISTS json_exists;;
CREATE FUNCTION json_exists(col JSON, path VARCHAR(500))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
RETURN NOT json_empty(col, path);
@ -23,7 +25,8 @@ END;;
-- =============================================================================
-- NULL
-- =============================================================================
CREATE OR REPLACE FUNCTION json_null_equals(col JSON, path VARCHAR(500))
DROP FUNCTION IF EXISTS json_null_equals;;
CREATE FUNCTION json_null_equals(col JSON, path VARCHAR(500))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -37,7 +40,8 @@ BEGIN
RETURN COALESCE(JSON_TYPE(val), 'NULL') = 'NULL';
END;;
CREATE OR REPLACE FUNCTION json_null_notequals(col JSON, path VARCHAR(500))
DROP FUNCTION IF EXISTS json_null_notequals;;
CREATE FUNCTION json_null_notequals(col JSON, path VARCHAR(500))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
RETURN NOT json_null_equals(col, path);
@ -47,7 +51,8 @@ END;;
-- =============================================================================
-- TEXT
-- =============================================================================
CREATE OR REPLACE FUNCTION json_text_equals(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_equals;;
CREATE FUNCTION json_text_equals(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -61,7 +66,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) = target;
END;;
CREATE OR REPLACE FUNCTION json_text_notequals(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_notequals;;
CREATE FUNCTION json_text_notequals(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -75,7 +81,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) != target;
END;;
CREATE OR REPLACE FUNCTION json_text_lessthan(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_lessthan;;
CREATE FUNCTION json_text_lessthan(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -89,7 +96,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) < target;
END;;
CREATE OR REPLACE FUNCTION json_text_lessthanorequal(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_lessthanorequal;;
CREATE FUNCTION json_text_lessthanorequal(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -103,7 +111,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) <= target;
END;;
CREATE OR REPLACE FUNCTION json_text_greaterthan(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_greaterthan;;
CREATE FUNCTION json_text_greaterthan(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -117,7 +126,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) > target;
END;;
CREATE OR REPLACE FUNCTION json_text_greaterthanorequal(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_greaterthanorequal;;
CREATE FUNCTION json_text_greaterthanorequal(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -131,7 +141,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) >= target;
END;;
CREATE OR REPLACE FUNCTION json_text_contains(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_contains;;
CREATE FUNCTION json_text_contains(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -145,7 +156,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) LIKE CONCAT('%', target, '%');
END;;
CREATE OR REPLACE FUNCTION json_text_startswith(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_startswith;;
CREATE FUNCTION json_text_startswith(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -159,7 +171,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) LIKE CONCAT(target, '%');
END;;
CREATE OR REPLACE FUNCTION json_text_endswith(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_endswith;;
CREATE FUNCTION json_text_endswith(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -173,7 +186,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) LIKE CONCAT('%', target);
END;;
CREATE OR REPLACE FUNCTION json_text_matchs(col JSON, path VARCHAR(500), target VARCHAR(2000))
DROP FUNCTION IF EXISTS json_text_matchs;;
CREATE FUNCTION json_text_matchs(col JSON, path VARCHAR(500), target VARCHAR(2000))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -187,7 +201,8 @@ BEGIN
RETURN JSON_UNQUOTE(val) REGEXP target;
END;;
CREATE OR REPLACE FUNCTION json_text_in(col JSON, path VARCHAR(500), targets JSON)
DROP FUNCTION IF EXISTS json_text_in;;
CREATE FUNCTION json_text_in(col JSON, path VARCHAR(500), targets JSON)
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -205,7 +220,8 @@ END;;
-- =============================================================================
-- NUMBER
-- =============================================================================
CREATE OR REPLACE FUNCTION json_number_equals(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_equals;;
CREATE FUNCTION json_number_equals(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -222,7 +238,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) = target;
END;;
CREATE OR REPLACE FUNCTION json_number_notequals(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_notequals;;
CREATE FUNCTION json_number_notequals(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -239,7 +256,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) != target;
END;;
CREATE OR REPLACE FUNCTION json_number_lessthan(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_lessthan;;
CREATE FUNCTION json_number_lessthan(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -256,7 +274,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) < target;
END;;
CREATE OR REPLACE FUNCTION json_number_lessthanorequal(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_lessthanorequal;;
CREATE FUNCTION json_number_lessthanorequal(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -273,7 +292,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) <= target;
END;;
CREATE OR REPLACE FUNCTION json_number_greaterthan(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_greaterthan;;
CREATE FUNCTION json_number_greaterthan(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -290,7 +310,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) > target;
END;;
CREATE OR REPLACE FUNCTION json_number_greaterthanorequal(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
DROP FUNCTION IF EXISTS json_number_greaterthanorequal;;
CREATE FUNCTION json_number_greaterthanorequal(col JSON, path VARCHAR(500), target DECIMAL(65, 10))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -307,7 +328,8 @@ BEGIN
RETURN CAST(JSON_UNQUOTE(val) AS DECIMAL(65, 10)) >= target;
END;;
CREATE OR REPLACE FUNCTION json_number_in(col JSON, path VARCHAR(500), targets JSON)
DROP FUNCTION IF EXISTS json_number_in;;
CREATE FUNCTION json_number_in(col JSON, path VARCHAR(500), targets JSON)
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -327,7 +349,8 @@ END;;
-- =============================================================================
-- BOOLEAN
-- =============================================================================
CREATE OR REPLACE FUNCTION json_boolean_equals(col JSON, path VARCHAR(500), target TINYINT(1))
DROP FUNCTION IF EXISTS json_boolean_equals;;
CREATE FUNCTION json_boolean_equals(col JSON, path VARCHAR(500), target TINYINT(1))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -344,7 +367,8 @@ BEGIN
RETURN val = target_json;
END;;
CREATE OR REPLACE FUNCTION json_boolean_notequals(col JSON, path VARCHAR(500), target TINYINT(1))
DROP FUNCTION IF EXISTS json_boolean_notequals;;
CREATE FUNCTION json_boolean_notequals(col JSON, path VARCHAR(500), target TINYINT(1))
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;
@ -361,7 +385,8 @@ BEGIN
RETURN val != target_json;
END;;
CREATE OR REPLACE FUNCTION json_boolean_in(col JSON, path VARCHAR(500), targets JSON)
DROP FUNCTION IF EXISTS json_boolean_in;;
CREATE FUNCTION json_boolean_in(col JSON, path VARCHAR(500), targets JSON)
RETURNS TINYINT(1) DETERMINISTIC
BEGIN
DECLARE val JSON;

1629
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

59
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.cs

@ -0,0 +1,59 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.Postgres;
#nullable disable
namespace Squidex.Providers.Postgres.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(PostgresDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.Postgres.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_empty(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_exists(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_null_equals(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_null_notequals(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_equals(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_notequals(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_lessthan(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_lessthanorequal(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_greaterthan(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_greaterthanorequal(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_contains(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_startswith(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_endswith(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_matchs(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_in(jsonb, text[])");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_equals(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_notequals(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_lessthan(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_lessthanorequal(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_greaterthan(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_greaterthanorequal(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_in(jsonb, numeric[])");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_equals(jsonb, boolean)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_notequals(jsonb, boolean)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_in(jsonb, boolean[])");
}
}
}

1631
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

64
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.cs

@ -0,0 +1,64 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.SqlServer;
#nullable disable
namespace Squidex.Providers.SqlServer.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(SqlServerDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.SqlServer.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
var functions = new[]
{
"dbo.json_empty",
"dbo.json_exists",
"dbo.json_null_equals",
"dbo.json_null_notequals",
"dbo.json_text_contains",
"dbo.json_text_endswith",
"dbo.json_text_equals",
"dbo.json_text_greaterthan",
"dbo.json_text_greaterthanorequal",
"dbo.json_text_lessthan",
"dbo.json_text_lessthanorequal",
"dbo.json_text_matchs",
"dbo.json_text_notequals",
"dbo.json_text_startswith",
"dbo.json_boolean_equals",
"dbo.json_boolean_notequals",
"dbo.json_number_equals",
"dbo.json_number_greaterthan",
"dbo.json_number_greaterthanorequal",
"dbo.json_number_lessthan",
"dbo.json_number_lessthanorequal",
"dbo.json_number_notequals",
};
foreach (var function in functions)
{
migrationBuilder.Sql($"DROP FUNCTION IF EXISTS {function}");
}
}
}
}

1
backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs

@ -271,7 +271,6 @@ public static class ServiceExtensions
.AddEntityFrameworkStore<TContext, CronJobContext>();
services.AddEntityFrameworkAssetKeyValueStore<TContext, TusMetadata>();
services.AddSingletonAs<SqlDialectInitializer<TContext>>();
}
public static void AddSquidexEntityFrameworkEventStore(this IServiceCollection services, IConfiguration config)

87
backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.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.MySql;
using Squidex.Providers.MySql.App;
using Testcontainers.MySql;
@ -18,7 +20,9 @@ namespace Squidex.EntityFramework.Migrations;
[Trait("Category", "TestContainer")]
public class MySqlMigrationTests : IAsyncLifetime
{
private readonly MySqlContainer mysql = new MySqlBuilder("mysql:8.0").Build();
private readonly MySqlContainer mysql = new MySqlBuilder("mysql:8.0")
.WithCommand("--log-bin-trust-function-creators=1")
.Build();
public async ValueTask InitializeAsync()
{
@ -60,4 +64,85 @@ public class MySqlMigrationTests : IAsyncLifetime
var migrations = await dbContext.Database.GetAppliedMigrationsAsync();
Assert.NotEmpty(migrations);
}
[Fact]
public async Task Should_migrate_idempotent_and_functions_callable()
{
var connectionString = mysql.GetConnectionString();
var services =
new ServiceCollection()
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<MySqlAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<MySqlAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<MySqlAppDbContext>>();
// Run migrations twice to verify idempotency.
await databaseMigrator.InitializeAsync(default);
await databaseMigrator.InitializeAsync(default);
// Verify the json_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 json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
[Fact]
public async Task Should_migrate_when_functions_already_exist()
{
var connectionString = mysql.GetConnectionString();
var services =
new ServiceCollection()
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<MySqlAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<MySqlAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<MySqlAppDbContext>>();
// 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("20260323154443_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 json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
}

77
backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.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.SqlServer;
using Squidex.Providers.SqlServer.App;
using Testcontainers.MsSql;
@ -59,4 +61,79 @@ public class SqlServerMigrationTests : 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<SqlServerAppDbContext>(b =>
{
b.UseSqlServer(sqlServer.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<SqlServerAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<SqlServerAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<SqlServerAppDbContext>>();
// Run migrations twice to verify idempotency.
await databaseMigrator.InitializeAsync(default);
await databaseMigrator.InitializeAsync(default);
// Verify the dbo.json_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 dbo.json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
[Fact]
public async Task Should_migrate_when_functions_already_exist()
{
var services =
new ServiceCollection()
.AddDbContextFactory<SqlServerAppDbContext>(b =>
{
b.UseSqlServer(sqlServer.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<SqlServerAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<SqlServerAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<SqlServerAppDbContext>>();
// 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("20260323155035_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 dbo.json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
}

Loading…
Cancel
Save