Skip to content
Open
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
36 changes: 12 additions & 24 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/AggregateRecordsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,7 @@ private static string ApplyOrderByAndPagination(
? $"{args.Function.ToUpperInvariant()}(DISTINCT {quotedCol})"
: $"{args.Function.ToUpperInvariant()}({quotedCol})";
string orderByClause = $" ORDER BY {orderByAggExpr} {direction}";
string paginationClause = string.Empty;

if (args.First.HasValue)
{
Expand All @@ -967,37 +968,24 @@ private static string ApplyOrderByAndPagination(
string limitParam = BaseQueryStructure.GetEncodedParamName(structure.Counter.Next());
structure.Parameters.Add(limitParam, new DbConnectionParam(fetchCount));

string paginationClause = $" OFFSET {offsetParam} ROWS FETCH NEXT {limitParam} ROWS ONLY";
paginationClause = $" OFFSET {offsetParam} ROWS FETCH NEXT {limitParam} ROWS ONLY";

// Remove TOP N from the SELECT clause (TOP conflicts with OFFSET/FETCH)
sql = Regex.Replace(sql, @"SELECT TOP \d+", "SELECT");

// Insert ORDER BY + pagination before FOR JSON PATH
int jsonPathIdx = sql.IndexOf(" FOR JSON PATH", StringComparison.OrdinalIgnoreCase);
if (jsonPathIdx > 0)
{
sql = sql.Insert(jsonPathIdx, orderByClause + paginationClause);
}
else
{
sql += orderByClause + paginationClause;
}
}
else

int insertionIndex = sql.IndexOf(" FOR JSON PATH", StringComparison.OrdinalIgnoreCase);
if (insertionIndex < 0 && queryBuilder is DwSqlQueryBuilder)
{
// Without pagination: insert ORDER BY before FOR JSON PATH
int jsonPathIdx = sql.IndexOf(" FOR JSON PATH", StringComparison.OrdinalIgnoreCase);
if (jsonPathIdx > 0)
{
sql = sql.Insert(jsonPathIdx, orderByClause);
}
else
{
sql += orderByClause;
}
// STRING_AGG wraps the grouped SELECT. Order and page inside that
// derived table, where the projected aggregation alias is in scope.
insertionIndex = sql.LastIndexOf($") AS {queryBuilder.QuoteIdentifier(structure.SourceAlias)}", StringComparison.Ordinal);
orderByClause = $" ORDER BY {queryBuilder.QuoteIdentifier(ComputeAlias(args.Function, args.Field))} {direction}";
Comment on lines +982 to +983
}

return sql;
return insertionIndex > 0
? sql.Insert(insertionIndex, orderByClause + paginationClause)
: sql + orderByClause + paginationClause;
}

#endregion
Expand Down
73 changes: 73 additions & 0 deletions src/Service.Tests/Mcp/AggregateRecordsToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,79 @@ public void ApplyOrderByAndPagination_RewritesSql(
Assert.AreEqual(paginate ? 2 : 0, structure.Parameters.Count);
}

[DataTestMethod]
[DataRow("sum", false)]
[DataRow("avg", false)]
[DataRow("min", false)]
[DataRow("max", false)]
[DataRow("count", false)]
[DataRow("sum", true)]
[DataRow("avg", true)]
[DataRow("min", true)]
[DataRow("max", true)]
[DataRow("count", true)]
public void ApplyOrderByAndPagination_DwSqlOrdersInsideGroupedQuery(string function, bool useJsonFunctions)
{
foreach (bool paginate in new[] { false, true })
{
foreach (bool distinct in new[] { false, true })
{
foreach (string direction in new[] { "asc", "desc" })
{
SqlQueryStructure structure = CreateUninitializedQueryStructure();
structure.IsListQuery = true;
ViewDefinition definition = new();
definition.Columns.Add("book_category", new ColumnDefinition { SystemType = typeof(string) });
definition.Columns.Add("book_price", new ColumnDefinition { SystemType = typeof(decimal) });
DatabaseView view = new("dbo", "books") { ViewDefinition = definition };
Mock<ISqlMetadataProvider> metadata = new();
metadata.Setup(x => x.GetSourceDefinition("Book")).Returns(definition);
metadata.Setup(x => x.TryGetBackingColumn("Book", "category", out It.Ref<string?>.IsAny))
.Returns((string _, string _, out string? backing) =>
{
backing = "book_category";
return true;
});
SetBackingField(structure, "EntityName", "Book");
SetBackingField(structure, "MetadataProvider", metadata.Object);
SetBackingField(structure, "DatabaseObject", view);
SetBackingField(structure, "JoinQueries", new Dictionary<string, SqlQueryStructure>());
SetBackingField(structure, "Joins", new List<SqlJoinStructure>());
SetBackingField(structure, "Predicates", new List<Predicate>());
SetBackingField(structure, "FilterPredicates", string.Empty);
SetBackingField(structure, "DbPolicyPredicatesForOperations", new Dictionary<EntityActionOperation, string?>());
SetBackingField(structure, "PaginationMetadata", new PaginationMetadata(structure));
typeof(SqlQueryStructure).GetField("_limit", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(structure, (uint?)10);
string field = function == "count" ? "*" : "price";
AggregateRecordsTool.AggregateArguments args = CreateAggregateArguments(
function: function, field: field, isCountStar: field == "*",
distinct: distinct, orderby: direction,
first: paginate ? 9 : null, groupby: new() { "category" })
with { UserProvidedOrderby = direction == "asc" };
string alias = AggregateRecordsTool.ComputeAlias(function, field);
InvokePrivate<object?>("BuildAggregationStructure",
args, structure, view, "book_price", alias, "Book", metadata.Object);
DwSqlQueryBuilder builder = new(enableNto1JoinOpt: useJsonFunctions);

string sql = InvokePrivate<string>("ApplyOrderByAndPagination",
builder.Build(structure), args, structure, builder, "book_price");

string order = useJsonFunctions
? $"{function.ToUpperInvariant()}({(distinct ? "DISTINCT " : string.Empty)}[table0].[book_price])"
: $"[{alias}]";
string paging = paginate ? " OFFSET @param0 ROWS FETCH NEXT @param1 ROWS ONLY" : string.Empty;
string suffix = useJsonFunctions ? " FOR JSON PATH, INCLUDE_NULL_VALUES" : ") AS [table0]";
StringAssert.EndsWith(sql, $" ORDER BY {order} {direction.ToUpperInvariant()}{paging}{suffix}");
StringAssert.Contains(sql, "GROUP BY [table0].[book_category]");
StringAssert.Contains(sql, $" AS [{alias}]");
Assert.AreEqual(!paginate, sql.Contains("SELECT TOP 10"));
Assert.AreEqual(paginate ? 2 : 0, structure.Parameters.Count);
}
}
}
}

#endregion

#region Configuration Tests
Expand Down
Loading