Repository navigation
Support contains, startswith and endswith in $filter and database policies - #3858
Open
Richard Cadogan (arkin-nz) wants to merge 1 commit into
Open
Richard Cadogan (arkin-nz) wants to merge 1 commit into
Richard Cadogan (arkin-nz) wants to merge 1 commit into
Conversation
…ilter Database policies and REST $filter only supported binary comparison operators. OData string functions parsed but failed at request time because ODataASTVisitor and ParameterAliasRewriter did not handle SingleValueFunctionCallNode. - SQL: translate contains/startswith/endswith to `column LIKE @param`. The search value (constant, bound claim, or concat of those) is resolved in C#, LIKE-escaped with the GraphQL filter's rules (now a shared helper) and bound as a single parameter. - Cosmos DB: map to the native CONTAINS/STARTSWITH/ENDSWITH. - ParameterAliasRewriter binds claim aliases inside function arguments and treats a function call as a predicate. - Unsupported shapes fail closed, including comparing a function's result (`startswith(...) eq true`). - MsSqlQueryBuilder adds the LIKE ESCAPE clause to insert, update, delete and upsert predicates, not only SELECT, since policies apply to those operations too. - ODataASTFieldVisitor traverses function arguments so referenced columns still go through column-level authorization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRDKmtDzv5kTiSFnUxh3ow
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Author
|
@microsoft-github-policy-service agree company="Arkin" |
Richard Cadogan (arkin-nz)
marked this pull request as ready for review
October 15, 2026 20:50
Richard Cadogan (arkin-nz)
requested review from
Alekhya-Polavarapu,
Aniruddh Munde (Aniruddh25),
RubenCerna2079,
aaronburtle,
cwebb-msft,
Mohit G (gmohit21),
mhyon,
rusamant,
Sourabh Jain (sourabh1007),
Souvik Ghosh (souvikghosh04),
Stuart Padley (stuartpa) and
vadeveka
as code owners
October 15, 2026 20:50
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
DWSQL can evaluate authorization predicates incorrectly, and rejected Cosmos policies lack authorization-error translation.
2 open findings
What changed in this PR
Adds OData string filtering to DAB’s REST filters and database policies, partially addressing #3240 and #936.
Changes:
- Translates
contains,startswith, andendswithinto parameterized SQL and Cosmos predicates. - Supports claim binding and constant
concat, while preserving column authorization. - Shares LIKE escaping with GraphQL and extends SQL Server mutation escaping.
| File | Description |
|---|---|
| src/Service.Tests/UnitTests/RelationalUpsertConcurrencyQueryBuilderTests.cs | Tests upsert policy escaping. |
| src/Service.Tests/UnitTests/ODataASTVisitorUnitTests.cs | Adds string-filter translation cases. |
| src/Service.Tests/UnitTests/DatabasePolicyClaimBindingUnitTests.cs | Tests claim binding, escaping, and rejection. |
| src/Core/Resolvers/MsSqlQueryBuilder.cs | Adds escaping to mutation predicates. |
| src/Core/Parsers/ParameterAliasRewriter.cs | Binds claims inside function arguments. |
| src/Core/Parsers/ODataStringFunctionHelper.cs | Centralizes validation, folding, and escaping. |
| src/Core/Parsers/ODataASTVisitor.cs | Generates parameterized LIKE predicates. |
| src/Core/Parsers/ODataASTFieldVisitor.cs | Traverses function arguments for authorization. |
| src/Core/Parsers/ODataASTCosmosVisitor.cs | Generates native Cosmos string predicates. |
| src/Core/Models/GraphQLFilterParsers.cs | Reuses shared LIKE escaping. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| (SingleValuePropertyAccessNode field, string value) = ODataStringFunctionHelper.GetFieldAndValue(nodeIn); | ||
| string column = field.Accept(this); | ||
| _metadataProvider.TryGetBackingColumn(_struct.EntityName, field.Property.Name, out string? backingColumnName); | ||
| string pattern = ODataStringFunctionHelper.GetLikePattern(nodeIn.Name, value); |
| /// <returns>String representing the function call.</returns> | ||
| public override string Visit(SingleValueFunctionCallNode nodeIn) | ||
| { | ||
| (SingleValuePropertyAccessNode field, string value) = ODataStringFunctionHelper.GetFieldAndValue(nodeIn); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Why make this change?
contains/startswith/endswithpart of [Enh]: Add basic string $filter functions #3240 (and the long-standing request in Add more OData expression support to $filter #936). It doesn't cover the regex,allow-regular-expressionsor MCP parts of [Enh]: Add basic string $filter functions #3240.$filterand database policies only support binary comparison operators. OData string functions parse, but fail at request time becauseODataASTVisitorandParameterAliasRewriterdon't handleSingleValueFunctionCallNode. GraphQLfilteralready supports these functions.|123456|957456|159854|), and a manager should see the rows where their own ID, a token claim, appears in that list.What is this change?
ODataASTVisitor):contains,startswithandendswithare translated tocolumn LIKE @param. The search value must resolve to a non-null string: a constant, a bound claim, or aconcatof those. It is resolved in C#, LIKE wildcards are escaped with the same rules as the GraphQL filter (the escaping is moved into a shared helper), and the whole pattern is bound as one parameter with the length override GraphQL uses for LIKE. No claim value reaches the SQL text.ODataASTCosmosVisitor): the same functions map to the nativeCONTAINS/STARTSWITH/ENDSWITH(c.col, @param).ParameterAliasRewriter): recurses into function arguments so claim aliases inside them get bound, and treats a function call as a predicate so it isn't rewritten as... eq true.tolower, field-to-field, a claim as the searched value, a null value) throw. Policy failures surface as the existing 403AuthorizationCheckFailed;$filterfailures as the existing 400 "$filterquery parameter is not well formed." Comparing a function's result (startswith(title, 'A') eq true) is also rejected rather than translated, soFindTestWithUnsupportedFilterKeywordskeeps passing unchanged.ESCAPE:MsSqlQueryBuilderonly addedESCAPE '\'to SELECT predicates. Database policies also apply to update, delete, upsert and insert, so the clause is now added there as well. Without it, an escaped wildcard in a mutation policy would not match.ODataASTFieldVisitortraverses function arguments, so columns referenced inside$filterfunctions still go through column-level authorization.Example policy for the use case above:
Out of scope, noted for follow-up:
DwSqlQueryBuildernever adds anESCAPEclause, not even for the existing GraphQLcontains. I left DWSQL alone because I couldn't test against a warehouse.inlists and other OData functions are still unsupported.How was this tested?
Integration Tests
Unit Tests
DatabasePolicyClaimBindingUnitTests(no database needed) covers the full claim → OData AST → SQL/Cosmos path:concatclaim bound as one parameter (%|123456|%for SQL,|123456|for Cosmos);and;true).These fail without the change.
RelationalUpsertConcurrencyQueryBuilderTests: the SQL Server upsert addsESCAPE '\'to LIKE in update and create policy predicates.ODataASTVisitorUnitTests(SQL Server):$filtercases forcontains,startswith,endswithandconcat.All non-database tests pass (3,264). The
TestCategory=MsSqlsuite was run against SQL Server 2025 in Docker and compared with an unmodified build at the same commit: 1,577 passed with this change, against 1,573 on unmodifiedmain. Both runs had the same 38 failures, all also failing without this change and caused by the local environment (mostlyConfigurationTestsand hot-reload tests).Manual end-to-end check against SQL Server, with the policy above and AppService auth carrying an
mgrclaim:123456reads exactly their rows;|91234567|(a superstring) and1234(a substring) don't match;1_3456doesn't match|1x3456|, so the wildcard is escaped;'returns an empty result, not an error;LIKEcarriedESCAPE '\'.PostgreSQL and MySQL were not run locally. Both use
\as the default LIKE escape character, which the existing GraphQLcontainsalready relies on.Sample Request(s)
Policy (as above), then:
returns only the rows whose
managerIdscontains|123456|.REST
$filter: