Skip to content

Commit 8f607a6

Browse files
Query improvements.
1 parent 075c200 commit 8f607a6

File tree

22 files changed

+2028
-192
lines changed

22 files changed

+2028
-192
lines changed

backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
// ==========================================================================
77

88
using System.Linq.Expressions;
9-
using Google.Protobuf;
109
using Microsoft.EntityFrameworkCore;
1110
using Microsoft.EntityFrameworkCore.Diagnostics;
1211
using Microsoft.EntityFrameworkCore.Infrastructure;
1312
using Microsoft.EntityFrameworkCore.Query;
1413
using Microsoft.Extensions.DependencyInjection;
15-
using Microsoft.Extensions.Options;
1614
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
1715
using PhenX.EntityFrameworkCore.BulkInsert.Options;
1816
using Squidex.Domain.Apps.Entities;

backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
using System.Collections.Concurrent;
99
using Microsoft.EntityFrameworkCore;
10-
using Microsoft.EntityFrameworkCore.Infrastructure;
1110
using Microsoft.EntityFrameworkCore.Internal;
1211

1312
#pragma warning disable EF1001 // Internal EF Core API usage.

backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialect.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System.Collections;
99
using System.Text;
10+
using Microsoft.EntityFrameworkCore;
1011

1112
namespace Squidex.Infrastructure.Queries;
1213

@@ -19,6 +20,12 @@ public virtual bool IsDuplicateIndexException(Exception exception, string name)
1920
return false;
2021
}
2122

23+
public virtual Task InitializeAsync(DbContext dbContext,
24+
CancellationToken ct)
25+
{
26+
return Task.CompletedTask;
27+
}
28+
2229
public virtual string BuildSelectStatement(SqlQuery request)
2330
{
2431
var sb = new StringBuilder("SELECT");
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// ==========================================================================
2+
// Squidex Headless CMS
3+
// ==========================================================================
4+
// Copyright (c) Squidex UG (haftungsbeschraenkt)
5+
// All rights reserved. Licensed under the MIT license.
6+
// ==========================================================================
7+
8+
using Microsoft.EntityFrameworkCore;
9+
using Squidex.Hosting;
10+
11+
namespace Squidex.Infrastructure.Queries;
12+
13+
public sealed class SqlDialectInitializer<TContext>(IDbContextFactory<TContext> dbContextFactory)
14+
: IInitializable where TContext : DbContext
15+
{
16+
public async Task InitializeAsync(CancellationToken ct)
17+
{
18+
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);
19+
if (dbContext is not IDbContextWithDialect withDialect)
20+
{
21+
return;
22+
}
23+
24+
await withDialect.Dialect.InitializeAsync(dbContext, ct);
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Squidex.Providers.MySql.Content;
2+
3+
public static class JsonFunction
4+
{
5+
}

backend/src/Squidex.Data.EntityFramework/Providers/MySql/Extensions.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath p
1717
{
1818
sb.Append('`');
1919
sb.Append(path[0]);
20-
sb.Append("`, \'$");
20+
sb.Append("`, ");
21+
sb.AppendJsonPropertyPath(path);
22+
return sb;
23+
}
24+
25+
public static StringBuilder AppendJsonPropertyPath(this StringBuilder sb, PropertyPath path)
26+
{
27+
sb.Append("\'$");
2128

2229
foreach (var property in path.Skip(1))
2330
{
@@ -38,6 +45,11 @@ public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath p
3845
return sb;
3946
}
4047

48+
public static string JsonSubPath(this PropertyPath path)
49+
{
50+
return new StringBuilder().AppendJsonPropertyPath(path).ToString();
51+
}
52+
4153
public static string JsonPath(this PropertyPath path)
4254
{
4355
return new StringBuilder().AppendJsonPath(path).ToString();
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// ==========================================================================
2+
// Squidex Headless CMS
3+
// ==========================================================================
4+
// Copyright (c) Squidex UG (haftungsbeschraenkt)
5+
// All rights reserved. Licensed under the MIT license.
6+
// ==========================================================================
7+
8+
using Microsoft.EntityFrameworkCore;
9+
using Squidex.Infrastructure.Queries;
10+
11+
namespace Squidex.Providers.MySql;
12+
13+
public static class JsonFunction
14+
{
15+
private const int TypeAny = 0;
16+
private const int TypeNull = 1;
17+
private const int TypeText = 2;
18+
private const int TypeBoolean = 3;
19+
private const int TypeNumber = 4;
20+
21+
private static readonly Dictionary<(int Type, CompareOperator Operator), string> Functions = new()
22+
{
23+
[(TypeAny, CompareOperator.Empty)] = "json_empty",
24+
[(TypeAny, CompareOperator.Exists)] = "json_exists",
25+
[(TypeNull, CompareOperator.Equals)] = "json_null_equals",
26+
[(TypeNull, CompareOperator.NotEquals)] = "json_null_notequals",
27+
[(TypeText, CompareOperator.Contains)] = "json_text_contains",
28+
[(TypeText, CompareOperator.EndsWith)] = "json_text_endswith",
29+
[(TypeText, CompareOperator.Equals)] = "json_text_equals",
30+
[(TypeText, CompareOperator.GreaterThan)] = "json_text_greaterthan",
31+
[(TypeText, CompareOperator.GreaterThanOrEqual)] = "json_text_greaterthanorequal",
32+
[(TypeText, CompareOperator.In)] = "json_text_in",
33+
[(TypeText, CompareOperator.LessThan)] = "json_text_lessthan",
34+
[(TypeText, CompareOperator.LessThanOrEqual)] = "json_text_lessthanorequal",
35+
[(TypeText, CompareOperator.Matchs)] = "json_text_matchs",
36+
[(TypeText, CompareOperator.NotEquals)] = "json_text_notequals",
37+
[(TypeText, CompareOperator.StartsWith)] = "json_text_startswith",
38+
[(TypeBoolean, CompareOperator.Equals)] = "json_boolean_equals",
39+
[(TypeBoolean, CompareOperator.In)] = "json_boolean_in",
40+
[(TypeBoolean, CompareOperator.NotEquals)] = "json_boolean_notequals",
41+
[(TypeNumber, CompareOperator.Equals)] = "json_number_equals",
42+
[(TypeNumber, CompareOperator.GreaterThan)] = "json_number_greaterthan",
43+
[(TypeNumber, CompareOperator.GreaterThanOrEqual)] = "json_number_greaterthanorequal",
44+
[(TypeNumber, CompareOperator.In)] = "json_number_in",
45+
[(TypeNumber, CompareOperator.LessThan)] = "json_number_lessthan",
46+
[(TypeNumber, CompareOperator.LessThanOrEqual)] = "json_number_lessthanorequal",
47+
[(TypeNumber, CompareOperator.NotEquals)] = "json_number_notequals",
48+
};
49+
50+
public static async Task InitializeAsync(DbContext dbContext,
51+
CancellationToken ct)
52+
{
53+
var sqlStream = typeof(MySqlDialect).Assembly.GetManifestResourceStream("Squidex.Providers.MySql.json_function.sql");
54+
var sqlText = await new StreamReader(sqlStream!).ReadToEndAsync(ct);
55+
56+
sqlText = sqlText.Replace("{", "{{", StringComparison.Ordinal);
57+
sqlText = sqlText.Replace("}", "}}", StringComparison.Ordinal);
58+
59+
var statements = sqlText.Split(";;", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
60+
// We want to filter out the drop statements and multiple function creations are not supported.
61+
foreach (var statement in statements)
62+
{
63+
#if RELEASE
64+
if (statement.StartsWith("DROP", StringComparison.Ordinal))
65+
{
66+
continue;
67+
}
68+
#endif
69+
await dbContext.Database.ExecuteSqlRawAsync(statement, ct);
70+
}
71+
}
72+
73+
public static string Create(PropertyPath path, CompareOperator op, ClrValue value, string formattedValue)
74+
{
75+
var type = -1;
76+
if (op is CompareOperator.Exists or CompareOperator.Empty)
77+
{
78+
type = TypeAny;
79+
}
80+
else
81+
{
82+
switch (value.ValueType)
83+
{
84+
case ClrValueType.Single:
85+
case ClrValueType.Double:
86+
case ClrValueType.Int32:
87+
case ClrValueType.Int64:
88+
type = TypeNumber;
89+
break;
90+
case ClrValueType.Instant:
91+
case ClrValueType.Guid:
92+
case ClrValueType.String:
93+
type = TypeText;
94+
break;
95+
case ClrValueType.Boolean:
96+
type = TypeBoolean;
97+
break;
98+
case ClrValueType.Null:
99+
type = TypeNull;
100+
break;
101+
}
102+
}
103+
104+
if (!Functions.TryGetValue((type, op), out var fn))
105+
{
106+
throw new NotSupportedException($"No jsonb function for type={value.ValueType}, operator={op}.");
107+
}
108+
109+
if (type is TypeNull or TypeAny)
110+
{
111+
return $"{fn}(`{path[0]}`, {path.JsonSubPath()})";
112+
}
113+
114+
if (value.IsList)
115+
{
116+
return $"{fn}(`{path[0]}`, {path.JsonSubPath()}, JSON_ARRAY({formattedValue}))";
117+
}
118+
119+
return $"{fn}(`{path[0]}`, {path.JsonSubPath()}, {formattedValue})";
120+
}
121+
}

backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs

Lines changed: 8 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// All rights reserved. Licensed under the MIT license.
66
// ==========================================================================
77

8-
using System.Text;
8+
using Microsoft.EntityFrameworkCore;
99
using MySqlConnector;
1010
using Squidex.Infrastructure.Queries;
1111

@@ -19,6 +19,12 @@ private MySqlDialect()
1919
{
2020
}
2121

22+
public override Task InitializeAsync(DbContext dbContext,
23+
CancellationToken ct)
24+
{
25+
return JsonFunction.InitializeAsync(dbContext, ct);
26+
}
27+
2228
public override bool IsDuplicateIndexException(Exception exception, string name)
2329
{
2430
return exception is MySqlException ex && ex.Number == 1061;
@@ -107,69 +113,7 @@ public override string Where(PropertyPath path, CompareOperator op, ClrValue val
107113
{
108114
if (isJson)
109115
{
110-
var sqlPath = path.JsonPath();
111-
var sqlOp = FormatOperator(op, value);
112-
var sqlRhs = FormatValues(op, value, queryParameters);
113-
114-
string BuildCondition(string path)
115-
{
116-
var isNumeric = value.ValueType is
117-
ClrValueType.Single or
118-
ClrValueType.Double or
119-
ClrValueType.Int32 or
120-
ClrValueType.Int64;
121-
if (isNumeric)
122-
{
123-
return $"""
124-
(CASE WHEN JSON_TYPE({path}) IN ('INTEGER', 'DOUBLE', 'DECIMAL')
125-
THEN CAST(JSON_UNQUOTE({path}) AS DECIMAL(65,10)) {sqlOp} {sqlRhs}
126-
ELSE FALSE
127-
END)
128-
""";
129-
}
130-
131-
var isBoolean = value.ValueType is ClrValueType.Boolean;
132-
if (isBoolean)
133-
{
134-
return $"""
135-
(CASE WHEN JSON_TYPE({path}) = 'BOOLEAN'
136-
THEN IF(JSON_UNQUOTE({path}) = 'true', TRUE, FALSE) {sqlOp} {sqlRhs}
137-
ELSE FALSE
138-
END)
139-
""";
140-
}
141-
142-
var isString = value.ValueType is
143-
ClrValueType.Instant or
144-
ClrValueType.Guid or
145-
ClrValueType.String;
146-
if (isString)
147-
{
148-
return $"JSON_UNQUOTE({path}) {sqlOp} {sqlRhs}";
149-
}
150-
151-
var isNull = value.ValueType is ClrValueType.Null;
152-
if (isNull)
153-
{
154-
var nullOp = FormatOperator(op, "null");
155-
return $"COALESCE(JSON_TYPE({path}), 'NULL') {nullOp} 'NULL'";
156-
}
157-
158-
return base.Where(path, op, value, queryParameters, false);
159-
}
160-
161-
return $"""
162-
CASE WHEN JSON_TYPE(JSON_EXTRACT({sqlPath})) = 'ARRAY'
163-
THEN EXISTS (
164-
SELECT 1
165-
FROM JSON_TABLE(JSON_EXTRACT({sqlPath}), '$[*]' COLUMNS (
166-
__element JSON PATH '$'
167-
)) AS __jt
168-
WHERE {BuildCondition("__element")}
169-
)
170-
ELSE {BuildCondition($"JSON_EXTRACT({sqlPath})")}
171-
END
172-
""";
116+
return JsonFunction.Create(path, op, value, FormatValues(op, value, queryParameters, true));
173117
}
174118

175119
return base.Where(path, op, value, queryParameters, isJson);

0 commit comments

Comments
 (0)