diff --git a/backend/Squidex.sln b/backend/Squidex.sln index d1a1ce102..8cd4e51bb 100644 --- a/backend/Squidex.sln +++ b/backend/Squidex.sln @@ -56,7 +56,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Data.MongoDb", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Data.Tests", "tests\Squidex.Data.Tests\Squidex.Data.Tests.csproj", "{AA2F3C32-E3C8-4DF3-A365-F25C7EC19BCD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Data.EntityFramework", "src\Squidex.Data.EntityFramework\Squidex.Data.EntityFramework.csproj", "{0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Data.EntityFramework", "src\Squidex.Data.EntityFramework\Squidex.Data.EntityFramework.csproj", "{0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Data.Tests.CodeGenerator", "tests\Squidex.Data.Tests.CodeGenerator\Squidex.Data.Tests.CodeGenerator.csproj", "{2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -276,6 +278,18 @@ Global {0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA}.Release|x64.Build.0 = Release|Any CPU {0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA}.Release|x86.ActiveCfg = Release|Any CPU {0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA}.Release|x86.Build.0 = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|x64.Build.0 = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Debug|x86.Build.0 = Debug|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|Any CPU.Build.0 = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|x64.ActiveCfg = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|x64.Build.0 = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|x86.ActiveCfg = Release|Any CPU + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -299,6 +313,7 @@ Global {F754F05E-02FF-47B2-AB46-BB05C7E6B29D} = {3378B841-53F8-48CC-87C1-1B30CF912BFD} {AA2F3C32-E3C8-4DF3-A365-F25C7EC19BCD} = {3378B841-53F8-48CC-87C1-1B30CF912BFD} {0348CFDA-4DA1-4DB5-9C6F-0D234FE3E4DA} = {3378B841-53F8-48CC-87C1-1B30CF912BFD} + {2E4CC21D-3AB9-4B8C-A13E-348AA3E2F328} = {3378B841-53F8-48CC-87C1-1B30CF912BFD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs index 8f0eb629c..e7277b68e 100644 --- a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs @@ -52,6 +52,7 @@ public class AppDbContext(DbContextOptions options, IJsonSerializer jsonSerializ builder.UseSettings(jsonSerializer, jsonColumnType); builder.UseTags(jsonSerializer, jsonColumnType); builder.UseTeams(jsonSerializer, jsonColumnType); + builder.UseTextIndex(); builder.UseUsage(); builder.UseUsageTracking(jsonSerializer, jsonColumnType); builder.UseYDotNet(); diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs index da9bb7860..4ced697c4 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs @@ -6,7 +6,6 @@ // ========================================================================== using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Text.State; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; @@ -16,14 +15,6 @@ public static class EFContentBuilder { public static void UseContent(this ModelBuilder builder, IJsonSerializer jsonSerializer, string? jsonColumn) { - builder.Entity(b => - { - b.ToTable("TextState"); - b.HasKey(x => x.UniqueContentId); - b.Property(x => x.UniqueContentId).AsString(); - b.Property(x => x.State).AsString(); - }); - builder.UseContentEntity("ContentsAll", jsonSerializer, jsonColumn); builder.UseContentReference("ContentReferencesAll"); diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs index c8d5aa05a..b7cec896d 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs @@ -35,7 +35,7 @@ internal static class Extensions public static void LimitFields(this ContentData data, IReadOnlySet fields) { List? toDelete = null; - foreach (var (key, value) in data) + foreach (var (key, _) in data) { if (!fields.Any(x => IsMatch(key, x))) { diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextBuilder.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextBuilder.cs new file mode 100644 index 000000000..989820325 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextBuilder.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.Contents.Text.State; +using Squidex.Infrastructure; + +namespace Microsoft.EntityFrameworkCore; + +public static class EFTextBuilder +{ + public static void UseTextIndex(this ModelBuilder builder) + { + builder.Entity(b => + { + b.ToTable("TextState"); + b.HasKey(x => x.UniqueContentId); + b.Property(x => x.UniqueContentId).AsString(); + b.Property(x => x.State).AsString(); + }); + + builder.Entity(b => + { + b.ToTable("Texts"); + b.Property(x => x.Id).HasMaxLength(400); + b.Property(x => x.AppId).AsString(); + b.Property(x => x.SchemaId).AsString(); + b.Property(x => x.ContentId).AsString(); + }); + + builder.Entity(b => + { + b.ToTable("Geos"); + b.Property(x => x.Id).HasMaxLength(400); + b.Property(x => x.AppId).AsString(); + b.Property(x => x.SchemaId).AsString(); + b.Property(x => x.ContentId).AsString(); + b.Property(x => x.GeoField).HasMaxLength(255); + }); + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs new file mode 100644 index 000000000..52e870dc3 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs @@ -0,0 +1,301 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using EFCore.BulkExtensions; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using NetTopologySuite.Geometries; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Squidex.Providers.SqlServer; + +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class EFTextIndex(IDbContextFactory dbContextFactory, SqlDialect dialect) + : ITextIndex, IInitializable, IDeleter where TContext : DbContext +{ + private record struct SearchOperation + { + required public App App { get; init; } + + required public List<(DomainId Id, double Score)> Results { get; init; } + + required public string SearchTerms { get; init; } + + required public int Take { get; set; } + + required public SearchScope SearchScope { get; init; } + } + + public async Task InitializeAsync( + CancellationToken ct) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Database.CreateTextIndexAsync(dialect, "IDX_Text", "Texts", "Texts", ct); + await dbContext.Database.CreateGeoIndexAsync(dialect, "IDX_Geo", "Geos", "GeoObject", ct); + } + + async Task IDeleter.DeleteAppAsync(App app, + CancellationToken ct) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Set().Where(x => x.AppId == app.Id) + .ExecuteDeleteAsync(ct); + + await dbContext.Set().Where(x => x.AppId == app.Id) + .ExecuteDeleteAsync(ct); + } + + async Task IDeleter.DeleteSchemaAsync(App app, Schema schema, + CancellationToken ct) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Set().Where(x => x.AppId == app.Id && x.SchemaId == schema.Id) + .ExecuteDeleteAsync(ct); + + await dbContext.Set().Where(x => x.AppId == app.Id && x.SchemaId == schema.Id) + .ExecuteDeleteAsync(ct); + } + + public async Task ClearAsync(CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Set() + .ExecuteDeleteAsync(ct); + + await dbContext.Set() + .ExecuteDeleteAsync(ct); + } + + public async Task?> SearchAsync(App app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + await using var dbContext = await CreateDbContextAsync(ct); + + var point = new Point(query.Longitude, query.Latitude) { SRID = 4326 }; + + // The distance must be converted to decrees (in contrast to MongoDB, which uses radian). + var degrees = query.Radius / 111320; + + var ids = + await dbContext.Set() + .Where(x => x.AppId == app.Id) + .Where(x => x.SchemaId == query.SchemaId) + .Where(x => x.GeoField == query.Field) + .Where(x => x.GeoObject.Distance(point) < degrees) + .WhereScope(scope) + .Select(x => x.ContentId) + .ToListAsync(ct); + + return ids; + } + + public async Task?> SearchAsync(App app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + if (string.IsNullOrWhiteSpace(query.Text)) + { + return null; + } + + // Use a custom tokenizer to leverage stop words from multiple languages. + var search = new SearchOperation + { + App = app, + SearchTerms = Tokenizer.Query(query.Text), + SearchScope = scope, + Results = [], + Take = query.Take, + }; + + await using var dbContext = await CreateDbContextAsync(ct); + + if (query.RequiredSchemaIds?.Count > 0) + { + await SearchBySchemaAsync(dbContext, search, query.RequiredSchemaIds, 1, ct); + } + else if (query.PreferredSchemaId == null) + { + await SearchByAppAsync(dbContext, search, 1, ct); + } + else + { + // We cannot write queries that prefer results from the same schema, therefore make two queries. + search.Take /= 2; + + // Increasing the scoring of the results from the schema by 10 percent. + await SearchBySchemaAsync(dbContext, search, Enumerable.Repeat(query.PreferredSchemaId.Value, 1), 1.1, ct); + await SearchByAppAsync(dbContext, search, 1, ct); + } + + return search.Results.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + } + + private Task SearchBySchemaAsync(TContext context, SearchOperation search, IEnumerable schemaIds, double factor, + CancellationToken ct = default) + { + var queryBuilder = + new SqlQueryBuilder(dialect, "Texts") + .Where(ClrFilter.Eq("AppId", search.App.Id)) + .Where(ClrFilter.In("SchemaId", schemaIds.ToList())) + .WhereMatch("Texts", search.SearchTerms) + .WhereScope(search.SearchScope); + + return SearchAsync(context, search, queryBuilder, factor, ct); + } + + private Task SearchByAppAsync(TContext context, SearchOperation search, double factor, + CancellationToken ct = default) + { + var queryBuilder = + new SqlQueryBuilder(dialect, "Texts") + .Where(ClrFilter.Eq("AppId", search.App.Id)) + .WhereMatch("Texts", search.SearchTerms) + .WhereScope(search.SearchScope); + + return SearchAsync(context, search, queryBuilder, factor, ct); + } + + private static async Task SearchAsync(TContext context, SearchOperation search, SqlQueryBuilder queryBuilder, double factor, + CancellationToken ct) + { + var (sql, parameters) = queryBuilder.Compile(); + + var ids = + await context.Set().FromSqlRaw(sql, parameters) + .Select(x => x.ContentId) + .ToListAsync(ct); + + search.Results.AddRange(ids.Select(x => (x, 1 * factor))); + } + + public async Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + var insertsText = new List(); + var insertsGeo = new List(); + + foreach (var batch in commands.Batch(1000)) + { + foreach (var command in batch) + { + var (appId, contentId) = command.UniqueContentId; + var id = $"{appId}_{contentId}_{command.Stage}"; + + switch (command) + { + case UpsertIndexEntry upsert: + if (upsert.Texts != null) + { + insertsText.Add(new EFTextIndexTextEntity + { + Id = id, + AppId = appId, + ContentId = contentId, + SchemaId = upsert.SchemaId.Id, + ServeAll = upsert.ServeAll, + ServePublished = upsert.ServePublished, + Stage = upsert.Stage, + Texts = Tokenizer.Terms(upsert.Texts), + }); + } + + foreach (var (field, obj) in upsert.GeoObjects.OrEmpty()) + { + obj.SRID = 4326; + + if (!obj.IsValid) + { + continue; + } + + var entity = new EFTextIndexGeoEntity + { + Id = id, + AppId = appId, + ContentId = contentId, + GeoField = field, + GeoObject = obj, + SchemaId = upsert.SchemaId.Id, + ServeAll = upsert.ServeAll, + ServePublished = upsert.ServePublished, + Stage = upsert.Stage, + }; + + // We can only check the validatity by inserting them one by one. + if (dialect is SqlServerDialect) + { + try + { + await dbContext.Set().AddAsync(entity, ct); + await dbContext.SaveChangesAsync(ct); + } + catch (DbUpdateException ex) when (ex.InnerException is SqlException { Number: 8023 }) + { + // Geo object is not valid. + dbContext.Entry(entity).State = EntityState.Detached; + } + } + else + { + insertsGeo.Add(entity); + } + } + + break; + case DeleteIndexEntry: + await dbContext.Set() + .Where(x => x.Id == id) + .ExecuteDeleteAsync(ct); + + await dbContext.Set() + .Where(x => x.Id == id) + .ExecuteDeleteAsync(ct); + break; + case UpdateIndexEntry update: + await dbContext.Set() + .Where(x => x.Id == id) + .ExecuteUpdateAsync(u => u + .SetProperty(x => x.ServeAll, update.ServeAll) + .SetProperty(x => x.ServePublished, update.ServePublished), + ct); + + await dbContext.Set() + .Where(x => x.Id == id) + .ExecuteUpdateAsync(u => u + .SetProperty(x => x.ServeAll, update.ServeAll) + .SetProperty(x => x.ServePublished, update.ServePublished), + ct); + break; + } + } + } + + await dbContext.BulkInsertOrUpdateAsync(insertsText, cancellationToken: ct); + await dbContext.BulkInsertOrUpdateAsync(insertsGeo, cancellationToken: ct); + } + + private Task CreateDbContextAsync(CancellationToken ct) + { + return dbContextFactory.CreateDbContextAsync(ct); + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexGeoEntity.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexGeoEntity.cs new file mode 100644 index 000000000..797a776b9 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexGeoEntity.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using NetTopologySuite.Geometries; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class EFTextIndexGeoEntity +{ + [Key] + required public string Id { get; set; } + + public DomainId AppId { get; set; } + + public DomainId SchemaId { get; set; } + + public DomainId ContentId { get; set; } + + public byte Stage { get; set; } + + public bool ServeAll { get; set; } + + public bool ServePublished { get; set; } + + public string GeoField { get; set; } + + public Geometry GeoObject { get; set; } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexTextEntity.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexTextEntity.cs new file mode 100644 index 000000000..03b9b7f7e --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexTextEntity.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class EFTextIndexTextEntity +{ + [Key] + required public string Id { get; set; } + + public DomainId AppId { get; set; } + + public DomainId SchemaId { get; set; } + + public DomainId ContentId { get; set; } + + public byte Stage { get; set; } + + public bool ServeAll { get; set; } + + public bool ServePublished { get; set; } + + public string Texts { get; set; } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/Extensions.cs new file mode 100644 index 000000000..83235a9b6 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/Extensions.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +internal static class Extensions +{ + public static SqlQueryBuilder WhereScope(this SqlQueryBuilder queryBuilder, SearchScope scope) + { + if (scope == SearchScope.All) + { + return queryBuilder.Where(ClrFilter.Eq("ServeAll", true)); + } + else + { + return queryBuilder.Where(ClrFilter.Eq("ServePublished", true)); + } + } + + public static IQueryable WhereScope(this IQueryable query, SearchScope scope) + { + if (scope == SearchScope.All) + { + return query.Where(x => x.ServeAll); + } + else + { + return query.Where(x => x.ServePublished); + } + } + + public static IQueryable WhereScope(this IQueryable query, SearchScope scope) + { + if (scope == SearchScope.All) + { + return query.Where(x => x.ServeAll); + } + else + { + return query.Where(x => x.ServePublished); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/NullTextIndex.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/NullTextIndex.cs deleted file mode 100644 index cbc952acf..000000000 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/NullTextIndex.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.Text; - -public sealed class NullTextIndex : ITextIndex -{ - public Task ClearAsync( - CancellationToken ct = default) - { - return Task.CompletedTask; - } - - public Task ExecuteAsync(IndexCommand[] commands, - CancellationToken ct = default) - { - return Task.CompletedTask; - } - - public Task?> SearchAsync(App app, TextQuery query, SearchScope scope, - CancellationToken ct = default) - { - return Task.FromResult?>(null); - } - - public Task?> SearchAsync(App app, GeoQuery query, SearchScope scope, - CancellationToken ct = default) - { - return Task.FromResult?>(null); - } -} diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs index 733789a9b..852f918a4 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs @@ -10,19 +10,29 @@ 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(IDbContextFactory 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 context = await dbContextFactory.CreateDbContextAsync(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 (context.Database.GetService() is not RelationalDatabaseCreator relationalDatabaseCreator) + if (dbContext.Database.GetService() is not RelationalDatabaseCreator relationalDatabaseCreator) { return; } diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs index 8b01dac63..8d4861c99 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs @@ -8,18 +8,28 @@ using Microsoft.EntityFrameworkCore; using Squidex.Hosting; +#pragma warning disable RECS0108 // Warns about static fields in generic types + namespace Squidex.Infrastructure.Migrations; public sealed class DatabaseMigrator(IDbContextFactory 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 context = await dbContextFactory.CreateDbContextAsync(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); + } - await context.Database.MigrateAsync(ct); + await dbContext.Database.MigrateAsync(ct); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/QueryExtensions.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/QueryExtensions.cs new file mode 100644 index 000000000..137421452 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/QueryExtensions.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Squidex.Infrastructure.Queries; + +public static class QueryExtensions +{ + public static async Task CreateGeoIndexAsync(this DatabaseFacade database, SqlDialect dialect, string name, string table, string column, + CancellationToken ct = default) + { + var sql = dialect.GeoIndex(name, table, column); + try + { + await database.ExecuteSqlRawAsync(sql, ct); + } + catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name)) + { + // NOOP + } + } + + public static async Task CreateTextIndexAsync(this DatabaseFacade database, SqlDialect dialect, string name, string table, string column, + CancellationToken ct = default) + { + var prepareSql = dialect.TextIndexPrepare(name); + + if (!string.IsNullOrWhiteSpace(prepareSql)) + { + try + { + await database.ExecuteSqlRawAsync(prepareSql, ct); + } + catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name)) + { + // NOOP + } + } + + var sql = dialect.TextIndex(name, table, column); + try + { + await database.ExecuteSqlRawAsync(sql, ct); + } + catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name)) + { + // NOOP + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialect.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialect.cs index abd792fbe..9f08cd907 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialect.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialect.cs @@ -14,6 +14,11 @@ public class SqlDialect { private const string Tab = " "; + public virtual bool IsDuplicateIndexException(Exception exception, string name) + { + return false; + } + public virtual string BuildSelectStatement(SqlQuery request) { var sb = new StringBuilder("SELECT"); @@ -47,6 +52,21 @@ public class SqlDialect return sb.ToString(); } + public virtual string GeoIndex(string name, string table, string field) + { + throw new NotSupportedException(); + } + + public virtual string TextIndex(string name, string table, string field) + { + throw new NotSupportedException(); + } + + public virtual string TextIndexPrepare(string name) + { + return string.Empty; + } + public virtual string FormatLimitOffset(long limit, long offset, bool hasOrder) { var hasLimit = limit > 0 && limit < long.MaxValue; @@ -119,6 +139,11 @@ public class SqlDialect return $"{FormatField(path, isJson)} {FormatOperator(op, ClrValue.Null)} ({query})"; } + public virtual string WhereMatch(PropertyPath path, string query, SqlParams queryParameters) + { + throw new NotSupportedException(); + } + protected virtual string FormatValues(CompareOperator op, ClrValue value, SqlParams queryParameters) { if (!value.IsList && value.ValueType == ClrValueType.Null) diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs index 5b573c337..1d3bf3ef0 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs @@ -35,6 +35,12 @@ public class SqlQueryBuilder(SqlDialect dialect, string table, SqlParams? parame return this; } + public SqlQueryBuilder WhereMatch(PropertyPath path, string query) + { + sqlQuery.Where.Add(dialect.WhereMatch(Visit(path), query, sqlParameters)); + return this; + } + public SqlQueryBuilder Order(PropertyPath path, SortOrder order) { sqlQuery.Order.Add(dialect.OrderBy(Visit(path), order, IsJsonPath(path))); diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.Designer.cs new file mode 100644 index 000000000..0af9eecdc --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.Designer.cs @@ -0,0 +1,1533 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Squidex.Providers.MySql; + +#nullable disable + +namespace Squidex.Providers.MySql.Migrations +{ + [DbContext(typeof(MySqlDbContext))] + [Migration("20250220163956_AddFullText")] + partial class AddFullText + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("varchar(255)"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("Scopes") + .HasColumnType("longtext"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("varchar(255)"); + + b.Property("ApplicationId") + .HasColumnType("varchar(255)"); + + b.Property("AuthorizationId") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Payload") + .HasColumnType("longtext"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("RedemptionDate") + .HasColumnType("datetime(6)"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("LastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedCreated") + .HasColumnType("datetime(6)") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("IsProtected") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("json"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("json"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NewData") + .HasColumnType("json"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("json"); + + b.Property("ScheduledAt") + .HasColumnType("datetime(6)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("json"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NewData") + .HasColumnType("json"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("json"); + + b.Property("ScheduledAt") + .HasColumnType("datetime(6)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEventEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Job") + .IsRequired() + .HasColumnType("json"); + + b.Property("JobResult") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LastDump") + .HasColumnType("longtext"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("NextAttempt") + .HasColumnType("datetime(6)"); + + b.Property("NumCalls") + .HasColumnType("int"); + + b.Property("Result") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RuleId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.ToTable("RuleEvents"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAuthDomain") + .HasColumnType("longtext") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EventStream") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("json"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("IsLocked") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Category") + .HasColumnType("varchar(255)"); + + b.Property("CounterKey") + .HasColumnType("varchar(255)"); + + b.Property("CounterValue") + .HasColumnType("double"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ChannelName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("MessageHeaders") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("QueueName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TimeHandled") + .HasColumnType("datetime(6)"); + + b.Property("TimeToLive") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasColumnType("varchar(255)"); + + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("ValueFormat") + .HasColumnType("longtext"); + + b.Property("ValueType") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.cs new file mode 100644 index 000000000..c692ce084 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; + +#nullable disable + +namespace Squidex.Providers.MySql.Migrations +{ + /// + public partial class AddFullText : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Geos", + columns: table => new + { + Id = table.Column(type: "varchar(400)", maxLength: 400, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AppId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SchemaId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ContentId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Stage = table.Column(type: "tinyint unsigned", nullable: false), + ServeAll = table.Column(type: "tinyint(1)", nullable: false), + ServePublished = table.Column(type: "tinyint(1)", nullable: false), + GeoField = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + GeoObject = table.Column(type: "geometry", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Geos", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Texts", + columns: table => new + { + Id = table.Column(type: "varchar(400)", maxLength: 400, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AppId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SchemaId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ContentId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Stage = table.Column(type: "tinyint unsigned", nullable: false), + ServeAll = table.Column(type: "tinyint(1)", nullable: false), + ServePublished = table.Column(type: "tinyint(1)", nullable: false), + Texts = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Texts", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Geos"); + + migrationBuilder.DropTable( + name: "Texts"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/MySqlDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/MySqlDbContextModelSnapshot.cs index 22132178e..fa307c992 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/MySqlDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/MySqlDbContextModelSnapshot.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; using Squidex.Providers.MySql; #nullable disable @@ -17,7 +18,7 @@ namespace Squidex.Providers.MySql.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "8.0.13") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -763,6 +764,89 @@ namespace Squidex.Providers.MySql.Migrations b.ToTable("ContentReferencesPublished", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => { b.Property("UniqueContentId") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContextDesignTimeFactory.cs index be488f6fe..e319861ab 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContextDesignTimeFactory.cs @@ -19,7 +19,10 @@ public sealed class MySqlDbContextDesignTimeFactory : IDesignTimeDbContextFactor const string ConnectionString = "Server=localhost;Port=33060;Database=test;User=mysql;Password=mysql"; var builder = new DbContextOptionsBuilder() - .UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString)); + .UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString), options => + { + options.UseNetTopologySuite(); + }); return new MySqlDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs index 0f8a6e552..8d8e41dce 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using MySqlConnector; using Squidex.Infrastructure.Queries; namespace Squidex.Providers.MySql; @@ -17,6 +18,21 @@ public sealed class MySqlDialect : SqlDialect { } + public override bool IsDuplicateIndexException(Exception exception, string name) + { + return exception is MySqlException ex && ex.Number == 1061; + } + + public override string GeoIndex(string name, string table, string field) + { + return $"CREATE SPATIAL INDEX {name} ON {FormatTable(table)} ({FormatField(field, false)});"; + } + + public override string TextIndex(string name, string table, string field) + { + return $"CREATE FULLTEXT INDEX {name} ON {FormatTable(table)} ({FormatField(field, false)});"; + } + public override string FormatLimitOffset(long limit, long offset, bool hasOrder) { var hasLimit = limit > 0 && limit < long.MaxValue; @@ -70,6 +86,11 @@ public sealed class MySqlDialect : SqlDialect return base.OrderBy(path, order, isJson); } + public override string WhereMatch(PropertyPath path, string query, SqlParams queryParameters) + { + return $"MATCH({FormatField(path, false)}) AGAINST({queryParameters.AddPositional(query)})"; + } + public override string Where(PropertyPath path, CompareOperator op, ClrValue value, SqlParams queryParameters, bool isJson) { if (isJson) diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.Designer.cs new file mode 100644 index 000000000..e1c93212a --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.Designer.cs @@ -0,0 +1,1534 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Squidex.Providers.Postgres; + +#nullable disable + +namespace Squidex.Providers.Postgres.Migrations +{ + [DbContext(typeof(PostgresDbContext))] + [Migration("20250220164002_AddFullText")] + partial class AddFullText + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("text") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsProtected") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NewData") + .HasColumnType("jsonb"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ScheduleJob") + .HasColumnType("jsonb"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TranslationStatus") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NewData") + .HasColumnType("jsonb"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ScheduleJob") + .HasColumnType("jsonb"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TranslationStatus") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEventEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Job") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("JobResult") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastDump") + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("NextAttempt") + .HasColumnType("timestamp with time zone"); + + b.Property("NumCalls") + .HasColumnType("integer"); + + b.Property("Result") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RuleId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("RuleEvents"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAuthDomain") + .HasColumnType("text") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("text") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventStream") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("CounterKey") + .HasColumnType("text"); + + b.Property("CounterValue") + .HasColumnType("double precision"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ChannelName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("MessageHeaders") + .IsRequired() + .HasColumnType("text"); + + b.Property("QueueName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TimeHandled") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeToLive") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ValueFormat") + .HasColumnType("text"); + + b.Property("ValueType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.cs new file mode 100644 index 000000000..0b1b02f15 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; + +#nullable disable + +namespace Squidex.Providers.Postgres.Migrations +{ + /// + public partial class AddFullText : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:postgis", ",,"); + + migrationBuilder.CreateTable( + name: "Geos", + columns: table => new + { + Id = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + AppId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + SchemaId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + ContentId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Stage = table.Column(type: "smallint", nullable: false), + ServeAll = table.Column(type: "boolean", nullable: false), + ServePublished = table.Column(type: "boolean", nullable: false), + GeoField = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + GeoObject = table.Column(type: "geometry", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Geos", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Texts", + columns: table => new + { + Id = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + AppId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + SchemaId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + ContentId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Stage = table.Column(type: "smallint", nullable: false), + ServeAll = table.Column(type: "boolean", nullable: false), + ServePublished = table.Column(type: "boolean", nullable: false), + Texts = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Texts", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Geos"); + + migrationBuilder.DropTable( + name: "Texts"); + + migrationBuilder.AlterDatabase() + .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/PostgresDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/PostgresDbContextModelSnapshot.cs index 481a27c52..bebaa63df 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/PostgresDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/PostgresDbContextModelSnapshot.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Squidex.Providers.Postgres; @@ -17,9 +18,10 @@ namespace Squidex.Providers.Postgres.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "8.0.13") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => @@ -763,6 +765,89 @@ namespace Squidex.Providers.Postgres.Migrations b.ToTable("ContentReferencesPublished", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => { b.Property("UniqueContentId") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContextDesignTimeFactory.cs index 1619914d2..d0f8a1ed4 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContextDesignTimeFactory.cs @@ -19,7 +19,10 @@ public sealed class PostgresDbContextDesignTimeFactory : IDesignTimeDbContextFac const string ConnectionString = "Server=localhost;Port=54320;Database=test;User=postgres;Password=postgres"; var builder = new DbContextOptionsBuilder() - .UseNpgsql(ConnectionString); + .UseNpgsql(ConnectionString, options => + { + options.UseNetTopologySuite(); + }); return new PostgresDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs index 085cb04ac..16a4f43d0 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Npgsql; using Squidex.Infrastructure.Queries; namespace Squidex.Providers.Postgres; @@ -17,6 +18,21 @@ public class PostgresDialect : SqlDialect { } + public override bool IsDuplicateIndexException(Exception exception, string name) + { + return exception is PostgresException ex && ex.SqlState == "42P07"; + } + + public override string GeoIndex(string name, string table, string field) + { + return $"CREATE INDEX {name} ON {FormatTable(table)} USING GIST ({FormatField(field, false)});"; + } + + public override string TextIndex(string name, string table, string field) + { + return $"CREATE INDEX {name} ON {FormatTable(table)} USING GIN (to_tsvector('simple', {FormatField(field, false)}));"; + } + protected override string FormatTable(string tableName) { return $"\"{tableName}\""; @@ -35,6 +51,16 @@ public class PostgresDialect : SqlDialect return base.OrderBy(path, order, isJson); } + public override string WhereMatch(PropertyPath path, string query, SqlParams queryParameters) + { + if (query.Contains(' ', StringComparison.OrdinalIgnoreCase)) + { + query = query.Replace(" ", " & ", StringComparison.Ordinal); + } + + return $"to_tsvector('simple', {FormatField(path, false)}) @@ to_tsquery({queryParameters.AddPositional(query)})"; + } + public override string Where(PropertyPath path, CompareOperator op, ClrValue value, SqlParams queryParameters, bool isJson) { if (isJson) diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.Designer.cs new file mode 100644 index 000000000..4445525cb --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.Designer.cs @@ -0,0 +1,1536 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Squidex.Providers.SqlServer; + +#nullable disable + +namespace Squidex.Providers.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerDbContext))] + [Migration("20250220164010_AddFullText")] + partial class AddFullText + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Expires") + .HasColumnType("datetimeoffset"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedCreated") + .HasColumnType("datetimeoffset") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsProtected") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NewData") + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledAt") + .HasColumnType("datetimeoffset"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NewData") + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledAt") + .HasColumnType("datetimeoffset"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geography"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEventEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("Expires") + .HasColumnType("datetimeoffset"); + + b.Property("Job") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobResult") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastDump") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("NextAttempt") + .HasColumnType("datetimeoffset"); + + b.Property("NumCalls") + .HasColumnType("int"); + + b.Property("Result") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RuleId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.ToTable("RuleEvents"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAuthDomain") + .HasColumnType("nvarchar(max)") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EventStream") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Category") + .HasColumnType("nvarchar(450)"); + + b.Property("CounterKey") + .HasColumnType("nvarchar(450)"); + + b.Property("CounterValue") + .HasColumnType("float"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ChannelName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("MessageHeaders") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QueueName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TimeHandled") + .HasColumnType("datetime2"); + + b.Property("TimeToLive") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasColumnType("nvarchar(450)"); + + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("ValueFormat") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.cs new file mode 100644 index 000000000..2a424786c --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; + +#nullable disable + +namespace Squidex.Providers.SqlServer.Migrations +{ + /// + public partial class AddFullText : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Geos", + columns: table => new + { + Id = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: false), + AppId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + SchemaId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + ContentId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Stage = table.Column(type: "tinyint", nullable: false), + ServeAll = table.Column(type: "bit", nullable: false), + ServePublished = table.Column(type: "bit", nullable: false), + GeoField = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + GeoObject = table.Column(type: "geography", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Geos", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Texts", + columns: table => new + { + Id = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: false), + AppId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + SchemaId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + ContentId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Stage = table.Column(type: "tinyint", nullable: false), + ServeAll = table.Column(type: "bit", nullable: false), + ServePublished = table.Column(type: "bit", nullable: false), + Texts = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Texts", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Geos"); + + migrationBuilder.DropTable( + name: "Texts"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs index cfacf8314..b405bcc9d 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; using Squidex.Providers.SqlServer; #nullable disable @@ -17,7 +18,7 @@ namespace Squidex.Providers.SqlServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "8.0.13") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -766,6 +767,89 @@ namespace Squidex.Providers.SqlServer.Migrations b.ToTable("ContentReferencesPublished", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geography"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => { b.Property("UniqueContentId") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContextDesignTimeFactory.cs index a3dbbb37b..e25b259e2 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContextDesignTimeFactory.cs @@ -19,7 +19,10 @@ public sealed class SqlServerDbContextDesignTimeFactory : IDesignTimeDbContextFa const string ConnectionString = "Server=localhost;Port=14330;Database=test;User=sa;Password=sqlserver"; var builder = new DbContextOptionsBuilder() - .UseSqlServer(ConnectionString); + .UseSqlServer(ConnectionString, options => + { + options.UseNetTopologySuite(); + }); return new SqlServerDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs index e8ad9cff5..bc14727bb 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.Infrastructure.Queries; +using SqlException = Microsoft.Data.SqlClient.SqlException; namespace Squidex.Providers.SqlServer; @@ -17,6 +18,26 @@ public sealed class SqlServerDialect : SqlDialect { } + public override bool IsDuplicateIndexException(Exception exception, string name) + { + return exception is SqlException ex && ex.Number is 1913 or 7642 or 7652; + } + + public override string GeoIndex(string name, string table, string field) + { + return $"CREATE SPATIAL INDEX {name} ON {FormatTable(table)} ({FormatField(field, false)}) USING GEOGRAPHY_GRID;"; + } + + public override string TextIndex(string name, string table, string field) + { + return $"CREATE FULLTEXT INDEX ON {FormatTable(table)} ({FormatField(field, false)}) KEY INDEX PK_{table} ON {name} WITH CHANGE_TRACKING AUTO;"; + } + + public override string TextIndexPrepare(string name) + { + return $"CREATE FULLTEXT CATALOG {name};"; + } + public override string FormatLimitOffset(long limit, long offset, bool hasOrder) { var hasLimit = limit > 0 && limit < long.MaxValue; @@ -52,6 +73,16 @@ public sealed class SqlServerDialect : SqlDialect return base.OrderBy(path, order, isJson); } + public override string WhereMatch(PropertyPath path, string query, SqlParams queryParameters) + { + if (query.Contains(' ', StringComparison.OrdinalIgnoreCase)) + { + query = $"\"{query}\""; + } + + return $"CONTAINS({FormatField(path, false)}, {queryParameters.AddPositional(query)})"; + } + public override string Where(PropertyPath path, CompareOperator op, ClrValue value, SqlParams queryParameters, bool isJson) { if (isJson) diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index 7d135858e..17f857cac 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Squidex.AI; using Squidex.Assets.TusAdapter; using Squidex.Domain.Apps.Core.Apps; @@ -71,9 +70,10 @@ public static class ServiceExtensions ServerVersion.AutoDetect(connectionString); builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer)); - builder.UseMySql(connectionString, version, mysql => + builder.UseMySql(connectionString, version, options => { - mysql.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); + options.UseNetTopologySuite(); + options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); }); }); @@ -86,7 +86,10 @@ public static class ServiceExtensions services.AddDbContextFactory(builder => { builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer)); - builder.UseNpgsql(connectionString); + builder.UseNpgsql(connectionString, options => + { + options.UseNetTopologySuite(); + }); }); services.AddSingleton(typeof(ISnapshotStore<>), typeof(PostgresSnapshotStore<>)); @@ -98,13 +101,16 @@ public static class ServiceExtensions services.AddDbContextFactory(builder => { builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer)); - builder.UseSqlServer(connectionString); + builder.UseSqlServer(connectionString, options => + { + options.UseNetTopologySuite(); + }); }); services.AddSingleton(typeof(ISnapshotStore<>), typeof(SqlServerSnapshotStore<>)); services.AddSingleton(SqlServerDialect.Instance); services.AddSquidexEntityFramework(config); - } + }, }); } @@ -176,14 +182,15 @@ public static class ServiceExtensions services.AddSingletonAs>() .As().As(); + services.AddSingletonAs>() + .As().As(); + services.AddSingletonAs>() .As(); services.AddSingletonAs() .As(); - services.TryAddSingleton(); - services.AddEntityFrameworkAssetKeyValueStore(); } diff --git a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj index 90027c373..0fcf4bca9 100644 --- a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj +++ b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj @@ -16,7 +16,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,19 +25,23 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + - + - - - - - - + + + + + + + diff --git a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.sln b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.sln index f8988fd48..ca36c5d24 100644 --- a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.sln +++ b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.sln @@ -19,6 +19,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {6AB9E82C-07C3-4D4E-9CB2-AB42853A1CCB} + SolutionGuid = {15141572-3832-432F-8D94-EDB1160B525E} EndGlobalSection EndGlobal diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs index cc4103e9e..43bbec04d 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs @@ -82,7 +82,7 @@ public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : M } private Task SearchBySchemaAsync(SearchOperation search, IEnumerable schemaIds, double factor, - CancellationToken ct = default) + CancellationToken ct) { var filter = Filter.And( @@ -95,7 +95,7 @@ public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : M } private Task SearchByAppAsync(SearchOperation search, double factor, - CancellationToken ct = default) + CancellationToken ct) { var filter = Filter.And( @@ -107,7 +107,7 @@ public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : M } private async Task SearchAsync(SearchOperation search, FilterDefinition>> filter, double factor, - CancellationToken ct = default) + CancellationToken ct) { var byText = await GetCollection(search.SearchScope).Find(filter).Limit(search.Take) diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs index b5669aeab..76ecfe2ea 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs @@ -102,13 +102,16 @@ public abstract class MongoTextIndexBase(IMongoDatabase database, string shar Guard.NotNull(app); Guard.NotNull(query); + // The distance must be converted to radian (in contrast to MongoDB, which uses degrees). + var radian = query.Radius / 6378100; + // Use the filter in the correct order to leverage the index in the best way. var findFilter = Filter.And( Filter.Eq(x => x.AppId, app.Id), Filter.Eq(x => x.SchemaId, query.SchemaId), Filter.Eq(x => x.GeoField, query.Field), - Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100), + Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, radian), FilterByScope(scope)); var byGeo = diff --git a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj index 49f48016d..857874561 100644 --- a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj +++ b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj @@ -16,7 +16,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,11 +24,11 @@ - - - - - + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 07efdf50a..1000eacb2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -28,8 +28,8 @@ - - + + diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/Tokenizer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Tokenizer.cs similarity index 71% rename from backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/Tokenizer.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Tokenizer.cs index 0ca657e93..6d450dc99 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/Tokenizer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Tokenizer.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Reflection; +using System.Text; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Core; using Lucene.Net.Analysis.Standard; @@ -76,29 +77,65 @@ public static class Tokenizer return Word(textReader, textLanguage); } + public static string Terms(Dictionary source) + { + Guard.NotNull(source); + + var sb = DefaultPools.StringBuilder.Get(); + try + { + foreach (var (language, text) in source) + { + var stopWords = GetStopWords(language); + + Tokenize(new StringReader(text), sb, stopWords); + } + + return sb.ToString(); + } + finally + { + DefaultPools.StringBuilder.Return(sb); + } + } + public static string Terms(string query, string language) { - var stopWords = - string.Equals(language, InvariantPartitioning.Key, StringComparison.OrdinalIgnoreCase) ? - null : - StopWords.GetValueOrDefault(language) ?? - StopWords["en"]; + var stopWords = GetStopWords(language); return Tokenize(new StringReader(query), stopWords); } private static string Word(TextReader reader, string language) { - var stopWords = - string.Equals(language, InvariantPartitioning.Key, StringComparison.OrdinalIgnoreCase) ? + var stopWords = GetStopWords(language); + + return Tokenize(reader, stopWords); + } + + private static CharArraySet? GetStopWords(string language) + { + return string.Equals(language, InvariantPartitioning.Key, StringComparison.OrdinalIgnoreCase) ? null : StopWords.GetValueOrDefault(language) ?? StopWords["en"]; - - return Tokenize(reader, stopWords); } private static string Tokenize(TextReader reader, CharArraySet? stopWords) + { + var sb = DefaultPools.StringBuilder.Get(); + try + { + Tokenize(reader, sb, stopWords); + return sb.ToString(); + } + finally + { + DefaultPools.StringBuilder.Return(sb); + } + } + + private static void Tokenize(TextReader reader, StringBuilder sb, CharArraySet? stopWords) { var tokenizer = new StandardTokenizer(Version, reader); var tokenStream = (TokenStream)new StandardFilter(Version, tokenizer); @@ -111,34 +148,24 @@ public static class Tokenizer tokenStream = new StopFilter(Version, tokenStream, stopWords); } - var sb = DefaultPools.StringBuilder.Get(); - try - { - // Actually not idea what this is doing, but it seems to work. - var attribute = tokenStream.AddAttribute(); + // Actually not idea what this is doing, but it seems to work. + var attribute = tokenStream.AddAttribute(); - tokenStream.Reset(); + tokenStream.Reset(); - using (tokenStream) + using (tokenStream) + { + while (tokenStream.IncrementToken()) { - while (tokenStream.IncrementToken()) - { - var text = attribute.ToString(); + var text = attribute.ToString(); - if (sb.Length > 0) - { - sb.Append(' '); - } - - sb.Append(text); + if (sb.Length > 0) + { + sb.Append(' '); } - } - return sb.ToString(); - } - finally - { - DefaultPools.StringBuilder.Return(sb); + sb.Append(text); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 9353e4cd1..76e539ab1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -27,6 +27,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -41,7 +42,6 @@ - diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 4de917b0f..45bdf0b05 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -24,13 +24,13 @@ - - - - - - - + + + + + + + diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 84a4463b9..a61d8d65d 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -62,18 +62,18 @@ - - - - - - - + + + + + + + - - - - + + + + diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj b/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj new file mode 100644 index 000000000..0a3f22da1 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj @@ -0,0 +1,42 @@ + + + netstandard2.0 + latest + enable + true + enable + latest + Squidex + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/Template.handlebar b/backend/tests/Squidex.Data.Tests.CodeGenerator/Template.handlebar new file mode 100644 index 000000000..107b80bf1 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/Template.handlebar @@ -0,0 +1,23 @@ +#pragma warning disable +// Auto-generated code +using Squidex.EntityFramework.TestHelpers; + +namespace {{classNamespace}}; + +[Trait("Category", "TestContainer")] +[Collection("Postgres")] +public class Postgres{{className}}(PostgresFixture fixture) : {{baseName}}(fixture) +{ +} + +[Trait("Category", "TestContainer")] +[Collection("MySql")] +public class MySql{{className}}(MySqlFixture fixture) : {{baseName}}(fixture) +{ +} + +[Trait("Category", "TestContainer")] +[Collection("SqlServer")] +public class SqlServer{{className}}(SqlServerFixture fixture) : {{baseName}}(fixture) +{ +} \ No newline at end of file diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/TemplateModel.cs b/backend/tests/Squidex.Data.Tests.CodeGenerator/TemplateModel.cs new file mode 100644 index 000000000..f32edc3e8 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/TemplateModel.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex; + +internal sealed class TemplateModel +{ + public string ClassNamespace { get; set; } + + public string ClassName { get; set; } + + public string BaseName { get; set; } +} diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/TestGenerator.cs b/backend/tests/Squidex.Data.Tests.CodeGenerator/TestGenerator.cs new file mode 100644 index 000000000..3b334f810 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/TestGenerator.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics; +using System.Text; +using HandlebarsDotNet; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Squidex.Data.Tests.CodeGenerator; + +[Generator] +public class TestGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var templateStream = typeof(TestGenerator).Assembly.GetManifestResourceStream("Squidex.Template.handlebar")!; + var templateText = new StreamReader(templateStream).ReadToEnd(); + + var template = Handlebars.Compile(templateText); + + static TemplateModel? Transform(GeneratorSyntaxContext ctx) + { + var classSyntax = (ClassDeclarationSyntax)ctx.Node; + + var className = classSyntax.Identifier.Text; + if (!className.StartsWith("EF", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (!classSyntax.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword))) + { + return null; + } + + if (classSyntax.TypeParameterList == null || + classSyntax.TypeParameterList.Parameters.Count != 1 || + classSyntax.TypeParameterList.Parameters[0].Identifier.Text != "TContext") + { + return null; + } + + var namespaceDeclaration = + classSyntax.Ancestors() + .OfType().First(); + + return new TemplateModel + { + BaseName = classSyntax.Identifier.Text, + ClassName = classSyntax.Identifier.Text.Substring(2), + ClassNamespace = namespaceDeclaration.Name.ToString(), + }; + } + + var fieldDeclarations = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => + { + return node is ClassDeclarationSyntax; + }, + static (ctx, _) => Transform(ctx)) + .Where(x => x != null); + + context.RegisterSourceOutput(fieldDeclarations, (context, model) => + { + var source = template(model); + + context.AddSource($"{model!.BaseName}_Tests.cs", SourceText.From(source, Encoding.UTF8)); + }); + } +} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Apps/EFAppRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Apps/EFAppRepositoryTests.cs index b74f42346..c702d000b 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Apps/EFAppRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Apps/EFAppRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Apps; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFAppRepositoryTests(PostgresFixture fixture) : AppRepositoryTests +public abstract class EFAppRepositoryTests(ISqlFixture fixture) : AppRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFAppRepository(fixture.DbContextFactory); + var sut = new EFAppRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositorySnapshotTests.cs index 028d63b54..6856c3e7c 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositorySnapshotTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.EntityFramework.TestHelpers; @@ -13,13 +14,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Assets; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFAssetFolderRepositorySnapshotTests(PostgresFixture fixture) : AssetFolderSnapshotStoreTests +public abstract class EFAssetFolderRepositorySnapshotTests(ISqlFixture fixture) : AssetFolderSnapshotStoreTests where TContext : DbContext { protected override Task> CreateSutAsync() { - var sut = new EFAssetFolderRepository(fixture.DbContextFactory); + var sut = new EFAssetFolderRepository(fixture.DbContextFactory); return Task.FromResult>(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositoryTests.cs index 4e6868a82..a36857bd0 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetFolderRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Assets; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFAssetFolderRepositoryTests(PostgresFixture fixture) : AssetFolderRepositoryTests +public abstract class EFAssetFolderRepositoryTests(ISqlFixture fixture) : AssetFolderRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFAssetFolderRepository(fixture.DbContextFactory); + var sut = new EFAssetFolderRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs index f9a170cc8..d978af52a 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; using Squidex.EntityFramework.TestHelpers; @@ -13,13 +14,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Assets; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFAssetRepositorySnapshotTests(PostgresFixture fixture) : AssetSnapshotStoreTests +public abstract class EFAssetRepositorySnapshotTests(ISqlFixture fixture) : AssetSnapshotStoreTests where TContext : DbContext { protected override Task> CreateSutAsync() { - var sut = new EFAssetRepository(fixture.DbContextFactory, fixture.Dialect); + var sut = new EFAssetRepository(fixture.DbContextFactory, fixture.Dialect); return Task.FromResult>(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositoryTests.cs index 37b1072e6..6940cc46e 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Assets; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFAssetRepositoryTests(PostgresFixture fixture) : AssetRepositoryTests +public abstract class EFAssetRepositoryTests(ISqlFixture fixture) : AssetRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFAssetRepository(fixture.DbContextFactory, fixture.Dialect); + var sut = new EFAssetRepository(fixture.DbContextFactory, fixture.Dialect); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositorySnapshotTests.cs index 867b0f6ba..4690bc265 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositorySnapshotTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents; using Squidex.EntityFramework.TestHelpers; @@ -13,13 +14,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Contents; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFContentRepositorySnapshotTests(PostgresFixture fixture) : ContentSnapshotStoreTests +public abstract class EFContentRepositorySnapshotTests(ISqlFixture fixture) : ContentSnapshotStoreTests where TContext : DbContext { protected override Task> CreateSutAsync() { - var sut = new EFContentRepository(fixture.DbContextFactory, Context.AppProvider, fixture.Dialect); + var sut = new EFContentRepository(fixture.DbContextFactory, Context.AppProvider, fixture.Dialect); return Task.FromResult>(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositoryTests.cs index 621ad4ab5..1c9a260ea 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/EFContentRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Contents; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFContentRepositoryTests(PostgresFixture fixture) : ContentRepositoryTests +public abstract class EFContentRepositoryTests(ISqlFixture fixture) : ContentRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFContentRepository(fixture.DbContextFactory, AppProvider, fixture.Dialect); + var sut = new EFContentRepository(fixture.DbContextFactory, AppProvider, fixture.Dialect); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexTests.cs new file mode 100644 index 000000000..cb1c963f6 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexTests.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.EntityFramework.TestHelpers; + +namespace Squidex.EntityFramework.Domain.Contents.Text; + +public abstract class EFTextIndexTests(ISqlFixture fixture) : TextIndexerTests where TContext : DbContext +{ + public override bool SupportsQuerySyntax => false; + + public override async Task CreateSutAsync() + { + var sut = new EFTextIndex(fixture.DbContextFactory, fixture.Dialect); + + await sut.InitializeAsync(default); + return sut; + } +} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexerStateTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexerStateTests.cs index d890f1ed3..0e8851084 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexerStateTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Contents/Text/EFTextIndexerStateTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text.State; @@ -13,13 +14,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Contents.Text; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public sealed class EFTextIndexerStateTests(PostgresFixture fixture) : TextIndexerStateTests +public abstract class EFTextIndexerStateTests(ISqlFixture fixture) : TextIndexerStateTests where TContext : DbContext { protected override Task CreateSutAsync(IContentRepository contentRepository) { - var sut = new EFTextIndexerState(fixture.DbContextFactory, fixture.Dialect, contentRepository); + var sut = new EFTextIndexerState(fixture.DbContextFactory, fixture.Dialect, contentRepository); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/History/EFHistoryEventRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/History/EFHistoryEventRepositoryTests.cs index 950315075..600b343ac 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/History/EFHistoryEventRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/History/EFHistoryEventRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.History; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFHistoryEventRepositoryTests(PostgresFixture fixture) : HistoryEventRepositoryTests +public abstract class EFHistoryEventRepositoryTests(ISqlFixture fixture) : HistoryEventRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFHistoryEventRepository(fixture.DbContextFactory); + var sut = new EFHistoryEventRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleEventRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleEventRepositoryTests.cs index fc6de89a8..c0aadbad3 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleEventRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleEventRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Rules; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFRuleEventRepositoryTests(PostgresFixture fixture) : RuleEventRepositoryTests +public abstract class EFRuleEventRepositoryTests(ISqlFixture fixture) : RuleEventRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFRuleEventRepository(fixture.DbContextFactory); + var sut = new EFRuleEventRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleRepositoryTests.cs index 333180582..bb719ccb7 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Rules/EFRuleRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Rules; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFRuleRepositoryTests(PostgresFixture fixture) : RuleRepositoryTests +public abstract class EFRuleRepositoryTests(ISqlFixture fixture) : RuleRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFRuleRepository(fixture.DbContextFactory); + var sut = new EFRuleRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Schemas/EFSchemaRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Schemas/EFSchemaRepositoryTests.cs index 675878fef..306b681c8 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Schemas/EFSchemaRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Schemas/EFSchemaRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Schemas; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFSchemaRepositoryTests(PostgresFixture fixture) : SchemaRepositoryTests +public abstract class EFSchemaRepositoryTests(ISqlFixture fixture) : SchemaRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFSchemaRepository(fixture.DbContextFactory); + var sut = new EFSchemaRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Teams/EFTeamRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Teams/EFTeamRepositoryTests.cs index 016c35927..9b8826c3c 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Teams/EFTeamRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Teams/EFTeamRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.EntityFramework.TestHelpers; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Domain.Teams; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFTeamRepositoryTests(PostgresFixture fixture) : TeamRepositoryTests +public abstract class EFTeamRepositoryTests(ISqlFixture fixture) : TeamRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFTeamRepository(fixture.DbContextFactory); + var sut = new EFTeamRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Caching/EFDistributedCacheTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Caching/EFDistributedCacheTests.cs index b3537fc51..e96878303 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Caching/EFDistributedCacheTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Caching/EFDistributedCacheTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Distributed; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.Caching; @@ -12,13 +13,11 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Infrastructure.Caching; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFDistributedCacheTests(PostgresFixture fixture) : DistributedCacheTests +public abstract class EFDistributedCacheTests(ISqlFixture fixture) : DistributedCacheTests where TContext : DbContext { protected override Task CreateSutAsync(TimeProvider timeProvider) { - var sut = new EFDistributedCache(fixture.DbContextFactory, timeProvider); + var sut = new EFDistributedCache(fixture.DbContextFactory, timeProvider); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Log/EFRequestLogRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Log/EFRequestLogRepositoryTests.cs index 643efcfb7..f79e8ef48 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Log/EFRequestLogRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Log/EFRequestLogRepositoryTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.Log; @@ -12,14 +13,12 @@ using Squidex.Shared; namespace Squidex.EntityFramework.Infrastructure.Log; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFRequestLogRepositoryTests(PostgresFixture fixture) : RequestLogRepositoryTests +public abstract class EFRequestLogRepositoryTests(ISqlFixture fixture) : RequestLogRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { var sut = - new EFRequestLogRepository( + new EFRequestLogRepository( fixture.DbContextFactory, Options.Create(new RequestLogStoreOptions())); return Task.FromResult(sut); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/EFMigrationStatusTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/EFMigrationStatusTests.cs index 89af507d5..f6da38798 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/EFMigrationStatusTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/EFMigrationStatusTests.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.Migrations; using Squidex.Shared; namespace Squidex.EntityFramework.Infrastructure.Migrations; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFMigrationStatusTests(PostgresFixture fixture) : MigrationStatusTests +public abstract class EFMigrationStatusTests(ISqlFixture fixture) : MigrationStatusTests where TContext : DbContext { protected override async Task CreateSutAsync() { - var sut = new EFMigrationStatus(fixture.DbContextFactory); + var sut = new EFMigrationStatus(fixture.DbContextFactory); await sut.InitializeAsync(default); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlQueryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs similarity index 84% rename from backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlQueryTests.cs rename to backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs index 0398609fb..65b8ca200 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlQueryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs @@ -6,16 +6,19 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; +using NetTopologySuite.Geometries; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.Queries; namespace Squidex.EntityFramework.Infrastructure.Queries; -public abstract class SqlQueryTests where TContext : DbContext +public abstract class EFQueryTests(ISqlFixture fixture) where TContext : DbContext { - protected abstract Task CreateDbContextAsync(); - - protected abstract SqlDialect CreateDialect(); + protected Task CreateDbContextAsync() + { + return fixture.DbContextFactory.CreateDbContextAsync(); + } private class TestSqlBuilder(SqlDialect dialect, string table) : SqlQueryBuilder(dialect, table) { @@ -29,6 +32,9 @@ public abstract class SqlQueryTests where TContext : DbContext { var dbContext = await CreateDbContextAsync(); + await dbContext.Database.CreateGeoIndexAsync(fixture.Dialect, "IDX_GEO", "TestEntity", "Point"); + await dbContext.Database.CreateTextIndexAsync(fixture.Dialect, "IDX_Text", "TestEntity", "FullText"); + var set = dbContext.Set(); if (await set.AnyAsync()) { @@ -66,6 +72,7 @@ public abstract class SqlQueryTests where TContext : DbContext BooleanOrNull = i > 10 ? true : null, Number = i, NumberOrNull = i > 10 ? null : i, + FullText = "hello world", Text = $"Prefix{i}Suffix", Json = new TestJson { @@ -77,6 +84,7 @@ public abstract class SqlQueryTests where TContext : DbContext Array = [0, i], Text = $"Prefix{i}Suffix", }, + Point = new Point(i * 2, i * 2) { SRID = 4326 }, }); } @@ -465,7 +473,7 @@ public abstract class SqlQueryTests where TContext : DbContext public async Task Should_query_count() { var builder = - new TestSqlBuilder(CreateDialect(), "TestEntity") + new TestSqlBuilder(fixture.Dialect, "TestEntity") .Count(); var (sql, parameters) = builder.Compile(); @@ -476,6 +484,47 @@ public abstract class SqlQueryTests where TContext : DbContext Assert.Equal(20, dbResult); } + [Fact] + public async Task Should_query_by_distance() + { + var point = new Point(4, 4) { SRID = 4326 }; + + var dbContext = await CreateAndPrepareDbContextAsync(); + var dbResult = await dbContext.Set().Where(x => x.Point.Distance(point) < 1).ToListAsync(); + + Assert.Single(dbResult); + } + + [Fact] + public async Task Should_query_full_text() + { + var builder = + new TestSqlBuilder(fixture.Dialect, "TestEntity") + .WhereMatch("FullText", "hello"); + + var (sql, parameters) = builder.Compile(); + + var dbContext = await CreateAndPrepareDbContextAsync(); + var dbResult = await PollAsync(dbContext, sql, parameters, 20); + + Assert.Equal(20, dbResult.Count); + } + + [Fact] + public async Task Should_query_full_text_with_space() + { + var builder = + new TestSqlBuilder(fixture.Dialect, "TestEntity") + .WhereMatch("FullText", "hello world"); + + var (sql, parameters) = builder.Compile(); + + var dbContext = await CreateAndPrepareDbContextAsync(); + var dbResult = await PollAsync(dbContext, sql, parameters, 0); + + Assert.Empty(dbResult); + } + private static long[] Range(int from, int to) { var result = new List(); @@ -500,7 +549,7 @@ public abstract class SqlQueryTests where TContext : DbContext private async Task> QueryAsync(ClrQuery query) { var builder = - new TestSqlBuilder(CreateDialect(), "TestEntity") + new TestSqlBuilder(fixture.Dialect, "TestEntity") .Limit(query) .Offset(query) .Order(query) @@ -513,4 +562,21 @@ public abstract class SqlQueryTests where TContext : DbContext return dbResult.Select(x => x.Number).ToList(); } + + private static async Task> PollAsync(TContext dbContext, string sql, object[] parameters, int expectedCount) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + while (!cts.IsCancellationRequested) + { + var dbResult = await dbContext.Set().FromSqlRaw(sql, parameters).ToListAsync(default); + if (dbResult.Count == expectedCount) + { + return dbResult; + } + + await Task.Delay(50, default); + } + + return []; + } } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/MySqlQueryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/MySqlQueryTests.cs deleted file mode 100644 index 507d0596c..000000000 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/MySqlQueryTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.EntityFramework.TestHelpers; -using Squidex.Infrastructure.Queries; -using Squidex.Providers.MySql; - -namespace Squidex.EntityFramework.Infrastructure.Queries; - -[Trait("Category", "TestContainer")] -[Collection("MySql")] -public class MySqlQueryTests(MySqlFixture fixture) : SqlQueryTests -{ - protected override async Task CreateDbContextAsync() - { - var context = await fixture.DbContextFactory.CreateDbContextAsync(); - - return context; - } - - protected override SqlDialect CreateDialect() - { - return MySqlDialect.Instance; - } -} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/PostgresQueryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/PostgresQueryTests.cs deleted file mode 100644 index c51f2a85e..000000000 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/PostgresQueryTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.EntityFramework.TestHelpers; -using Squidex.Infrastructure.Queries; -using Squidex.Providers.Postgres; - -namespace Squidex.EntityFramework.Infrastructure.Queries; - -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class PostgresQueryTests(PostgresFixture fixture) : SqlQueryTests -{ - protected override async Task CreateDbContextAsync() - { - var context = await fixture.DbContextFactory.CreateDbContextAsync(); - - return context; - } - - protected override SqlDialect CreateDialect() - { - return PostgresDialect.Instance; - } -} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlServerQueryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlServerQueryTests.cs deleted file mode 100644 index ce5b5fcf7..000000000 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/SqlServerQueryTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.EntityFramework.TestHelpers; -using Squidex.Infrastructure.Queries; -using Squidex.Providers.SqlServer; - -namespace Squidex.EntityFramework.Infrastructure.Queries; - -[Trait("Category", "TestContainer")] -[Collection("SqlServer")] -public class SqlServerQueryTests(SqlServerFixture fixture) : SqlQueryTests -{ - protected override async Task CreateDbContextAsync() - { - var context = await fixture.DbContextFactory.CreateDbContextAsync(); - - return context; - } - - protected override SqlDialect CreateDialect() - { - return SqlServerDialect.Instance; - } -} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/States/EFSnapshotStoreTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/States/EFSnapshotStoreTests.cs index 96e6d824c..e10883659 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/States/EFSnapshotStoreTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/States/EFSnapshotStoreTests.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.States; using Squidex.Shared; namespace Squidex.EntityFramework.Infrastructure.States; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFSnapshotStoreTests(PostgresFixture fixture) : SnapshotStoreTests +public abstract class EFSnapshotStoreTests(ISqlFixture fixture) : SnapshotStoreTests where TContext : DbContext { protected override Task> CreateSutAsync() { - var sut = new EFSnapshotStore>(fixture.DbContextFactory); + var sut = new EFSnapshotStore>(fixture.DbContextFactory); return Task.FromResult>(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/UsageTracking/EFUsageRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/UsageTracking/EFUsageRepositoryTests.cs index ded266110..507919676 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/UsageTracking/EFUsageRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/UsageTracking/EFUsageRepositoryTests.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.EntityFrameworkCore; using Squidex.EntityFramework.TestHelpers; using Squidex.Infrastructure.UsageTracking; using Squidex.Shared; namespace Squidex.EntityFramework.Infrastructure.UsageTracking; -[Trait("Category", "TestContainer")] -[Collection("Postgres")] -public class EFUsageRepositoryTests(PostgresFixture fixture) : UsageRepositoryTests +public abstract class EFUsageRepositoryTests(ISqlFixture fixture) : UsageRepositoryTests where TContext : DbContext { protected override Task CreateSutAsync() { - var sut = new EFUsageRepository(fixture.DbContextFactory); + var sut = new EFUsageRepository(fixture.DbContextFactory); return Task.FromResult(sut); } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs index 78863e7a2..6fefd5107 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure.Migrations; using Squidex.Providers.MySql; -using Squidex.Providers.Postgres; using Testcontainers.MySql; namespace Squidex.EntityFramework.Migrations; @@ -39,9 +38,10 @@ public class MySqlMigrationTests : IAsyncLifetime { var connectionString = mysql.GetConnectionString(); - b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mysql => + b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options => { - mysql.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); + options.UseNetTopologySuite(); + options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); }); }) .AddSingleton(TestUtils.DefaultSerializer) diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs index 5af740111..44ec072e5 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs @@ -17,7 +17,10 @@ namespace Squidex.EntityFramework.Migrations; [Trait("Category", "TestContainer")] public class PostgresMigrationTests : IAsyncLifetime { - private readonly PostgreSqlContainer postgreSql = new PostgreSqlBuilder().Build(); + private readonly PostgreSqlContainer postgreSql = + new PostgreSqlBuilder() + .WithImage("postgis/postgis") + .Build(); public async Task InitializeAsync() { @@ -36,7 +39,10 @@ public class PostgresMigrationTests : IAsyncLifetime new ServiceCollection() .AddDbContextFactory(b => { - b.UseNpgsql(postgreSql.GetConnectionString()); + b.UseNpgsql(postgreSql.GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); }) .AddSingleton(TestUtils.DefaultSerializer) .AddSingleton>() diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs index 06505d551..37586c0e0 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs @@ -37,7 +37,10 @@ public class SqlServerMigrationTests : IAsyncLifetime new ServiceCollection() .AddDbContextFactory(b => { - b.UseSqlServer(sqlServer.GetConnectionString()); + b.UseSqlServer(sqlServer.GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); }) .AddSingleton(TestUtils.DefaultSerializer) .AddSingleton>() diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/ISqlFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/ISqlFixture.cs new file mode 100644 index 000000000..5a9eb604c --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/ISqlFixture.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using Squidex.Infrastructure.Queries; + +namespace Squidex.EntityFramework.TestHelpers; + +public interface ISqlFixture where TContext : DbContext +{ + SqlDialect Dialect { get; } + + IDbContextFactory DbContextFactory { get; } +} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs index 62a035b3e..06e593957 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs @@ -6,11 +6,12 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Queries; +using Squidex.Providers.MySql; using Testcontainers.MySql; #pragma warning disable MA0048 // File name must match type name @@ -22,18 +23,21 @@ public sealed class MySqlFixtureCollection : ICollectionFixture { } -public sealed class MySqlFixture : IAsyncLifetime +public sealed class MySqlFixture : IAsyncLifetime, ISqlFixture { private readonly MySqlContainer mysql = new MySqlBuilder() .WithReuse(true) .WithLabel("reuse-id", "squidex-mysql") + .WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1") .Build(); private IServiceProvider services; public IDbContextFactory DbContextFactory => services.GetRequiredService>(); + public SqlDialect Dialect => MySqlDialect.Instance; + public async Task InitializeAsync() { await mysql.StartAsync(); @@ -42,22 +46,18 @@ public sealed class MySqlFixture : IAsyncLifetime new ServiceCollection() .AddDbContextFactory(b => { - var connectionString = mysql.GetConnectionString(); + var connectionString = $"{mysql.GetConnectionString()};AllowLoadLocalInfile=true"; - b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mysql => + b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options => { - mysql.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); + options.UseNetTopologySuite(); + options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); }); }) + .AddSingletonAs>().Done() .AddSingleton(TestUtils.DefaultSerializer) .BuildServiceProvider(); - var factory = services.GetRequiredService>(); - var context = await factory.CreateDbContextAsync(); - var creator = (RelationalDatabaseCreator)context.Database.GetService(); - - await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) { await service.InitializeAsync(default); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs index 6c7a0ec87..731d19588 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs @@ -6,11 +6,10 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; +using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Queries; using Squidex.Providers.Postgres; using Testcontainers.PostgreSql; @@ -25,10 +24,11 @@ public sealed class PostgresFixtureCollection : ICollectionFixture { private readonly PostgreSqlContainer postgreSql = new PostgreSqlBuilder() + .WithImage("postgis/postgis") .WithReuse(true) .WithLabel("reuse-id", "squidex-postgres") .Build(); @@ -47,17 +47,15 @@ public sealed class PostgresFixture : IAsyncLifetime new ServiceCollection() .AddDbContextFactory(b => { - b.UseNpgsql(postgreSql.GetConnectionString()); + b.UseNpgsql(postgreSql.GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); }) + .AddSingletonAs>().Done() .AddSingleton(TestUtils.DefaultSerializer) .BuildServiceProvider(); - var factory = services.GetRequiredService>(); - var context = await factory.CreateDbContextAsync(); - var creator = (RelationalDatabaseCreator)context.Database.GetService(); - - await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) { await service.InitializeAsync(default); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs index 594bdc538..fec351d78 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs @@ -5,12 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Queries; +using Squidex.Providers.SqlServer; using Testcontainers.MsSql; #pragma warning disable MA0048 // File name must match type name @@ -22,37 +24,39 @@ public sealed class SqlServerFixtureCollection : ICollectionFixture { private readonly MsSqlContainer sqlServer = new MsSqlBuilder() + .WithImage("vibs2006/sql_server_fts") .WithReuse(true) .WithLabel("reuse-id", "squidex-mssql") .Build(); private IServiceProvider services; - public IDbContextFactory DbContextFactory => services.GetRequiredService>(); + public IDbContextFactory DbContextFactory => services.GetRequiredService>(); + + public SqlDialect Dialect => SqlServerDialect.Instance; public async Task InitializeAsync() { await sqlServer.StartAsync(); + await sqlServer.ExecScriptAsync($"create database squidex;"); services = new ServiceCollection() - .AddDbContextFactory(b => + .AddDbContextFactory(b => { - b.UseSqlServer(sqlServer.GetConnectionString()); + b.UseSqlServer(GetConnectionString(), options => + { + options.UseNetTopologySuite(); + }); }) + .AddSingletonAs>().Done() .AddSingleton(TestUtils.DefaultSerializer) .BuildServiceProvider(); - var factory = services.GetRequiredService>(); - var context = await factory.CreateDbContextAsync(); - var creator = (RelationalDatabaseCreator)context.Database.GetService(); - - await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) { await service.InitializeAsync(default); @@ -68,4 +72,14 @@ public sealed class SqlServerFixture : IAsyncLifetime await sqlServer.StopAsync(); } + + private string GetConnectionString() + { + var builder = new SqlConnectionStringBuilder(sqlServer.GetConnectionString()) + { + InitialCatalog = "squidex", + }; + + return builder.ConnectionString; + } } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs index d8164a8e1..8972a768b 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs @@ -5,6 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using EFCore.BulkExtensions.SqlAdapters; +using EFCore.BulkExtensions.SqlAdapters.MySql; +using EFCore.BulkExtensions.SqlAdapters.PostgreSql; +using EFCore.BulkExtensions.SqlAdapters.SqlServer; using Microsoft.EntityFrameworkCore; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; @@ -19,6 +23,8 @@ namespace Squidex.EntityFramework.TestHelpers; public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { + public static readonly IDbServer Server = new MySqlDbServer(); + protected override string? JsonColumnType() { return "json"; @@ -28,20 +34,41 @@ public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSe public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { + public static readonly IDbServer Server = new PostgreSqlDbServer(); + protected override string? JsonColumnType() { return "jsonb"; } } -public class TestDbContexSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer) +public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { + public static readonly IDbServer Server = new SqlServerDbServer(); } public class TestDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) : AppDbContext(options, jsonSerializer) { + static TestDbContext() + { + SqlAdaptersMapping.Provider = context => + { + switch (context) + { + case TestDbContextMySql: + return TestDbContextMySql.Server; + case TestDbContextPostgres: + return TestDbContextPostgres.Server; + case TestDbContextSqlServer: + return TestDbContextSqlServer.Server; + } + + throw new ArgumentException("Not supported.", nameof(context)); + }; + } + protected override void OnModelCreating(ModelBuilder builder) { builder.UseSnapshot>(jsonSerializer, JsonColumnType()); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs index d0236798c..dcfb0d9c4 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using NetTopologySuite.Geometries; #pragma warning disable MA0048 // File name must match type name @@ -22,10 +23,14 @@ public class TestEntity public string Text { get; set; } + public string FullText { get; set; } + public bool Boolean { get; set; } public bool? BooleanOrNull { get; set; } + public Point Point { get; set; } + public TestJson Json { get; set; } } diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasParsingTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasParsingTests.cs index 1f5e82b86..7a2be7395 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasParsingTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasParsingTests.cs @@ -395,12 +395,12 @@ public class AtlasParsingTests Assert.Equal(expected, actual); } - private static object CreateQuery(object query) + private static string CreateQuery(object query) { return JsonSerializer.Serialize(query, JsonSerializerOptions); } - private static object ParseQuery(string query) + private static string ParseQuery(string query) { var luceneQuery = QueryParser.Parse(query); diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs index 5b6769c85..a83bb6212 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs @@ -5,21 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - using Squidex.Domain.Apps.Entities.Contents.Text; namespace Squidex.MongoDb.Domain.Contents.Text; [Trait("Category", "Dependencies")] -public class AtlasTextIndexTests(AtlasTextIndexFixture fixture) : TextIndexerTestsBase, IClassFixture +public class AtlasTextIndexTests(AtlasTextIndexFixture fixture) : TextIndexerTests, IClassFixture { public override bool SupportsQuerySyntax => true; public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; - public AtlasTextIndexFixture _ { get; } = fixture; public override Task CreateSutAsync() @@ -30,26 +26,26 @@ public class AtlasTextIndexTests(AtlasTextIndexFixture fixture) : TextIndexerTes [Fact] public async Task Should_retrieve_english_stopword_only_for_german_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); + await SearchText(expected: Ids2, text: "und"); } [Fact] public async Task Should_retrieve_german_stopword_only_for_english_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); + await SearchText(expected: Ids1, text: "and"); } [Fact] public async Task Should_index_cjk_content_and_retrieve() { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + await CreateTextAsync(Ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); + await SearchText(expected: Ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs index 2a97ef22e..e41d53db6 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.MongoDb.TestHelpers; @@ -14,7 +12,7 @@ namespace Squidex.MongoDb.Domain.Contents.Text; [Trait("Category", "Dependencies")] [Collection("Mongo")] -public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTestsBase +public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTests { public override bool SupportsQuerySyntax => false; @@ -31,18 +29,18 @@ public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTestsBase [Fact] public async Task Should_retrieve_all_stopwords_for_english_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "de:and"); + await SearchText(expected: Ids1, text: "de:and"); } [Fact] public async Task Should_retrieve_all_stopwords_for_german_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "en:und"); + await SearchText(expected: Ids2, text: "en:und"); } } diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs index 92bb9416d..d352a4477 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text.State; diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs index 6f9b9e984..25d79e847 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - using Squidex.Events; namespace Squidex.MongoDb.Infrastructure.EventSourcing; diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs index fb6d9a46d..288e5c6d0 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs @@ -13,8 +13,6 @@ using Squidex.Infrastructure.EventSourcing.Consume; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.TestHelpers; -#pragma warning disable SA1300 // Element should begin with upper-case letter - namespace Squidex.MongoDb.Infrastructure.EventSourcing; [Trait("Category", "Dependencies")] diff --git a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj index 0e7b70f9d..d57059643 100644 --- a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj +++ b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj @@ -7,11 +7,13 @@ enable enable en + true + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index 6d51e6160..2207015f4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -35,7 +35,6 @@ public class FileTagAssetMetadataSourceTests : GivenContext await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Image, command.Type); - Assert.Equal(272, command.Metadata.GetInt32(KnownMetadataKeys.PixelWidth)); Assert.Equal(170, command.Metadata.GetInt32(KnownMetadataKeys.PixelHeight)); } @@ -59,8 +58,7 @@ public class FileTagAssetMetadataSourceTests : GivenContext await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Audio, command.Type); - - Assert.Equal(JsonValue.Create("00:00:28.2708750"), command.Metadata[KnownMetadataKeys.Duration]); + Assert.Equal(JsonValue.Create("00:00:27.7447500"), command.Metadata[KnownMetadataKeys.Duration]); Assert.Equal(JsonValue.Create(2L), command.Metadata[KnownMetadataKeys.AudioChannels]); Assert.Equal(JsonValue.Create(128L), command.Metadata[KnownMetadataKeys.AudioBitrate]); Assert.Equal(JsonValue.Create(44100L), command.Metadata[KnownMetadataKeys.AudioSampleRate]); @@ -74,7 +72,6 @@ public class FileTagAssetMetadataSourceTests : GivenContext await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Video, command.Type); - Assert.Equal(JsonValue.Create("00:00:05.3120000"), command.Metadata[KnownMetadataKeys.Duration]); Assert.Equal(JsonValue.Create(2L), command.Metadata[KnownMetadataKeys.AudioChannels]); Assert.Equal(JsonValue.Create(384L), command.Metadata[KnownMetadataKeys.AudioBitrate]); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs index 45407da2f..45004314b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs @@ -5,17 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - namespace Squidex.Domain.Apps.Entities.Contents.Text; [Trait("Category", "Dependencies")] -public class AzureTextIndexTests(AzureTextIndexFixture fixture) : TextIndexerTestsBase, IClassFixture +public class AzureTextIndexTests(AzureTextIndexFixture fixture) : TextIndexerTests, IClassFixture { public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; - public AzureTextIndexFixture _ { get; } = fixture; public override Task CreateSutAsync() @@ -26,26 +22,26 @@ public class AzureTextIndexTests(AzureTextIndexFixture fixture) : TextIndexerTes [Fact] public async Task Should_retrieve_english_stopword_only_for_german_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); + await SearchText(expected: Ids2, text: "und"); } [Fact] public async Task Should_retrieve_german_stopword_only_for_english_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); + await SearchText(expected: Ids1, text: "and"); } [Fact] public async Task Should_index_cjk_content_and_retrieve() { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + await CreateTextAsync(Ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); + await SearchText(expected: Ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs index 82ef3781e..d743b51ae 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs @@ -5,17 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - namespace Squidex.Domain.Apps.Entities.Contents.Text; [Trait("Category", "Dependencies")] -public class ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) : TextIndexerTestsBase, IClassFixture +public class ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) : TextIndexerTests, IClassFixture { public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; - public ElasticSearchTextIndexFixture _ { get; } = fixture; public override Task CreateSutAsync() @@ -26,26 +22,26 @@ public class ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) [Fact] public async Task Should_retrieve_english_stopword_only_for_german_query() { - await CreateTextAsync(ids1[0], "de", "and y"); - await CreateTextAsync(ids2[0], "en", "and y"); + await CreateTextAsync(Ids1[0], "de", "and y"); + await CreateTextAsync(Ids2[0], "en", "and y"); - await SearchText(expected: ids2, text: "und"); + await SearchText(expected: Ids2, text: "und"); } [Fact] public async Task Should_retrieve_german_stopword_only_for_english_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); + await SearchText(expected: Ids1, text: "and"); } [Fact] public async Task Should_index_cjk_content_and_retrieve() { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + await CreateTextAsync(Ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); + await SearchText(expected: Ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs index 70f800e1d..4c1c68681 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs @@ -5,17 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable SA1300 // Element should begin with upper-case letter - namespace Squidex.Domain.Apps.Entities.Contents.Text; [Trait("Category", "Dependencies")] -public class OpenSearchTextIndexTests(OpenSearchTextIndexFixture fixture) : TextIndexerTestsBase, IClassFixture +public class OpenSearchTextIndexTests(OpenSearchTextIndexFixture fixture) : TextIndexerTests, IClassFixture { public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; - public OpenSearchTextIndexFixture _ { get; } = fixture; public override Task CreateSutAsync() @@ -26,26 +22,26 @@ public class OpenSearchTextIndexTests(OpenSearchTextIndexFixture fixture) : Text [Fact] public async Task Should_retrieve_english_stopword_only_for_german_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); + await SearchText(expected: Ids2, text: "und"); } [Fact] public async Task Should_retrieve_german_stopword_only_for_english_query() { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); + await SearchText(expected: Ids1, text: "and"); } [Fact] public async Task Should_index_cjk_content_and_retrieve() { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + await CreateTextAsync(Ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); + await SearchText(expected: Ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs similarity index 51% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs index 12b195d3b..a44fb8d77 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs @@ -15,22 +15,22 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; -#pragma warning disable SA1401 // Fields should be private +#pragma warning disable CA1859 // Use concrete types when possible for improved performance namespace Squidex.Domain.Apps.Entities.Contents.Text; -public abstract class TextIndexerTestsBase : GivenContext +public abstract class TextIndexerTests : GivenContext { private TextIndexingProcess? process; - protected readonly List ids1 = [DomainId.NewGuid()]; - protected readonly List ids2 = [DomainId.NewGuid()]; + protected List Ids1 { get; } = [DomainId.NewGuid()]; + protected List Ids2 { get; } = [DomainId.NewGuid()]; public virtual bool SupportsQuerySyntax => true; public virtual bool SupportsGeo => false; - public virtual int WaitAfterUpdate => 0; + public virtual TimeSpan WaitTime => TimeSpan.FromSeconds(30); public abstract Task CreateSutAsync(); @@ -50,9 +50,9 @@ public abstract class TextIndexerTestsBase : GivenContext return; } - await CreateTextAsync(ids1[0], "iv", "Hello"); + await CreateTextAsync(Ids1[0], "iv", "Hello"); - await SearchText(expected: ids1, text: "helo~"); + await SearchText(expected: Ids1, text: "helo~"); } [Fact] @@ -63,9 +63,9 @@ public abstract class TextIndexerTestsBase : GivenContext return; } - await CreateTextAsync(ids1[0], "en", "City"); + await CreateTextAsync(Ids1[0], "en", "City"); - await SearchText(expected: ids1, text: "en:city"); + await SearchText(expected: Ids1, text: "en:city"); } [Fact] @@ -79,13 +79,13 @@ public abstract class TextIndexerTestsBase : GivenContext var field = Guid.NewGuid().ToString(); // Within search radius - await CreateGeoAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); + await CreateGeoAsync(Ids1[0], field, 51.343391192211506, 12.401476788622826); // Outside of search radius - await CreateGeoAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); + await CreateGeoAsync(Ids2[0], field, 51.30765141427311, 12.379631713912486); // Within search radius and correct field. - await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); + await SearchGeo(expected: Ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); // Within search radius but incorrect field. await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); @@ -102,13 +102,13 @@ public abstract class TextIndexerTestsBase : GivenContext var field = Guid.NewGuid().ToString(); // Within search radius - await CreateGeoJsonAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); + await CreateGeoJsonAsync(Ids1[0], field, 51.343391192211506, 12.401476788622826); // Outside of search radius - await CreateGeoJsonAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); + await CreateGeoJsonAsync(Ids2[0], field, 51.30765141427311, 12.379631713912486); // Within search radius and correct field. - await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); + await SearchGeo(expected: Ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); // Within search radius but incorrect field. await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); @@ -117,19 +117,19 @@ public abstract class TextIndexerTestsBase : GivenContext [Fact] public async Task Should_search_by_app() { - await CreateTextAsync(ids1[0], "iv", "Hello"); + await CreateTextAsync(Ids1[0], "iv", "Hello"); - await SearchByAppText(expected: ids1, text: "helo~"); + await SearchByAppText(expected: Ids1, text: "hello"); } [Fact] public async Task Should_index_invariant_content_and_retrieve() { - await CreateTextAsync(ids1[0], "iv", "Hello"); - await CreateTextAsync(ids2[0], "iv", "World"); + await CreateTextAsync(Ids1[0], "iv", "Hello"); + await CreateTextAsync(Ids2[0], "iv", "World"); - await SearchText(expected: ids1, text: "Hello"); - await SearchText(expected: ids2, text: "World"); + await SearchText(expected: Ids1, text: "Hello"); + await SearchText(expected: Ids2, text: "World"); await SearchText(expected: null, text: "Hello", SearchScope.Published); await SearchText(expected: null, text: "World", SearchScope.Published); @@ -138,201 +138,201 @@ public abstract class TextIndexerTestsBase : GivenContext [Fact] public async Task Should_update_draft_only() { - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "Version1", target: SearchScope.All); + await SearchText(expected: null, text: "Version1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); } [Fact] public async Task Should_update_draft_only_multiple_times() { - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); - await UpdateTextAsync(ids1[0], "iv", "V2"); - await UpdateTextAsync(ids1[0], "iv", "V3"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); + await UpdateTextAsync(Ids1[0], "iv", "Version3"); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: null, text: "V3", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version3", target: SearchScope.All); + await SearchText(expected: null, text: "Version3", target: SearchScope.Published); } [Fact] public async Task Should_also_serve_published_after_publish() { - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); } [Fact] public async Task Should_also_update_published_content() { // Create initial content. - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); // Publish the content. - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); // Update the published content once. - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "Version1", target: SearchScope.All); + await SearchText(expected: null, text: "Version1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.Published); } [Fact] public async Task Should_also_update_published_content_multiple_times() { // Create initial content. - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); // Publish the content. - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); // Update the published content twice. - await UpdateTextAsync(ids1[0], "iv", "V2"); - await UpdateTextAsync(ids1[0], "iv", "V3"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); + await UpdateTextAsync(Ids1[0], "iv", "Version3"); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version3", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version3", target: SearchScope.Published); } [Fact] public async Task Should_simulate_new_version() { // Create initial content. - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); // Publish the content. - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); // Create a new version, the value is still the same as old version. - await CreateDraftAsync(ids1[0]); + await CreateDraftAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); // Make an update, this updates the new version only. - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); - // Publish the new version to get rid of the "V1" version. - await PublishAsync(ids1[0]); + // Publish the new version to get rid of the "Version1" version. + await PublishAsync(Ids1[0]); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "Version1", target: SearchScope.All); + await SearchText(expected: null, text: "Version1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.Published); // Unpublish the version - await UnpublishAsync(ids1[0]); + await UnpublishAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); } [Fact] public async Task Should_simulate_new_version_with_migration() { - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); // Publish the content. - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); // Create a new version, his updates the new version also. - await CreateDraftWithTextAsync(ids1[0], "iv", "V2"); + await CreateDraftWithTextAsync(Ids1[0], "iv", "Version2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); } [Fact] public async Task Should_simulate_content_reversion() { - await CreateTextAsync(ids1[0], "iv", "V1"); + await CreateTextAsync(Ids1[0], "iv", "Version1"); // Publish the content. - await PublishAsync(ids1[0]); + await PublishAsync(Ids1[0]); // Create a new version, the value is still the same as old version. - await CreateDraftAsync(ids1[0]); + await CreateDraftAsync(Ids1[0]); // Make an update, this updates the new version only. - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(Ids1[0], "iv", "Version2"); // Make an update, this updates the new version only. - await DeleteDraftAsync(ids1[0]); + await DeleteDraftAsync(Ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version1", target: SearchScope.Published); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "Version2", target: SearchScope.All); + await SearchText(expected: null, text: "Version2", target: SearchScope.Published); // Make an update, this updates the current version only. - await UpdateTextAsync(ids1[0], "iv", "V3"); + await UpdateTextAsync(Ids1[0], "iv", "Version3"); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); + await SearchText(expected: Ids1, text: "Version3", target: SearchScope.All); + await SearchText(expected: Ids1, text: "Version3", target: SearchScope.Published); } [Fact] public async Task Should_delete_documents_from_index() { - await CreateTextAsync(ids1[0], "iv", "V1_1"); - await CreateTextAsync(ids2[0], "iv", "V2_1"); + await CreateTextAsync(Ids1[0], "iv", "Version1_1"); + await CreateTextAsync(Ids2[0], "iv", "Version2_1"); - await SearchText(expected: ids1, text: "V1_1"); - await SearchText(expected: ids2, text: "V2_1"); + await SearchText(expected: Ids1, text: "Version1_1"); + await SearchText(expected: Ids2, text: "Version2_1"); - await DeleteAsync(ids1[0]); + await DeleteAsync(Ids1[0]); - await SearchText(expected: null, text: "V1_1"); - await SearchText(expected: ids2, text: "V2_1"); + await SearchText(expected: null, text: "Version1_1"); + await SearchText(expected: Ids2, text: "Version2_1"); } [Fact] public async Task Should_index_invalid_geodata() { - await CreateGeoAsync(ids1[0], "field", 144.34, -200); + await CreateGeoAsync(Ids1[0], "field", 144.34, -200); } [Fact] public async Task Should_index_invalid_geojsondata() { - await CreateGeoJsonAsync(ids1[0], "field", 144.34, -200); + await CreateGeoJsonAsync(Ids1[0], "field", 144.34, -200); } protected Task CreateTextAsync(DomainId id, string language, string text) @@ -404,8 +404,6 @@ public abstract class TextIndexerTestsBase : GivenContext contentEvent.SchemaId = SchemaId; await sut.On(Enumerable.Repeat(Envelope.Create(contentEvent), 1)); - - await Task.Delay(WaitAfterUpdate, default); } private static ContentData TextData(string language, string text) @@ -444,17 +442,9 @@ public abstract class TextIndexerTestsBase : GivenContext SchemaId = default, }; - var sut = await GetProcessAsync(); + var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); - var actual = await sut.TextIndex.SearchAsync(App, query, target, default); - if (expected != null) - { - actual.Should().BeEquivalentTo(expected.ToHashSet()); - } - else - { - actual.Should().BeEmpty(); - } + AssertIds(actual, expected); } protected async Task SearchText(List? expected, string text, SearchScope target = SearchScope.All) @@ -464,17 +454,9 @@ public abstract class TextIndexerTestsBase : GivenContext RequiredSchemaIds = [SchemaId.Id], }; - var sut = await GetProcessAsync(); + var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); - var actual = await sut.TextIndex.SearchAsync(App, query, target, default); - if (expected != null) - { - actual.Should().BeEquivalentTo(expected.ToHashSet()); - } - else - { - actual.Should().BeEmpty(); - } + AssertIds(actual, expected); } protected async Task SearchByAppText(List? expected, string text, SearchScope target = SearchScope.All) @@ -484,17 +466,42 @@ public abstract class TextIndexerTestsBase : GivenContext PreferredSchemaId = Schema.Id, }; + var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); + + AssertIds(actual, expected); + } + + private async Task SearchAsync(Func> query, Predicate isValid) + { var sut = await GetProcessAsync(); - var actual = await sut.TextIndex.SearchAsync(App, query, target, default); - if (expected != null) + using var cts = new CancellationTokenSource(WaitTime); + while (!cts.IsCancellationRequested) { - actual.Should().BeEquivalentTo(expected.ToHashSet()); + var actual = await query(sut.TextIndex); + if (isValid(actual)) + { + return actual; + } + + await Task.Delay(100, default); } - else - { + + return default; + } + + private static bool IsExpected(List? actual, List? expected) + { + return expected != null ? + actual != null && actual.SetEquals(expected) : + actual == null || actual.Count == 0; + } + + private static object AssertIds(List? actual, List? expected) + { + return expected != null ? + actual.Should().BeEquivalentTo(expected) : actual.Should().BeEmpty(); - } } private async Task GetProcessAsync() diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs index 001337575..4468fb563 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs @@ -41,13 +41,14 @@ public class QueryFromODataTests new FilterField(FilterSchema.Any, "properties"), }; - var filterSchema = new FilterSchema(FilterSchemaType.Object) + var queryModel = new QueryModel { - Fields = fields.ToReadonlyList(), + Schema = new FilterSchema(FilterSchemaType.Object) + { + Fields = fields.ToReadonlyList(), + }, }; - var queryModel = new QueryModel { Schema = filterSchema }; - EdmModel = queryModel.ConvertToEdm("Squidex", "Content"); } diff --git a/backend/tests/mssql/Dockerfile b/backend/tests/mssql/Dockerfile new file mode 100644 index 000000000..1ad3d0755 --- /dev/null +++ b/backend/tests/mssql/Dockerfile @@ -0,0 +1,23 @@ +# Base OS layer: Latest Ubuntu LTS +FROM --platform=linux/amd64 ubuntu:focal + +# Install prerequistes since it is needed to get repo config for SQL server +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get install -yq curl apt-transport-https gnupg && \ + # Get official Microsoft repository configuration + curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + curl https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb --output packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb && \ + curl https://packages.microsoft.com/config/ubuntu/20.04/mssql-server-2022.list | tee /etc/apt/sources.list.d/mssql-server.list && \ + apt-get update && \ + # Install SQL Server from apt + apt-get install -y mssql-server && \ + # Install optional packages + apt-get install -y mssql-server-fts && \ + ACCEPT_EULA=Y apt-get install -y mssql-tools && \ + # Cleanup the Dockerfile + apt-get clean && \ + rm -rf /var/lib/apt/lists + +# Run SQL Server process +CMD /opt/mssql/bin/sqlservr \ No newline at end of file diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs index 392f96afc..db7fe8105 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -168,9 +168,9 @@ public class AssetFormatTests(CreatedAppFixture fixture) : IClassFixture { ["en"] = index.ToString(CultureInfo.InvariantCulture) diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 09ad3f9fb..d0f5105aa 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -377,10 +377,9 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture true); @@ -388,14 +387,13 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture } }, new UpsertSchemaFieldDto + { + Name = TestEntityData.SearchableField, + Properties = new StringFieldPropertiesDto + { + IsRequired = false + } + }, + new UpsertSchemaFieldDto { Name = TestEntityData.GeoField, Properties = new GeolocationFieldPropertiesDto @@ -123,6 +131,8 @@ public sealed class TestEntityData { public static readonly string LocalizedField = nameof(Localized).ToLowerInvariant(); + public static readonly string SearchableField = nameof(Searchable).ToLowerInvariant(); + public static readonly string StringField = nameof(String).ToLowerInvariant(); public static readonly string NumberField = nameof(Number).ToLowerInvariant(); @@ -144,6 +154,9 @@ public sealed class TestEntityData [JsonConverter(typeof(InvariantConverter))] public string? String { get; set; } + [JsonConverter(typeof(InvariantConverter))] + public string? Searchable { get; set; } + [JsonConverter(typeof(InvariantConverter))] public JToken? Json { get; set; } diff --git a/tools/TestSuite/docker-compose.yml b/tools/TestSuite/docker-compose.yml index 2e959c108..f3d38f382 100644 --- a/tools/TestSuite/docker-compose.yml +++ b/tools/TestSuite/docker-compose.yml @@ -7,7 +7,7 @@ services: - internal db_postgres: - image: "postgres" + image: "postgis/postgis" ports: - "5432:5432" environment: @@ -41,11 +41,12 @@ services: retries: 10 db_sqlserver: - image: squidex/mssql-dev + image: squidex/mssql-dev:2 ports: - "1433:1433" environment: - SA_PASSWORD=1q2w3e!R + - SA_USER=none - ACCEPT_EULA=Y networks: - internal @@ -54,6 +55,16 @@ services: interval: 10s retries: 10 + db_sqlserver_init: + image: mcr.microsoft.com/mssql-tools + entrypoint: > + /opt/mssql-tools/bin/sqlcmd -S db_sqlserver -U sa -P 1q2w3e!R -d master -Q 'CREATE DATABASE ''squidex''' + networks: + - internal + depends_on: + db_sqlserver: + condition: service_healthy + # Base configuration squidex_mongo1: extends: diff --git a/tools/TestSuite/sql-server/Dockerfile b/tools/TestSuite/sql-server/Dockerfile index 431583cb7..00ce444e8 100644 --- a/tools/TestSuite/sql-server/Dockerfile +++ b/tools/TestSuite/sql-server/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04 +FROM vibs2006/sql_server_fts # Switch to root to perform installation USER root