mirror of https://github.com/Squidex/squidex.git
Browse Source
* Run tests on all servers. * Revert schema tests. * T * t * Text * Fix. * Add migration. * Update tests. * Test degrees * Update test * Update duration. * Improvements. * Add searchable text field. * Fix whitespaces.pull/1192/head
committed by
GitHub
98 changed files with 6533 additions and 570 deletions
@ -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<TextContentState>(b => |
||||
|
{ |
||||
|
b.ToTable("TextState"); |
||||
|
b.HasKey(x => x.UniqueContentId); |
||||
|
b.Property(x => x.UniqueContentId).AsString(); |
||||
|
b.Property(x => x.State).AsString(); |
||||
|
}); |
||||
|
|
||||
|
builder.Entity<EFTextIndexTextEntity>(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<EFTextIndexGeoEntity>(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); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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<TContext>(IDbContextFactory<TContext> 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<EFTextIndexTextEntity>().Where(x => x.AppId == app.Id) |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
|
||||
|
await dbContext.Set<EFTextIndexGeoEntity>().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<EFTextIndexTextEntity>().Where(x => x.AppId == app.Id && x.SchemaId == schema.Id) |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
|
||||
|
await dbContext.Set<EFTextIndexGeoEntity>().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<EFTextIndexTextEntity>() |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
|
||||
|
await dbContext.Set<EFTextIndexGeoEntity>() |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<DomainId>?> 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<EFTextIndexGeoEntity>() |
||||
|
.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<List<DomainId>?> 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<DomainId> 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<EFTextIndexTextEntity>().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<EFTextIndexTextEntity>(); |
||||
|
var insertsGeo = new List<EFTextIndexGeoEntity>(); |
||||
|
|
||||
|
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<EFTextIndexGeoEntity>().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<EFTextIndexTextEntity>() |
||||
|
.Where(x => x.Id == id) |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
|
||||
|
await dbContext.Set<EFTextIndexGeoEntity>() |
||||
|
.Where(x => x.Id == id) |
||||
|
.ExecuteDeleteAsync(ct); |
||||
|
break; |
||||
|
case UpdateIndexEntry update: |
||||
|
await dbContext.Set<EFTextIndexTextEntity>() |
||||
|
.Where(x => x.Id == id) |
||||
|
.ExecuteUpdateAsync(u => u |
||||
|
.SetProperty(x => x.ServeAll, update.ServeAll) |
||||
|
.SetProperty(x => x.ServePublished, update.ServePublished), |
||||
|
ct); |
||||
|
|
||||
|
await dbContext.Set<EFTextIndexGeoEntity>() |
||||
|
.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<TContext> CreateDbContextAsync(CancellationToken ct) |
||||
|
{ |
||||
|
return dbContextFactory.CreateDbContextAsync(ct); |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
@ -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<EFTextIndexTextEntity> WhereScope(this IQueryable<EFTextIndexTextEntity> query, SearchScope scope) |
||||
|
{ |
||||
|
if (scope == SearchScope.All) |
||||
|
{ |
||||
|
return query.Where(x => x.ServeAll); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return query.Where(x => x.ServePublished); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IQueryable<EFTextIndexGeoEntity> WhereScope(this IQueryable<EFTextIndexGeoEntity> query, SearchScope scope) |
||||
|
{ |
||||
|
if (scope == SearchScope.All) |
||||
|
{ |
||||
|
return query.Where(x => x.ServeAll); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return query.Where(x => x.ServePublished); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<List<DomainId>?> SearchAsync(App app, TextQuery query, SearchScope scope, |
|
||||
CancellationToken ct = default) |
|
||||
{ |
|
||||
return Task.FromResult<List<DomainId>?>(null); |
|
||||
} |
|
||||
|
|
||||
public Task<List<DomainId>?> SearchAsync(App app, GeoQuery query, SearchScope scope, |
|
||||
CancellationToken ct = default) |
|
||||
{ |
|
||||
return Task.FromResult<List<DomainId>?>(null); |
|
||||
} |
|
||||
} |
|
||||
@ -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
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,74 @@ |
|||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
using NetTopologySuite.Geometries; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.MySql.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddFullText : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "Geos", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "varchar(400)", maxLength: 400, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
AppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
SchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
ContentId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
Stage = table.Column<byte>(type: "tinyint unsigned", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "tinyint(1)", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "tinyint(1)", nullable: false), |
||||
|
GeoField = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
GeoObject = table.Column<Geometry>(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<string>(type: "varchar(400)", maxLength: 400, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
AppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
SchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
ContentId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
|
Stage = table.Column<byte>(type: "tinyint unsigned", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "tinyint(1)", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "tinyint(1)", nullable: false), |
||||
|
Texts = table.Column<string>(type: "longtext", nullable: false) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4") |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_Texts", x => x.Id); |
||||
|
}) |
||||
|
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Geos"); |
||||
|
|
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Texts"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,68 @@ |
|||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
using NetTopologySuite.Geometries; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.Postgres.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddFullText : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.AlterDatabase() |
||||
|
.Annotation("Npgsql:PostgresExtension:postgis", ",,"); |
||||
|
|
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "Geos", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false), |
||||
|
AppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
SchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
ContentId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
Stage = table.Column<byte>(type: "smallint", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "boolean", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "boolean", nullable: false), |
||||
|
GeoField = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
GeoObject = table.Column<Geometry>(type: "geometry", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_Geos", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "Texts", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false), |
||||
|
AppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
SchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
ContentId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false), |
||||
|
Stage = table.Column<byte>(type: "smallint", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "boolean", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "boolean", nullable: false), |
||||
|
Texts = table.Column<string>(type: "text", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_Texts", x => x.Id); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Geos"); |
||||
|
|
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Texts"); |
||||
|
|
||||
|
migrationBuilder.AlterDatabase() |
||||
|
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,62 @@ |
|||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
using NetTopologySuite.Geometries; |
||||
|
|
||||
|
#nullable disable |
||||
|
|
||||
|
namespace Squidex.Providers.SqlServer.Migrations |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public partial class AddFullText : Migration |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "Geos", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: false), |
||||
|
AppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
SchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
ContentId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
Stage = table.Column<byte>(type: "tinyint", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "bit", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "bit", nullable: false), |
||||
|
GeoField = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
GeoObject = table.Column<Geometry>(type: "geography", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_Geos", x => x.Id); |
||||
|
}); |
||||
|
|
||||
|
migrationBuilder.CreateTable( |
||||
|
name: "Texts", |
||||
|
columns: table => new |
||||
|
{ |
||||
|
Id = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: false), |
||||
|
AppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
SchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
ContentId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false), |
||||
|
Stage = table.Column<byte>(type: "tinyint", nullable: false), |
||||
|
ServeAll = table.Column<bool>(type: "bit", nullable: false), |
||||
|
ServePublished = table.Column<bool>(type: "bit", nullable: false), |
||||
|
Texts = table.Column<string>(type: "nvarchar(max)", nullable: false) |
||||
|
}, |
||||
|
constraints: table => |
||||
|
{ |
||||
|
table.PrimaryKey("PK_Texts", x => x.Id); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||
|
{ |
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Geos"); |
||||
|
|
||||
|
migrationBuilder.DropTable( |
||||
|
name: "Texts"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<LangVersion>latest</LangVersion> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<IsRoslynComponent>true</IsRoslynComponent> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<LangVersion>latest</LangVersion> |
||||
|
<RootNamespace>Squidex</RootNamespace> |
||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<None Remove="Template.handlebar" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Handlebars.Net" Version="2.1.6" PrivateAssets="all" GeneratePathProperty="true" /> |
||||
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179"> |
||||
|
<PrivateAssets>all</PrivateAssets> |
||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
||||
|
</PackageReference> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" /> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0"> |
||||
|
<PrivateAssets>all</PrivateAssets> |
||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
||||
|
</PackageReference> |
||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> |
||||
|
</ItemGroup> |
||||
|
<PropertyGroup> |
||||
|
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> |
||||
|
</PropertyGroup> |
||||
|
<Target Name="GetDependencyTargetPaths"> |
||||
|
<ItemGroup> |
||||
|
<TargetPathWithTargetPlatformMoniker Include="$(PKGHandlebars_Net)\lib\netstandard2.0\Handlebars.dll" IncludeRuntimeDependency="false" /> |
||||
|
</ItemGroup> |
||||
|
</Target> |
||||
|
<ItemGroup> |
||||
|
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Template.handlebar" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -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}}<TestDbContextPostgres>(fixture) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Trait("Category", "TestContainer")] |
||||
|
[Collection("MySql")] |
||||
|
public class MySql{{className}}(MySqlFixture fixture) : {{baseName}}<TestDbContextMySql>(fixture) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Trait("Category", "TestContainer")] |
||||
|
[Collection("SqlServer")] |
||||
|
public class SqlServer{{className}}(SqlServerFixture fixture) : {{baseName}}<TestDbContextSqlServer>(fixture) |
||||
|
{ |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
@ -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<BaseNamespaceDeclarationSyntax>().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)); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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<TContext>(ISqlFixture<TContext> fixture) : TextIndexerTests where TContext : DbContext |
||||
|
{ |
||||
|
public override bool SupportsQuerySyntax => false; |
||||
|
|
||||
|
public override async Task<ITextIndex> CreateSutAsync() |
||||
|
{ |
||||
|
var sut = new EFTextIndex<TContext>(fixture.DbContextFactory, fixture.Dialect); |
||||
|
|
||||
|
await sut.InitializeAsync(default); |
||||
|
return sut; |
||||
|
} |
||||
|
} |
||||
@ -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<TestDbContext> |
|
||||
{ |
|
||||
protected override async Task<TestDbContext> CreateDbContextAsync() |
|
||||
{ |
|
||||
var context = await fixture.DbContextFactory.CreateDbContextAsync(); |
|
||||
|
|
||||
return context; |
|
||||
} |
|
||||
|
|
||||
protected override SqlDialect CreateDialect() |
|
||||
{ |
|
||||
return MySqlDialect.Instance; |
|
||||
} |
|
||||
} |
|
||||
@ -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<TestDbContext> |
|
||||
{ |
|
||||
protected override async Task<TestDbContext> CreateDbContextAsync() |
|
||||
{ |
|
||||
var context = await fixture.DbContextFactory.CreateDbContextAsync(); |
|
||||
|
|
||||
return context; |
|
||||
} |
|
||||
|
|
||||
protected override SqlDialect CreateDialect() |
|
||||
{ |
|
||||
return PostgresDialect.Instance; |
|
||||
} |
|
||||
} |
|
||||
@ -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<TestDbContext> |
|
||||
{ |
|
||||
protected override async Task<TestDbContext> CreateDbContextAsync() |
|
||||
{ |
|
||||
var context = await fixture.DbContextFactory.CreateDbContextAsync(); |
|
||||
|
|
||||
return context; |
|
||||
} |
|
||||
|
|
||||
protected override SqlDialect CreateDialect() |
|
||||
{ |
|
||||
return SqlServerDialect.Instance; |
|
||||
} |
|
||||
} |
|
||||
@ -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<TContext> where TContext : DbContext |
||||
|
{ |
||||
|
SqlDialect Dialect { get; } |
||||
|
|
||||
|
IDbContextFactory<TContext> DbContextFactory { get; } |
||||
|
} |
||||
@ -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 |
||||
Loading…
Reference in new issue