mirror of https://github.com/Squidex/squidex.git
Browse Source
* fix: use CREATE OR ALTER/REPLACE FUNCTION to prevent startup failure on restart
On application restart the hosted service fails with:
SqlException: There is already an object named 'json_exists' in the database.
The #if RELEASE guard in JsonFunction.cs skips DROP FUNCTION IF EXISTS
statements in release builds, so CREATE FUNCTION fails if the functions
already exist from a previous run.
Replace the DROP + CREATE pattern with idempotent alternatives:
- SQL Server: CREATE OR ALTER FUNCTION (supported since SQL Server 2016 SP1)
- MySQL: CREATE OR REPLACE FUNCTION
PostgreSQL was already using CREATE OR REPLACE FUNCTION correctly.
DROP FUNCTION IF EXISTS statements are removed from both SQL files.
* 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
* Add Postgres migration tests; fix image tag for arm64 compatibility
* Replace DatabaseCreator+SqlDialectInitializer with DatabaseMigrator in test fixtures
- Test fixtures now use the same code path as production (MigrateAsync)
- DatabaseCreator and SqlDialectInitializer are no longer needed and deleted
- Functions are created via the AddJsonFunctions migration, not at every startup
* Fix missing Squidex.Infrastructure using in test fixtures
* Fix test fixtures: use EnsureCreated+Dialect.InitializeAsync and per-prefix migration history
Two bugs fixed in the EF Core test fixtures (PostgresFixture, MySqlFixture, SqlServerFixture):
1. Replace DatabaseMigrator with EnsureCreatedAsync + Dialect.InitializeAsync
TestDbContext* are test-only contexts with no EF migration files, so
DatabaseMigrator<TestDbContext*>.InitializeAsync called MigrateAsync which was
a complete no-op — no tables were ever created and all integration tests failed
with 'relation does not exist'.
EnsureCreatedAsync builds the schema directly from the EF Core model, which is
the correct approach for contexts without migrations. Dialect.InitializeAsync is
then called explicitly to create the database-specific JSON functions (json_exists
etc.) that EnsureCreated does not set up.
2. Add per-prefix MigrationsHistoryTable for named ContentDbContext registrations
DynamicTables.PrepareAsync calls MigrateAsync on the named ContentDbContext
(e.g. PostgresContentDbContext) to create per-app/schema dedicated tables such
as '__c5_ContentsAll'. The migration (AddInitial) reads TableName.Prefix to
build the table name at runtime.
All named contexts shared the default '__EFMigrationsHistory' table, so after
the first prefix ran AddInitial and recorded it, every subsequent prefix saw the
migration as already applied and skipped it — leaving its dedicated tables
uncreated and causing 'relation __cN_ContentsAll does not exist' failures in
all but the first dedicated-table test.
Setting options.MigrationsHistoryTable(\$"{name}MigrationHistory") gives each
prefix its own independent migration history, so AddInitial runs once per prefix
and creates the correct tables each time.
Also add *.lscache to .gitignore (C# language server cache files).
master
committed by
GitHub
18 changed files with 5375 additions and 134 deletions
@ -1,42 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore.Infrastructure; |
|||
using Microsoft.EntityFrameworkCore.Storage; |
|||
using Squidex.Hosting; |
|||
|
|||
#pragma warning disable RECS0108 // Warns about static fields in generic types
|
|||
|
|||
namespace Squidex.Infrastructure.Migrations; |
|||
|
|||
public sealed class DatabaseCreator<TContext>(IDbContextFactory<TContext> dbContextFactory) : IInitializable |
|||
where TContext : DbContext |
|||
{ |
|||
private static readonly TimeSpan WaitTime = TimeSpan.FromSeconds(30); |
|||
|
|||
public int Order => -1000; |
|||
|
|||
public async Task InitializeAsync( |
|||
CancellationToken ct) |
|||
{ |
|||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct); |
|||
|
|||
using var cts = new CancellationTokenSource(WaitTime); |
|||
while (!await dbContext.Database.CanConnectAsync(cts.Token)) |
|||
{ |
|||
await Task.Delay(100, cts.Token); |
|||
} |
|||
|
|||
if (dbContext.Database.GetService<IDatabaseCreator>() is not RelationalDatabaseCreator relationalDatabaseCreator) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await relationalDatabaseCreator.EnsureCreatedAsync(ct); |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.EntityFrameworkCore; |
|||
using Squidex.Hosting; |
|||
|
|||
namespace Squidex.Infrastructure.Queries; |
|||
|
|||
public sealed class SqlDialectInitializer<TContext>(IDbContextFactory<TContext> dbContextFactory) |
|||
: IInitializable where TContext : DbContext |
|||
{ |
|||
public async Task InitializeAsync(CancellationToken ct) |
|||
{ |
|||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct); |
|||
if (dbContext is not IDbContextWithDialect withDialect) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await withDialect.Dialect.InitializeAsync(dbContext, ct); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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[])"); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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}"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue