Browse Source

More powerful queries.

pull/1293/head
Sebastian Stehle 3 months ago
parent
commit
075c20082b
  1. 85
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs
  2. 84
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs
  3. 108
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs
  4. 2
      backend/src/Squidex.Infrastructure/Queries/QueryModel.cs
  5. 930
      backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs
  6. 10
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs
  7. 315
      frontend/package-lock.json
  8. 2
      frontend/package.json
  9. 5
      frontend/src/app/shared/components/search/search-form.component.ts

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

@ -110,51 +110,66 @@ public sealed class MySqlDialect : SqlDialect
var sqlPath = path.JsonPath();
var sqlOp = FormatOperator(op, value);
var sqlRhs = FormatValues(op, value, queryParameters);
var isBoolean = value.ValueType is ClrValueType.Boolean;
string ScalarCondition()
string BuildCondition(string path)
{
if (isBoolean)
var isNumeric = value.ValueType is
ClrValueType.Single or
ClrValueType.Double or
ClrValueType.Int32 or
ClrValueType.Int64;
if (isNumeric)
{
return $"IF(JSON_VALUE({sqlPath}) = 'true', 1, 0) {sqlOp} {sqlRhs}";
return $"""
(CASE WHEN JSON_TYPE({path}) IN ('INTEGER', 'DOUBLE', 'DECIMAL')
THEN CAST(JSON_UNQUOTE({path}) AS DECIMAL(65,10)) {sqlOp} {sqlRhs}
ELSE FALSE
END)
""";
}
return base.Where(path, op, value, queryParameters, isJson);
}
var isBoolean = value.ValueType is ClrValueType.Boolean;
if (isBoolean)
{
return $"""
(CASE WHEN JSON_TYPE({path}) = 'BOOLEAN'
THEN IF(JSON_UNQUOTE({path}) = 'true', TRUE, FALSE) {sqlOp} {sqlRhs}
ELSE FALSE
END)
""";
}
if (value.IsList && op == CompareOperator.In)
{
return $"""
IF(
JSON_TYPE(JSON_EXTRACT({sqlPath})) = 'ARRAY',
JSON_OVERLAPS(JSON_EXTRACT({sqlPath}), JSON_ARRAY{sqlRhs}),
JSON_CONTAINS(JSON_ARRAY{sqlRhs}, JSON_EXTRACT({sqlPath}))
)
""";
}
var isString = value.ValueType is
ClrValueType.Instant or
ClrValueType.Guid or
ClrValueType.String;
if (isString)
{
return $"JSON_UNQUOTE({path}) {sqlOp} {sqlRhs}";
}
if (op == CompareOperator.Equals)
{
var valueExpr = value.ValueType switch
var isNull = value.ValueType is ClrValueType.Null;
if (isNull)
{
ClrValueType.Boolean =>
$"IF({sqlRhs} = 1, 'true', 'false')",
ClrValueType.String =>
$"JSON_QUOTE({sqlRhs})",
_ =>
$"CAST({sqlRhs} AS JSON)",
};
return $"""
IF(
JSON_TYPE(JSON_EXTRACT({sqlPath})) = 'ARRAY',
JSON_CONTAINS(JSON_EXTRACT({sqlPath}), {valueExpr}),
{ScalarCondition()}
)
""";
var nullOp = FormatOperator(op, "null");
return $"COALESCE(JSON_TYPE({path}), 'NULL') {nullOp} 'NULL'";
}
return base.Where(path, op, value, queryParameters, false);
}
return ScalarCondition();
return $"""
CASE WHEN JSON_TYPE(JSON_EXTRACT({sqlPath})) = 'ARRAY'
THEN EXISTS (
SELECT 1
FROM JSON_TABLE(JSON_EXTRACT({sqlPath}), '$[*]' COLUMNS (
__element JSON PATH '$'
)) AS __jt
WHERE {BuildCondition("__element")}
)
ELSE {BuildCondition($"JSON_EXTRACT({sqlPath})")}
END
""";
}
return base.Where(path, op, value, queryParameters, isJson);

84
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
using Npgsql;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.Postgres.App;
@ -77,69 +76,54 @@ public class PostgresDialect : SqlDialect
var sqlOp = FormatOperator(op, value);
var sqlRhs = FormatValues(op, value, queryParameters);
var isBoolean = value.ValueType is ClrValueType.Boolean;
var isNumeric = value.ValueType is
ClrValueType.Single or
ClrValueType.Double or
ClrValueType.Int32 or
ClrValueType.Int64;
string ScalarCondition()
string BuildCondition(string path, string castPath)
{
var isNumeric = value.ValueType is
ClrValueType.Single or
ClrValueType.Double or
ClrValueType.Int32 or
ClrValueType.Int64;
if (isNumeric)
{
return $"(CASE WHEN jsonb_typeof({sqlPath}) = 'number' THEN ({sqlPathCast})::numeric {sqlOp} {sqlRhs} ELSE FALSE END)";
return $"(CASE WHEN jsonb_typeof({path}) = 'number' THEN ({castPath})::numeric {sqlOp} {sqlRhs} ELSE FALSE END)";
}
var isBoolean = value.ValueType is ClrValueType.Boolean;
if (isBoolean)
{
return $"(CASE WHEN jsonb_typeof({sqlPath}) = 'boolean' THEN ({sqlPathCast})::boolean {sqlOp} {sqlRhs} ELSE FALSE END)";
return $"(CASE WHEN jsonb_typeof({path}) = 'boolean' THEN ({castPath})::boolean {sqlOp} {sqlRhs} ELSE FALSE END)";
}
return base.Where(path, op, value, queryParameters, true);
}
string ToJsonbValue()
{
if (isNumeric)
var isString = value.ValueType is
ClrValueType.Instant or
ClrValueType.Guid or
ClrValueType.String;
if (isString)
{
return $"to_jsonb({sqlRhs}::numeric)";
return $"{path} #>> '{{{{}}}}' {sqlOp} {sqlRhs}";
}
if (isBoolean)
var isNull = value.ValueType is ClrValueType.Null;
if (isNull)
{
return $"to_jsonb({sqlRhs}::boolean)";
var nullOp = FormatOperator(op, "null");
return $"jsonb_typeof({path}) {nullOp} 'null'";
}
return $"to_jsonb({sqlRhs}::text)";
}
if (value.IsList && op == CompareOperator.In)
{
return $"""
CASE WHEN jsonb_typeof({sqlPath}) = 'array'
THEN EXISTS (
SELECT 1
FROM jsonb_array_elements({sqlPath}) AS elem
WHERE jsonb_build_array{sqlRhs} @> elem
)
ELSE {sqlPath} <@ jsonb_build_array{sqlRhs}
END
""";
}
if (op == CompareOperator.Equals)
{
return $"""
CASE WHEN jsonb_typeof({sqlPath}) = 'array'
THEN {sqlPath} @> jsonb_build_array({ToJsonbValue()})
ELSE {ScalarCondition()}
END
""";
return base.Where(path, op, value, queryParameters, false);
}
return ScalarCondition();
return $"""
CASE WHEN jsonb_typeof({sqlPath}) = 'array'
THEN EXISTS (
SELECT 1
FROM jsonb_array_elements({sqlPath}) AS __element
WHERE {BuildCondition("__element", "__element")}
)
ELSE {BuildCondition(sqlPath, sqlPathCast)}
END
""";
}
return base.Where(path, op, value, queryParameters, isJson);
@ -148,12 +132,16 @@ public class PostgresDialect : SqlDialect
protected override string FormatField(PropertyPath path, bool isJson)
{
var baseField = path[0];
if (isJson && path.Count > 1)
{
return path.JsonPath(true);
}
if (baseField == "__element" || baseField.Contains("->", StringComparison.Ordinal))
{
return baseField;
}
return $"\"{baseField}\"";
}
}

108
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDialect.cs

@ -92,92 +92,62 @@ public sealed class SqlServerDialect : SqlDialect
var sqlOp = FormatOperator(op, value);
var sqlRhs = FormatValues(op, value, queryParameters);
var isNull = value.ValueType is ClrValueType.Null;
var isBoolean = value.ValueType is ClrValueType.Boolean;
var isNumeric = value.ValueType is
ClrValueType.Single or
ClrValueType.Double or
ClrValueType.Int32 or
ClrValueType.Int64;
string ScalarCondition()
string BuildCondition(string path, string queryPath)
{
if (isNull)
{
if (op == CompareOperator.Equals)
{
return $"JSON_QUERY({sqlPath}) IS NULL AND JSON_VALUE({sqlPath}) IS NULL";
}
if (op == CompareOperator.NotEquals)
{
return $"JSON_QUERY({sqlPath}) IS NOT NULL OR JSON_VALUE({sqlPath}) IS NOT NULL";
}
}
var isNumeric = value.ValueType is
ClrValueType.Single or
ClrValueType.Double or
ClrValueType.Int32 or
ClrValueType.Int64;
if (isNumeric)
{
return $"TRY_CAST(JSON_VALUE({sqlPath}) AS NUMERIC) {sqlOp} {sqlRhs}";
return $"TRY_CAST({path} AS NUMERIC) {sqlOp} {sqlRhs}";
}
var isBoolean = value.ValueType is ClrValueType.Boolean;
if (isBoolean)
{
return $"IIF(JSON_VALUE({sqlPath}) = 'true', 1, 0) {sqlOp} {sqlRhs}";
return $"IIF({path} = 'true', 1, IIF({path} = 'false', 0, NULL)) {sqlOp} {sqlRhs}";
}
return base.Where(path, op, value, queryParameters, isJson);
}
string ArrayCondition(string field, string op)
{
if (isNumeric)
var isString = value.ValueType is
ClrValueType.Instant or
ClrValueType.Guid or
ClrValueType.String;
if (isString)
{
return $"TRY_CAST({field} AS NUMERIC) {op} {sqlRhs}";
return $"{path} {sqlOp} {sqlRhs}";
}
if (isBoolean)
var isNull = value.ValueType is ClrValueType.Null;
if (isNull)
{
return $"IIF({field} = 'true', 1, 0) {op} {sqlRhs}";
}
return $"{field} = {sqlRhs}";
}
if (op == CompareOperator.Equals)
{
return $"{queryPath} IS NULL AND {path} IS NULL";
}
if (value.IsList && op == CompareOperator.In)
{
return $"""
CASE WHEN LEFT(JSON_QUERY({sqlPath}), 1) = '['
THEN (
SELECT COUNT(*)
FROM OPENJSON({sqlPath}) AS field_arr
WHERE {ArrayCondition("field_arr.value", "IN")}
)
ELSE (
SELECT COUNT(*)
WHERE {ScalarCondition()}
)
END > 0
""";
}
if (op == CompareOperator.NotEquals)
{
return $"{queryPath} IS NOT NULL OR {path} IS NOT NULL";
}
}
if (op == CompareOperator.Equals)
{
return $"""
CASE WHEN LEFT(JSON_QUERY({sqlPath}), 1) = '['
THEN (
SELECT COUNT(*)
FROM OPENJSON({sqlPath}) AS field_arr
WHERE {ArrayCondition("field_arr.value", "=")}
)
ELSE (
SELECT COUNT(*)
WHERE {ScalarCondition()}
)
END > 0
""";
return base.Where(path, op, value, queryParameters, false);
}
return ScalarCondition();
return $"""
(
CASE WHEN LEFT(LTRIM(JSON_QUERY({sqlPath})), 1) = '['
THEN CASE WHEN EXISTS (
SELECT 1
FROM OPENJSON({sqlPath}) AS __element
WHERE {BuildCondition("__element.[value]", "__element.[value]")}
) THEN 1 ELSE 0 END
ELSE CASE WHEN {BuildCondition($"JSON_VALUE({sqlPath})", $"JSON_QUERY({sqlPath})")} THEN 1 ELSE 0 END
END
) = 1
""";
}
return base.Where(path, op, value, queryParameters, isJson);

2
backend/src/Squidex.Infrastructure/Queries/QueryModel.cs

@ -117,14 +117,12 @@ public sealed class QueryModel
public QueryModel Flatten(int maxDepth = 7, bool onlyWithOperators = true)
{
var predicate = (Predicate<FilterSchema>?)null;
if (onlyWithOperators)
{
predicate = x => Operators.TryGetValue(x.Type, out var operators) && operators.Count > 0;
}
var flatten = Schema.Flatten(maxDepth, predicate);
if (ReferenceEquals(flatten, Schema))
{
return this;

930
backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs

File diff suppressed because it is too large

10
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestEntity.cs

@ -41,13 +41,21 @@ public class TestJson
public long? NumberOrNull { get; set; }
public long[] NumberArray { get; set; }
public string Text { get; set; }
public string? TextOrNull { get; set; }
public string[] TextArray { get; set; }
public bool Boolean { get; set; }
public bool? BooleanOrNull { get; set; }
public bool[] BooleanArray { get; set; }
public object? Mixed { get; set; }
public long[] Array { get; set; }
public object?[] MixedArray { get; set; }
}

315
frontend/package-lock.json

File diff suppressed because it is too large

2
frontend/package.json

@ -50,7 +50,7 @@
"mousetrap": "1.6.5",
"ng2-charts": "^8.0.0",
"ngx-doc-viewer": "15.0.1",
"ngx-inline-filter": "^0.2.5",
"ngx-inline-filter": "^0.3.0",
"ngx-scrollbar": "^19.1.4",
"ngx-ui-tour-core": "16.0.0",
"oidc-client-ts": "^3.4.1",

5
frontend/src/app/shared/components/search/search-form.component.ts

@ -157,6 +157,8 @@ export class SearchFormComponent {
let args;
let component: Type<FieldComponent<any>> | undefined = undefined;
let operators = [...model.operators[field.schema.type]];
const { type, extra } = field.schema;
if (field.schema.type === 'Boolean') {
component = BooleanValue;
@ -176,6 +178,7 @@ export class SearchFormComponent {
} else if (type === 'String' && !extra) {
component = StringValue;
} else if (type === 'StringArray' && extra?.schemaIds) {
operators = ['eq'];
args = { editor: 'Reference', schemaIds: extra.schemaIds };
} else if (type === 'StringArray') {
component = StringValue;
@ -189,7 +192,7 @@ export class SearchFormComponent {
defaultValue: null,
description: field.description,
label: field.path,
operators: model.operators[field.schema.type] as any,
operators,
path: field.path,
};

Loading…
Cancel
Save