diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
index daee3bfb1b..51bbb5a5b3 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
@@ -567,6 +567,22 @@ await SetupAndRunRestApiTest(
);
}
+ ///
+ /// Tests the REST Api for Find operation for all records.
+ /// Uses entity with mapped columns, and order by title in ascending order.
+ ///
+ [TestMethod]
+ public async Task FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc()
+ {
+ await SetupAndRunRestApiTest(
+ primaryKeyRoute: string.Empty,
+ queryString: "?$orderby=fancyName",
+ entity: _integrationMappingDifferentEntity,
+ sqlQuery: GetQuery(nameof(FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc)),
+ controller: _restController
+ );
+ }
+
///
/// Tests the REST Api for Find operation for all records
/// when there is a space in the column name.
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs
index 8e6c4e6527..ada887e31d 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs
@@ -261,6 +261,12 @@ public class MsSqlFindApiTests : FindApiTestBase
$"ORDER BY title, id " +
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
},
+ {
+ "FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
+ $"SELECT [treeId], [species] AS [fancyName], [region], [height] FROM { _integrationMappingTable } " +
+ $"ORDER BY species " +
+ $"FOR JSON PATH, INCLUDE_NULL_VALUES"
+ },
{
"FindTestWithQueryStringSpaceInNamesOrderByAsc",
$"SELECT * FROM { _integrationTableHasColumnWithSpace } " +
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/MySqlFindApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/MySqlFindApiTests.cs
index 5460b6764b..34430806fc 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Find/MySqlFindApiTests.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Find/MySqlFindApiTests.cs
@@ -490,6 +490,17 @@ SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title, 'publisher_id', publi
LIMIT 100
) AS subq"
},
+ {
+ "FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
+ @"
+ SELECT JSON_ARRAYAGG(JSON_OBJECT('treeId', treeId, 'fancyName', species, 'region', region, 'height', height)) AS data
+ FROM (
+ SELECT *
+ FROM " + _integrationMappingTable + @"
+ ORDER BY species
+ LIMIT 100
+ ) AS subq"
+ },
{
"FindTestWithQueryStringSpaceInNamesOrderByAsc",
@"
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/PostgreSqlFindApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/PostgreSqlFindApiTests.cs
index dfedd8ca59..cdbd7976c1 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Find/PostgreSqlFindApiTests.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Find/PostgreSqlFindApiTests.cs
@@ -383,6 +383,16 @@ SELECT json_agg(to_jsonb(subq)) AS data
ORDER BY title, id
) AS subq"
},
+ {
+ "FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
+ @"
+ SELECT json_agg(to_jsonb(subq)) AS data
+ FROM (
+ SELECT ""treeId"", ""species"" AS ""fancyName"", ""region"", ""height""
+ FROM " + _integrationMappingTable + @"
+ ORDER BY ""species""
+ ) AS subq"
+ },
{
"FindTestWithFirstAndSpacedColumnOrderBy",
@"
diff --git a/src/Service/Models/RestRequestContexts/RestRequestContext.cs b/src/Service/Models/RestRequestContexts/RestRequestContext.cs
index d98ba35dfc..b4de92e9b7 100644
--- a/src/Service/Models/RestRequestContexts/RestRequestContext.cs
+++ b/src/Service/Models/RestRequestContexts/RestRequestContext.cs
@@ -62,6 +62,12 @@ protected RestRequestContext(string entityName, DatabaseObject dbo)
///
public virtual List? OrderByClauseInUrl { get; set; }
+ ///
+ /// List of OrderBy Columns which represent the OrderByClause using backing columns.
+ /// Based on the operation type, this property may or may not be populated.
+ ///
+ public virtual List? OrderByClauseOfBackingColumns { get; set; }
+
///
/// Dictionary of field names and their values given in the request body.
/// Based on the operation type, this property may or may not be populated.
diff --git a/src/Service/Parsers/RequestParser.cs b/src/Service/Parsers/RequestParser.cs
index 9f53899b7d..d02ca1f6d3 100644
--- a/src/Service/Parsers/RequestParser.cs
+++ b/src/Service/Parsers/RequestParser.cs
@@ -109,7 +109,7 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
break;
case SORT_URL:
string sortQueryString = $"?{SORT_URL}={context.ParsedQueryString[key]}";
- context.OrderByClauseInUrl = GenerateOrderByList(context, sqlMetadataProvider, sortQueryString);
+ (context.OrderByClauseInUrl, context.OrderByClauseOfBackingColumns) = GenerateOrderByLists(context, sqlMetadataProvider, sortQueryString);
break;
case AFTER_URL:
context.After = context.ParsedQueryString[key];
@@ -135,9 +135,9 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
/// associated with the sort param.
/// A List
///
- private static List? GenerateOrderByList(RestRequestContext context,
- ISqlMetadataProvider sqlMetadataProvider,
- string sortQueryString)
+ private static (List?, List?) GenerateOrderByLists(RestRequestContext context,
+ ISqlMetadataProvider sqlMetadataProvider,
+ string sortQueryString)
{
string schemaName = context.DatabaseObject.SchemaName;
string tableName = context.DatabaseObject.Name;
@@ -148,7 +148,9 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
// used for performant Remove operations
HashSet remainingKeys = new(primaryKeys);
- List orderByList = new();
+ List orderByListUrl = new();
+ List orderByListBackingColumn = new();
+
// OrderBy AST is in the form of a linked list
// so we traverse by calling node.ThenBy until
// node is null
@@ -159,43 +161,49 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
// column name of null. ie: $orderby='hello world', or $orderby=null
// note: null support is not currently implemented.
QueryNode? expression = node.Expression is not null ? node.Expression :
- throw new DataApiBuilderException(message: "OrderBy property is not supported.",
- HttpStatusCode.BadRequest,
- DataApiBuilderException.SubStatusCodes.BadRequest);
+ throw new DataApiBuilderException(
+ message: "OrderBy property is not supported.",
+ HttpStatusCode.BadRequest,
+ DataApiBuilderException.SubStatusCodes.BadRequest);
- string backingColumnName;
+ string? backingColumnName;
+ string exposedName;
if (expression.Kind is QueryNodeKind.SingleValuePropertyAccess)
{
+ exposedName = ((SingleValuePropertyAccessNode)expression).Property.Name;
+ sqlMetadataProvider.TryGetBackingColumn(context.EntityName, exposedName, out backingColumnName);
// if name is in SingleValuePropertyAccess node it matches our model and we will
// always be able to get backing column successfully
- sqlMetadataProvider.TryGetBackingColumn(context.EntityName, ((SingleValuePropertyAccessNode)expression).Property.Name, out backingColumnName!);
}
else if (expression.Kind is QueryNodeKind.Constant &&
((ConstantNode)expression).Value is not null)
{
// since this comes from constant node, it was not checked against our model
// so this may return false in which case we throw for a bad request
- if (!sqlMetadataProvider.TryGetBackingColumn(context.EntityName, ((ConstantNode)expression).Value.ToString()!, out backingColumnName!))
+ exposedName = ((ConstantNode)expression).Value.ToString()!;
+ if (!sqlMetadataProvider.TryGetBackingColumn(context.EntityName, exposedName, out backingColumnName))
{
throw new DataApiBuilderException(
- message: $"Invalid orderby column requested: {((ConstantNode)expression).Value.ToString()!}.",
+ message: $"Invalid orderby column requested: {exposedName}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
else
{
- throw new DataApiBuilderException(message: "OrderBy property is not supported.",
- HttpStatusCode.BadRequest,
- DataApiBuilderException.SubStatusCodes.BadRequest);
+ throw new DataApiBuilderException(
+ message: "OrderBy property is not supported.",
+ HttpStatusCode.BadRequest,
+ DataApiBuilderException.SubStatusCodes.BadRequest);
}
// Sorting order is stored in node.Direction as OrderByDirection Enum
// We convert to an Enum of our own that matches the SQL text we want
OrderBy direction = GetDirection(node.Direction);
// Add OrderByColumn and remove any matching columns from our primary key set
- orderByList.Add(new OrderByColumn(schemaName, tableName, backingColumnName, direction: direction));
- remainingKeys.Remove(backingColumnName);
+ orderByListUrl.Add(new OrderByColumn(schemaName, tableName, exposedName, direction: direction));
+ orderByListBackingColumn.Add(new OrderByColumn(schemaName, tableName, backingColumnName!, direction: direction));
+ remainingKeys.Remove(backingColumnName!);
node = node.ThenBy;
}
@@ -206,11 +214,13 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
{
if (remainingKeys.Contains(column))
{
- orderByList.Add(new OrderByColumn(schemaName, tableName, column));
+ sqlMetadataProvider.TryGetExposedColumnName(context.EntityName, column, out string? exposedName);
+ orderByListUrl.Add(new OrderByColumn(schemaName, tableName, exposedName!));
+ orderByListBackingColumn.Add(new OrderByColumn(schemaName, tableName, column));
}
}
- return orderByList;
+ return (orderByListUrl, orderByListBackingColumn);
}
///
diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs
index d57cd9c0e4..7776d62d0b 100644
--- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs
+++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs
@@ -171,10 +171,12 @@ public SqlQueryStructure(
value: predicate.Value);
}
- // context.OrderByColumnsInUrl will lack TableAlias because it is created in RequestParser
- // which may be called for any type of operation. To avoid coupling the OrderByClauseInUrl
+ // context.OrderByClauseOfBackingColumns will lack TableAlias because it is created in RequestParser
+ // which may be called for any type of operation. To avoid coupling the OrderByClauseOfBackingColumns
// to only Find, we populate the TableAlias in this constructor where we know we have a Find operation.
- OrderByColumns = context.OrderByClauseInUrl is not null ? context.OrderByClauseInUrl : PrimaryKeyAsOrderByColumns();
+ OrderByColumns = context.OrderByClauseOfBackingColumns is not null ?
+ context.OrderByClauseOfBackingColumns : PrimaryKeyAsOrderByColumns();
+
foreach (OrderByColumn column in OrderByColumns)
{
if (string.IsNullOrEmpty(column.TableAlias))
@@ -247,6 +249,28 @@ private void AddFields(RestRequestContext context, ISqlMetadataProvider sqlMetad
}
}
+ ///
+ /// Exposes the primary key of the underlying table of the structure
+ /// as a list of OrderByColumn
+ ///
+ private List PrimaryKeyAsOrderByColumns()
+ {
+ if (_primaryKeyAsOrderByColumns is null)
+ {
+ _primaryKeyAsOrderByColumns = new();
+
+ foreach (string column in PrimaryKey())
+ {
+ _primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName,
+ tableName: DatabaseObject.Name,
+ columnName: column,
+ tableAlias: TableAlias));
+ }
+ }
+
+ return _primaryKeyAsOrderByColumns;
+ }
+
///
/// Private constructor that is used for recursive query generation,
/// for each subquery that's necessary to resolve a nested GraphQL
@@ -851,28 +875,6 @@ private List ProcessGqlOrderByArg(List orderByFi
return orderByColumnsList;
}
- ///
- /// Exposes the primary key of the underlying table of the structure
- /// as a list of OrderByColumn
- ///
- public List PrimaryKeyAsOrderByColumns()
- {
- if (_primaryKeyAsOrderByColumns == null)
- {
- _primaryKeyAsOrderByColumns = new();
-
- foreach (string column in PrimaryKey())
- {
- _primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName,
- tableName: DatabaseObject.Name,
- columnName: column,
- tableAlias: TableAlias));
- }
- }
-
- return _primaryKeyAsOrderByColumns;
- }
-
///
/// Adds a labelled column to this query's columns, where
/// the column name is all that is provided, and we add
diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs
index d9e07cc160..f17ad47eb7 100644
--- a/src/Service/Resolvers/SqlQueryEngine.cs
+++ b/src/Service/Resolvers/SqlQueryEngine.cs
@@ -172,7 +172,7 @@ private OkObjectResult FormatFindResult(JsonDocument jsonDoc, FindRequestContext
rootEnumerated = rootEnumerated.Take(rootEnumerated.Count() - 1);
string after = SqlPaginationUtil.MakeCursorFromJsonElement(
element: rootEnumerated.Last(),
- orderByColumns: context.OrderByClauseInUrl,
+ orderByColumns: context.OrderByClauseOfBackingColumns,
primaryKey: _sqlMetadataProvider.GetTableDefinition(context.EntityName).PrimaryKey,
entityName: context.EntityName,
schemaName: context.DatabaseObject.SchemaName,