Skip to content

Commit cffedaa

Browse files
Address PR feedback: Add null checks, more special character tests, and improve documentation
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
1 parent 029d51b commit cffedaa

11 files changed

Lines changed: 197 additions & 10 deletions

src/Core/Parsers/RequestParser.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,28 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
115115
case FILTER_URL:
116116
// Use raw (URL-encoded) filter value to preserve special characters like &
117117
string? rawFilterValue = ExtractRawQueryParameter(context.RawQueryString, FILTER_URL);
118-
if (rawFilterValue is not null)
119-
context.FilterClauseInUrl = sqlMetadataProvider.GetODataParser().GetFilterClause($"?{FILTER_URL}={rawFilterValue}", $"{context.EntityName}.{context.DatabaseObject.FullName}");
118+
// If key exists in ParsedQueryString but not in RawQueryString, something is wrong
119+
if (rawFilterValue is null)
120+
{
121+
throw new DataApiBuilderException(
122+
message: $"Unable to extract {FILTER_URL} parameter from query string.",
123+
statusCode: HttpStatusCode.BadRequest,
124+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
125+
}
126+
context.FilterClauseInUrl = sqlMetadataProvider.GetODataParser().GetFilterClause($"?{FILTER_URL}={rawFilterValue}", $"{context.EntityName}.{context.DatabaseObject.FullName}");
120127
break;
121128
case SORT_URL:
122129
// Use raw (URL-encoded) orderby value to preserve special characters
123130
string? rawSortValue = ExtractRawQueryParameter(context.RawQueryString, SORT_URL);
124-
if (rawSortValue is not null)
125-
(context.OrderByClauseInUrl, context.OrderByClauseOfBackingColumns) = GenerateOrderByLists(context, sqlMetadataProvider, $"?{SORT_URL}={rawSortValue}");
131+
// If key exists in ParsedQueryString but not in RawQueryString, something is wrong
132+
if (rawSortValue is null)
133+
{
134+
throw new DataApiBuilderException(
135+
message: $"Unable to extract {SORT_URL} parameter from query string.",
136+
statusCode: HttpStatusCode.BadRequest,
137+
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
138+
}
139+
(context.OrderByClauseInUrl, context.OrderByClauseOfBackingColumns) = GenerateOrderByLists(context, sqlMetadataProvider, $"?{SORT_URL}={rawSortValue}");
126140
break;
127141
case AFTER_URL:
128142
context.After = context.ParsedQueryString[key];

src/Service.Tests/DatabaseSchema-DwSql.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,10 @@ VALUES (1, 'Awesome book', 1234),
338338
(19, 'ME\YOU', 1234),
339339
(20, 'C:\\LIFE', 1234),
340340
(21, '', 1234),
341-
(22, 'filter & test', 1234);
341+
(22, 'filter & test', 1234),
342+
(23, 'A+B=C', 1234),
343+
(24, 'Tom & Jerry', 1234),
344+
(25, '100% Complete', 1234);
342345

343346
INSERT INTO book_website_placements(id, book_id, price) VALUES (1, 1, 100), (2, 2, 50), (3, 3, 23), (4, 5, 33);
344347

src/Service.Tests/DatabaseSchema-MsSql.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,10 @@ VALUES (1, 'Awesome book', 1234),
533533
(19, 'ME\YOU', 1234),
534534
(20, 'C:\\LIFE', 1234),
535535
(21, '', 1234),
536-
(22, 'filter & test', 1234);
536+
(22, 'filter & test', 1234),
537+
(23, 'A+B=C', 1234),
538+
(24, 'Tom & Jerry', 1234),
539+
(25, '100% Complete', 1234);
537540
SET IDENTITY_INSERT books OFF
538541

539542
SET IDENTITY_INSERT books_mm ON

src/Service.Tests/DatabaseSchema-MySql.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,10 @@ INSERT INTO books(id, title, publisher_id)
390390
(19, 'ME\\YOU', 1234),
391391
(20, 'C:\\\\LIFE', 1234),
392392
(21, '', 1234),
393-
(22, 'filter & test', 1234);
393+
(22, 'filter & test', 1234),
394+
(23, 'A+B=C', 1234),
395+
(24, 'Tom & Jerry', 1234),
396+
(25, '100% Complete', 1234);
394397
INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33);
395398
INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null');
396399
INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126);

src/Service.Tests/DatabaseSchema-PostgreSql.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,10 @@ INSERT INTO books(id, title, publisher_id)
393393
(19, 'ME\YOU', 1234),
394394
(20, 'C:\\LIFE', 1234),
395395
(21, '', 1234),
396-
(22, 'filter & test', 1234);
396+
(22, 'filter & test', 1234),
397+
(23, 'A+B=C', 1234),
398+
(24, 'Tom & Jerry', 1234),
399+
(25, '100% Complete', 1234);
397400
INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33);
398401
INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null');
399402
INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126);;

src/Service.Tests/SqlTests/RestApiTests/Find/DwSqlFindApiTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,24 @@ public class DwSqlFindApiTests : FindApiTestBase
227227
$"WHERE title = 'filter & test' " +
228228
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
229229
},
230+
{
231+
"FindTestWithFilterContainingMultipleSpecialCharacters",
232+
$"SELECT * FROM { _integrationTableName } " +
233+
$"WHERE title = 'A+B=C' " +
234+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
235+
},
236+
{
237+
"FindTestWithFilterContainingAmpersandInPhrase",
238+
$"SELECT * FROM { _integrationTableName } " +
239+
$"WHERE title = 'Tom & Jerry' " +
240+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
241+
},
242+
{
243+
"FindTestWithFilterContainingPercentSign",
244+
$"SELECT * FROM { _integrationTableName } " +
245+
$"WHERE title = '100% Complete' " +
246+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
247+
},
230248
{
231249
"FindTestWithPrimaryKeyContainingForeignKey",
232250
$"SELECT [id], [content] FROM reviews " +

src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,53 @@ await SetupAndRunRestApiTest(
709709
);
710710
}
711711

712+
/// <summary>
713+
/// Tests the REST Api for Find operation with filters containing various special characters
714+
/// that need URL encoding: plus (+), equals (=), and percent (%).
715+
/// Validates that multiple types of special characters are handled correctly.
716+
/// </summary>
717+
[TestMethod]
718+
public async Task FindTestWithFilterContainingMultipleSpecialCharacters()
719+
{
720+
// Test with plus and equals signs
721+
await SetupAndRunRestApiTest(
722+
primaryKeyRoute: string.Empty,
723+
queryString: "?$filter=title eq 'A+B=C'",
724+
entityNameOrPath: _integrationEntityName,
725+
sqlQuery: GetQuery(nameof(FindTestWithFilterContainingMultipleSpecialCharacters))
726+
);
727+
}
728+
729+
/// <summary>
730+
/// Tests the REST Api for Find operation with a filter containing ampersand
731+
/// in a different context to ensure robustness across various data patterns.
732+
/// </summary>
733+
[TestMethod]
734+
public async Task FindTestWithFilterContainingAmpersandInPhrase()
735+
{
736+
await SetupAndRunRestApiTest(
737+
primaryKeyRoute: string.Empty,
738+
queryString: "?$filter=title eq 'Tom & Jerry'",
739+
entityNameOrPath: _integrationEntityName,
740+
sqlQuery: GetQuery(nameof(FindTestWithFilterContainingAmpersandInPhrase))
741+
);
742+
}
743+
744+
/// <summary>
745+
/// Tests the REST Api for Find operation with a filter containing percent sign (%)
746+
/// which has special meaning in URL encoding and SQL LIKE patterns.
747+
/// </summary>
748+
[TestMethod]
749+
public async Task FindTestWithFilterContainingPercentSign()
750+
{
751+
await SetupAndRunRestApiTest(
752+
primaryKeyRoute: string.Empty,
753+
queryString: "?$filter=title eq '100% Complete'",
754+
entityNameOrPath: _integrationEntityName,
755+
sqlQuery: GetQuery(nameof(FindTestWithFilterContainingPercentSign))
756+
);
757+
}
758+
712759
/// <summary>
713760
/// Tests the REST Api for Find operation where we compare one field
714761
/// to the bool returned from another comparison.

src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,24 @@ public class MsSqlFindApiTests : FindApiTestBase
234234
$"WHERE title = 'filter & test' " +
235235
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
236236
},
237+
{
238+
"FindTestWithFilterContainingMultipleSpecialCharacters",
239+
$"SELECT * FROM { _integrationTableName } " +
240+
$"WHERE title = 'A+B=C' " +
241+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
242+
},
243+
{
244+
"FindTestWithFilterContainingAmpersandInPhrase",
245+
$"SELECT * FROM { _integrationTableName } " +
246+
$"WHERE title = 'Tom & Jerry' " +
247+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
248+
},
249+
{
250+
"FindTestWithFilterContainingPercentSign",
251+
$"SELECT * FROM { _integrationTableName } " +
252+
$"WHERE title = '100% Complete' " +
253+
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
254+
},
237255
{
238256
"FindTestWithPrimaryKeyContainingForeignKey",
239257
$"SELECT [id], [content] FROM reviews " +

src/Service.Tests/SqlTests/RestApiTests/Find/MySqlFindApiTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,42 @@ ORDER BY id asc
409409
) AS subq
410410
"
411411
},
412+
{
413+
"FindTestWithFilterContainingMultipleSpecialCharacters",
414+
@"
415+
SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title, 'publisher_id', publisher_id)) AS data
416+
FROM (
417+
SELECT *
418+
FROM " + _integrationTableName + @"
419+
WHERE title = 'A+B=C'
420+
ORDER BY id asc
421+
) AS subq
422+
"
423+
},
424+
{
425+
"FindTestWithFilterContainingAmpersandInPhrase",
426+
@"
427+
SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title, 'publisher_id', publisher_id)) AS data
428+
FROM (
429+
SELECT *
430+
FROM " + _integrationTableName + @"
431+
WHERE title = 'Tom & Jerry'
432+
ORDER BY id asc
433+
) AS subq
434+
"
435+
},
436+
{
437+
"FindTestWithFilterContainingPercentSign",
438+
@"
439+
SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title, 'publisher_id', publisher_id)) AS data
440+
FROM (
441+
SELECT *
442+
FROM " + _integrationTableName + @"
443+
WHERE title = '100% Complete'
444+
ORDER BY id asc
445+
) AS subq
446+
"
447+
},
412448
{
413449
"FindTestWithFilterQueryStringBoolResultFilter",
414450
@"

src/Service.Tests/SqlTests/RestApiTests/Find/PostgreSqlFindApiTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,39 @@ SELECT json_agg(to_jsonb(subq)) AS data
422422
ORDER BY id asc
423423
) AS subq"
424424
},
425+
{
426+
"FindTestWithFilterContainingMultipleSpecialCharacters",
427+
@"
428+
SELECT json_agg(to_jsonb(subq)) AS data
429+
FROM (
430+
SELECT *
431+
FROM " + _integrationTableName + @"
432+
WHERE title = 'A+B=C'
433+
ORDER BY id asc
434+
) AS subq"
435+
},
436+
{
437+
"FindTestWithFilterContainingAmpersandInPhrase",
438+
@"
439+
SELECT json_agg(to_jsonb(subq)) AS data
440+
FROM (
441+
SELECT *
442+
FROM " + _integrationTableName + @"
443+
WHERE title = 'Tom & Jerry'
444+
ORDER BY id asc
445+
) AS subq"
446+
},
447+
{
448+
"FindTestWithFilterContainingPercentSign",
449+
@"
450+
SELECT json_agg(to_jsonb(subq)) AS data
451+
FROM (
452+
SELECT *
453+
FROM " + _integrationTableName + @"
454+
WHERE title = '100% Complete'
455+
ORDER BY id asc
456+
) AS subq"
457+
},
425458
{
426459
"FindTestWithPrimaryKeyContainingForeignKey",
427460
@"

0 commit comments

Comments
 (0)