Browse Source

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>
pull/1314/head
copilot-swe-agent[bot] 1 week ago
committed by GitHub
parent
commit
ad5e0e205e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 80
      backend/src/Squidex.Data.EntityFramework/Providers/JsonFunctionMigration.cs
  2. 22
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512143000_AddJsonFunctions.cs
  3. 22
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512143008_AddJsonFunctions.cs
  4. 22
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512143016_AddJsonFunctions.cs
  5. 1
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  6. 24
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs
  7. 19
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs
  8. 19
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs

80
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<string> 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();
}

22
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);
}
}
}

22
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);
}
}
}

22
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);
}
}
}

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)

24
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<int> 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);
}
}

19
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<int> 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);
}
}

19
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<int> 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);
}
}

Loading…
Cancel
Save