From ad5e0e205e04d38a7afaf01f18ebfa6a440692a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:40:43 +0000 Subject: [PATCH] Add JSON function EF migrations Agent-Logs-Url: https://github.com/Squidex/squidex/sessions/7f1356a2-bd23-4452-a915-c46854bc604d Co-authored-by: SebastianStehle <1236435+SebastianStehle@users.noreply.github.com> --- .../Providers/JsonFunctionMigration.cs | 80 +++++++++++++++++++ .../20260512143000_AddJsonFunctions.cs | 22 +++++ .../20260512143008_AddJsonFunctions.cs | 22 +++++ .../20260512143016_AddJsonFunctions.cs | 22 +++++ .../ServiceExtensions.cs | 1 - .../Migrations/MySqlMigrationTests.cs | 24 +++++- .../Migrations/PostgresMigrationTests.cs | 19 +++++ .../Migrations/SqlServerMigrationTests.cs | 19 +++++ 8 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/JsonFunctionMigration.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512143000_AddJsonFunctions.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512143008_AddJsonFunctions.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512143016_AddJsonFunctions.cs diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/JsonFunctionMigration.cs b/backend/src/Squidex.Data.EntityFramework/Providers/JsonFunctionMigration.cs new file mode 100644 index 000000000..7ce43d5ff --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/JsonFunctionMigration.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Squidex.Providers; + +internal static partial class JsonFunctionMigration +{ + public static void Create(MigrationBuilder migrationBuilder, Type anchorType, string resourceName, bool splitStatements) + { + var sqlText = ReadSql(anchorType, resourceName); + + if (splitStatements) + { + foreach (var statement in SplitStatements(sqlText)) + { + if (statement.StartsWith("CREATE", StringComparison.OrdinalIgnoreCase)) + { + migrationBuilder.Sql(statement); + } + } + } + else + { + migrationBuilder.Sql(sqlText); + } + } + + public static void Drop(MigrationBuilder migrationBuilder, Type anchorType, string resourceName, bool splitStatements) + { + var sqlText = ReadSql(anchorType, resourceName); + + if (splitStatements) + { + foreach (var statement in SplitStatements(sqlText).Reverse()) + { + if (statement.StartsWith("DROP", StringComparison.OrdinalIgnoreCase)) + { + migrationBuilder.Sql(statement); + } + } + } + else + { + foreach (var functionName in ParseFunctions(sqlText).Reverse()) + { + migrationBuilder.Sql($"DROP FUNCTION IF EXISTS {functionName} CASCADE;"); + } + } + } + + private static string ReadSql(Type anchorType, string resourceName) + { + using var sqlStream = anchorType.Assembly.GetManifestResourceStream(resourceName) ?? + throw new InvalidOperationException($"Cannot find embedded resource '{resourceName}'."); + + using var reader = new StreamReader(sqlStream); + + return reader.ReadToEnd(); + } + + private static string[] SplitStatements(string sqlText) + { + return sqlText.Split(";;", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + private static IEnumerable ParseFunctions(string sqlText) + { + return FunctionRegex().Matches(sqlText).Select(x => x.Groups[1].Value); + } + + [GeneratedRegex(@"CREATE\s+OR\s+REPLACE\s+FUNCTION\s+([a-zA-Z0-9_]+)\s*\(", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)] + private static partial Regex FunctionRegex(); +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512143000_AddJsonFunctions.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512143000_AddJsonFunctions.cs new file mode 100644 index 000000000..2c72b9963 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512143000_AddJsonFunctions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.MySql.App.Migrations +{ + [DbContext(typeof(MySqlAppDbContext))] + [Migration("20260512143000_AddJsonFunctions")] + internal sealed class AddJsonFunctions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Create(migrationBuilder, typeof(MySqlDialect), "Squidex.Providers.MySql.json_function.sql", splitStatements: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Drop(migrationBuilder, typeof(MySqlDialect), "Squidex.Providers.MySql.json_function.sql", splitStatements: true); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512143008_AddJsonFunctions.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512143008_AddJsonFunctions.cs new file mode 100644 index 000000000..017118dbd --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512143008_AddJsonFunctions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.Postgres.App.Migrations +{ + [DbContext(typeof(PostgresAppDbContext))] + [Migration("20260512143008_AddJsonFunctions")] + internal sealed class AddJsonFunctions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Create(migrationBuilder, typeof(PostgresDialect), "Squidex.Providers.Postgres.json_function.sql", splitStatements: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Drop(migrationBuilder, typeof(PostgresDialect), "Squidex.Providers.Postgres.json_function.sql", splitStatements: false); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512143016_AddJsonFunctions.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512143016_AddJsonFunctions.cs new file mode 100644 index 000000000..9f73d87ba --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512143016_AddJsonFunctions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.SqlServer.App.Migrations +{ + [DbContext(typeof(SqlServerAppDbContext))] + [Migration("20260512143016_AddJsonFunctions")] + internal sealed class AddJsonFunctions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Create(migrationBuilder, typeof(SqlServerDialect), "Squidex.Providers.SqlServer.json_function.sql", splitStatements: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + JsonFunctionMigration.Drop(migrationBuilder, typeof(SqlServerDialect), "Squidex.Providers.SqlServer.json_function.sql", splitStatements: true); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index 1679cdefb..8c0ad4c61 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -271,7 +271,6 @@ public static class ServiceExtensions .AddEntityFrameworkStore(); services.AddEntityFrameworkAssetKeyValueStore(); - services.AddSingletonAs>(); } public static void AddSquidexEntityFrameworkEventStore(this IServiceCollection services, IConfiguration config) diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs index 722021ea8..94df5d10e 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Data; +using System.Globalization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; @@ -18,7 +20,10 @@ 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() { @@ -58,6 +63,23 @@ public class MySqlMigrationTests : IAsyncLifetime await using var dbContext = await databaseFactory.CreateDbContextAsync(); var migrations = await dbContext.Database.GetAppliedMigrationsAsync(); + var result = await ExecuteScalarAsync(dbContext, "SELECT json_empty(NULL, '$')"); + Assert.NotEmpty(migrations); + Assert.Equal(1, result); + } + + private static async Task ExecuteScalarAsync(DbContext dbContext, string sql) + { + var connection = dbContext.Database.GetDbConnection(); + if (connection.State != ConnectionState.Open) + { + await connection.OpenAsync(); + } + + await using var command = connection.CreateCommand(); + command.CommandText = sql; + + return Convert.ToInt32(await command.ExecuteScalarAsync(), CultureInfo.InvariantCulture); } } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs index d7dd7a589..050adb46a 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Data; +using System.Globalization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; @@ -57,6 +59,23 @@ public class PostgresMigrationTests : IAsyncLifetime await using var dbContext = await databaseFactory.CreateDbContextAsync(); var migrations = await dbContext.Database.GetAppliedMigrationsAsync(); + var result = await ExecuteScalarAsync(dbContext, "SELECT CASE WHEN jsonb_empty(NULL) THEN 1 ELSE 0 END"); + Assert.NotEmpty(migrations); + Assert.Equal(1, result); + } + + private static async Task ExecuteScalarAsync(DbContext dbContext, string sql) + { + var connection = dbContext.Database.GetDbConnection(); + if (connection.State != ConnectionState.Open) + { + await connection.OpenAsync(); + } + + await using var command = connection.CreateCommand(); + command.CommandText = sql; + + return Convert.ToInt32(await command.ExecuteScalarAsync(), CultureInfo.InvariantCulture); } } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs index 38808e059..186b8b4ef 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Data; +using System.Globalization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; @@ -57,6 +59,23 @@ public class SqlServerMigrationTests : IAsyncLifetime await using var dbContext = await databaseFactory.CreateDbContextAsync(); var migrations = await dbContext.Database.GetAppliedMigrationsAsync(); + var result = await ExecuteScalarAsync(dbContext, "SELECT CAST(dbo.json_empty(NULL, '$') AS INT)"); + Assert.NotEmpty(migrations); + Assert.Equal(1, result); + } + + private static async Task ExecuteScalarAsync(DbContext dbContext, string sql) + { + var connection = dbContext.Database.GetDbConnection(); + if (connection.State != ConnectionState.Open) + { + await connection.OpenAsync(); + } + + await using var command = connection.CreateCommand(); + command.CommandText = sql; + + return Convert.ToInt32(await command.ExecuteScalarAsync(), CultureInfo.InvariantCulture); } }