Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FlexQuery.NET.Diagnostics;
using FlexQuery.NET.Models;
using FlexQuery.NET.Models.Aggregates;
using FlexQuery.NET.Models.Filters;

namespace FlexQuery.NET.Samples.WebApi.Controllers;
Expand Down Expand Up @@ -77,10 +78,7 @@ public static object BuildRichDiagnostics(
}).ToList(),
having = options.Having is not null ? new
{
function = options.Having.Function,
field = options.Having.Field,
op = options.Having.Operator,
value = options.Having.Value
expression = SerializeHaving(options.Having)
} : null,
paging = new
{
Expand Down Expand Up @@ -329,4 +327,31 @@ public static object BuildManualShape(
} : null,
timeline = (object[]?)null
};

private static object? SerializeHaving(HavingNode having)
{
return having switch
{
HavingConditionNode c => new
{
type = "condition",
function = c.Function.ToString(),
field = c.Field,
@operator = c.Operator,
value = c.Value
},
HavingLogicalNode l => new
{
type = "logical",
logic = l.Logic,
children = l.Children.Select(SerializeHaving).ToList()
},
HavingGroupNode g => new
{
type = "group",
inner = SerializeHaving(g.Inner)
},
_ => null
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static QueryOptions Parse(AgGridRequest request)

var fn = AggregateFunctionConverter.Parse(col.AggFunc);
var alias = ParserUtilities.BuildAggregateAlias(fn.ToString().ToLowerInvariant(), col.Field);
result.Aggregates.Add(new AggregateModel
result.Aggregates.Add(new Aggregate
{
Field = col.Field,
Function = fn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ private static void AddAggregate(QueryOptions result, string? field, string? agg
var fn = NormalizeAggregateFunction(aggregateFunction);
var alias = BuildAggregateAlias(fn, field);

result.Aggregates.Add(new AggregateModel
result.Aggregates.Add(new Aggregate
{
Field = field,
Function = fn,
Expand Down
60 changes: 47 additions & 13 deletions src/FlexQuery.NET.Dapper/SQL/Builders/SqlHavingBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using FlexQuery.NET.Dapper.Dialects;
using FlexQuery.NET.Dapper.Mapping;
using FlexQuery.NET.Dapper.Sql.Converters;
Expand All @@ -14,7 +15,7 @@ internal static class SqlHavingBuilder
{
public static string Build(
ISqlDialect dialect,
HavingCondition? having,
HavingNode? having,
IEntityMapping mapping,
SqlParameterContext parameters)
{
Expand All @@ -23,6 +24,47 @@ public static string Build(
return string.Empty;
}

var sb = new StringBuilder();
sb.Append("HAVING ");
AppendExpression(sb, having, dialect, mapping, parameters);
return sb.ToString();
}

private static void AppendExpression(StringBuilder sb, HavingNode node, ISqlDialect dialect, IEntityMapping mapping, SqlParameterContext parameters)
{
switch (node)
{
case HavingConditionNode c:
sb.Append(BuildCondition(c, dialect, mapping, parameters));
break;
case HavingLogicalNode l:
{
var parts = new List<string>();
foreach (var child in l.Children)
{
var childSb = new StringBuilder();
AppendExpression(childSb, child, dialect, mapping, parameters);
parts.Add(childSb.ToString());
}

var op = l.Logic.ToKeyword();
sb.Append('(');
sb.Append(string.Join($" {op} ", parts));
sb.Append(')');
break;
}
case HavingGroupNode g:
{
sb.Append('(');
AppendExpression(sb, g.Inner, dialect, mapping, parameters);
sb.Append(')');
break;
}
}
}

private static string BuildCondition(HavingConditionNode having, ISqlDialect dialect, IEntityMapping mapping, SqlParameterContext parameters)
{
var isCountStar =
having.Function == AggregateFunction.Count &&
string.IsNullOrWhiteSpace(having.Field);
Expand All @@ -44,22 +86,14 @@ public static string Build(
$"{having.Function.ToKeyword().ToUpperInvariant()}({column})";
}

var value = ConvertValue(
dialect,
having,
mapping);

var value = ConvertValue(dialect, having, mapping);
var parameterName = parameters.Add(value);

var sqlOperator = NormalizeOperator(having.Operator);

return $"HAVING {aggregateExpression} {sqlOperator} {parameterName}";
return $"{aggregateExpression} {sqlOperator} {parameterName}";
}

private static object? ConvertValue(
ISqlDialect dialect,
HavingCondition having,
IEntityMapping mapping)
private static object? ConvertValue(ISqlDialect dialect, HavingConditionNode having, IEntityMapping mapping)
{
var value = having.Value?.Trim('"');

Expand Down Expand Up @@ -122,4 +156,4 @@ private static string NormalizeOperator(string op)
_ => op
};
}
}
}
8 changes: 4 additions & 4 deletions src/FlexQuery.NET.OpenApi/Documentation/ExampleProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ internal static class ExampleProvider
],
Aggregate =
[
new AggregateModel { Function = AggregateFunction.Sum, Field = "TotalAmount", Alias = "TotalRevenue" },
new AggregateModel { Function = AggregateFunction.Count, Alias = "OrderCount" },
new AggregateModel { Function = AggregateFunction.Avg, Field = "Rating", Alias = "AvgRating" }
new Aggregate { Function = AggregateFunction.Sum, Field = "TotalAmount", Alias = "TotalRevenue" },
new Aggregate { Function = AggregateFunction.Count, Alias = "OrderCount" },
new Aggregate { Function = AggregateFunction.Avg, Field = "Rating", Alias = "AvgRating" }
],
GroupBy = ["Region", "Category"],
Having = new HavingCondition
Having = new HavingConditionNode
{
Function = AggregateFunction.Sum,
Field = "TotalAmount",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
[typeof(FilterCondition)] = new(SchemaDocumentation.FilterCondition, null),
[typeof(SortNode)] = new(SchemaDocumentation.SortNode, null),
[typeof(PagingOptions)] = new(SchemaDocumentation.PagingOptions, null),
[typeof(AggregateModel)] = new(SchemaDocumentation.AggregateModel, null),
[typeof(HavingCondition)] = new(SchemaDocumentation.HavingCondition, null),
[typeof(Aggregate)] = new(SchemaDocumentation.Aggregate, null),
[typeof(HavingNode)] = new(SchemaDocumentation.HavingNode, null),
[typeof(IncludeNode)] = new(SchemaDocumentation.IncludeNode, null),
[typeof(ProjectionMode)] = new(SchemaDocumentation.ProjectionMode, null),
[typeof(LogicOperator)] = new(SchemaDocumentation.LogicOperator, null),
Expand All @@ -39,7 +39,7 @@
return true;
}

documentation = default;

Check warning on line 42 in src/FlexQuery.NET.OpenApi/Documentation/FlexQueryDocumentationRegistry.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Cannot convert null literal to non-nullable reference type.
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ internal static class SchemaDocumentation
internal static readonly string PagingOptions =
"Pagination parameters: page number, page size, and disable option.";

internal static readonly string AggregateModel =
internal static readonly string Aggregate =
"An aggregate projection expression with function, field, and alias.";

internal static readonly string HavingCondition =
internal static readonly string HavingConditionNode =
"A HAVING condition against an aggregate projection.";

internal static readonly string HavingNode =
"A HAVING expression tree supporting logical combinations of aggregate conditions.";

internal static readonly string IncludeNode =
"A filtered navigation include path with optional child expansions.";

Expand Down
15 changes: 15 additions & 0 deletions src/FlexQuery.NET.Parsers.Fql/Ast/FqlHavingConditionNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FlexQuery.NET.Models.Aggregates;
using FlexQuery.NET.Parsers;

namespace FlexQuery.NET.Parsers.Fql;

internal sealed class FqlHavingConditionNode(AggregateFunction function, string field, string @operator, string value)
: FqlAstNode
{
public AggregateFunction Function { get; } = function;
public string Field { get; } = field;
public string Operator { get; } = @operator;
public string Value { get; } = value;

public override string ToString() => $"{Function.ToKeyword().ToUpperInvariant()}({Field}) {Operator} {Value}";
}
8 changes: 8 additions & 0 deletions src/FlexQuery.NET.Parsers.Fql/Ast/FqlHavingGroupNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FlexQuery.NET.Parsers.Fql;

internal sealed class FqlHavingGroupNode(FqlAstNode inner) : FqlAstNode
{
public FqlAstNode Inner { get; } = inner;

public override string ToString() => $"({Inner})";
}
10 changes: 10 additions & 0 deletions src/FlexQuery.NET.Parsers.Fql/Ast/FqlHavingLogicalNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace FlexQuery.NET.Parsers.Fql;
using FlexQuery.NET.Models.Filters;

internal sealed class FqlHavingLogicalNode(LogicOperator logic, IReadOnlyList<FqlAstNode> children) : FqlAstNode
{
public LogicOperator Logic { get; } = logic;
public IReadOnlyList<FqlAstNode> Children { get; } = children;

public override string ToString() => $"{Logic.ToKeyword()}({string.Join(", ", Children)})";
}
13 changes: 10 additions & 3 deletions src/FlexQuery.NET.Parsers.Fql/Parsing/FqlAggregateParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ namespace FlexQuery.NET.Parsers.Fql;

internal static class FqlAggregateParser
{
public static List<AggregateModel> Parse(string? rawSelect)
public static List<Aggregate> Parse(string? rawSelect)
{
if (string.IsNullOrWhiteSpace(rawSelect))
return [];

var result = new List<AggregateModel>();
var result = new List<Aggregate>();
var segments = SplitTopLevel(rawSelect);

foreach (var segment in segments)
Expand Down Expand Up @@ -66,6 +66,13 @@ public static List<AggregateModel> Parse(string? rawSelect)
$"COUNT(*) is not supported. Use COUNT(<collection>) or another aggregate over a property instead.");
}

if (function == AggregateFunction.Count && fieldRaw == "*")
{
throw new FqlParseException(
$"Unable to parse aggregate expression '{rawSelect}'. " +
$"COUNT(*) is not supported. Use COUNT(<collection>) or another aggregate over a property instead.");
}

if (fieldRaw != "*" && !ParserUtilities.IsValidPropertyPath(fieldRaw.AsSpan()))
throw new FqlParseException(
$"Invalid field '{fieldRaw}' in aggregate expression '{rawSelect}'. " +
Expand Down Expand Up @@ -127,7 +134,7 @@ public static List<AggregateModel> Parse(string? rawSelect)
"Aliases must be valid identifiers (e.g. 'TotalSales').");
}

result.Add(new AggregateModel
result.Add(new Aggregate
{
Function = function,
Field = field,
Expand Down
Loading
Loading