diff --git a/docs/querying/filtering-query-results/conditional-filtering.mdx b/docs/querying/filtering-query-results/conditional-filtering.mdx new file mode 100644 index 0000000000..7a170d169b --- /dev/null +++ b/docs/querying/filtering-query-results/conditional-filtering.mdx @@ -0,0 +1,27 @@ +--- +title: "Conditional Filtering" +sidebar_label: "Conditional Filtering" +description: "Learn how to apply a query filter only when a condition holds, using the RQL when(condition, filter) operator, including its behavior, composition with and/or/not, parameter and null handling, type safety, and in / all in support." +sidebar_position: 6 +supported_languages: ["csharp"] +see_also: + - title: "Query Overview" + link: "querying/overview" + source: "docs" + path: "Querying" + - title: "Filter by Date and Time" + link: "querying/filtering-query-results/filter-by-date-and-time" + source: "docs" + path: "Querying > Filtering Query Results" +--- + +import LanguageSwitcher from "@site/src/components/LanguageSwitcher"; +import LanguageContent from "@site/src/components/LanguageContent"; + +import ConditionalFilteringCsharp from './content/_conditional-filtering-csharp.mdx'; + + + + + + diff --git a/docs/querying/filtering-query-results/content/_conditional-filtering-csharp.mdx b/docs/querying/filtering-query-results/content/_conditional-filtering-csharp.mdx new file mode 100644 index 0000000000..50a6e5eed4 --- /dev/null +++ b/docs/querying/filtering-query-results/content/_conditional-filtering-csharp.mdx @@ -0,0 +1,456 @@ +import Admonition from '@theme/Admonition'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeBlock from '@theme/CodeBlock'; +import ContentFrame from '@site/src/components/ContentFrame'; +import Panel from '@site/src/components/Panel'; + + + +* Conditional filtering lets a single RQL query handle different combinations of search criteria based on query parameter values, + instead of composing different query strings on the client. + +* Query parameters are values passed separately from the query text and referenced in RQL with `$`, + such as `$title` or `$country`. + +* This feature is available only in **RQL**. It is not exposed through LINQ queries or DocumentQuery. + +* The RQL `when(condition, predicate)` function applies a query predicate only when `condition` is true. + * **If `condition` evaluates to `true`** - + the predicate is applied. + * **If `condition` evaluates to `false`** - + the `when(...)` clause is removed from the query expression and the predicate is not applied. + +* The `condition` is evaluated **server-side** before documents are matched. + It is not evaluated per document and cannot inspect document fields. + The second argument, `predicate`, is a regular query predicate that is matched against documents. + +* In this article: + * [When to use conditional filtering](../../../querying/filtering-query-results/conditional-filtering.mdx#when-to-use-conditional-filtering) + * [Basic example](../../../querying/filtering-query-results/conditional-filtering.mdx#basic-example) + * [Combine `when` with `and`, `or`, and `not`](../../../querying/filtering-query-results/conditional-filtering.mdx#combine-when-with-and-or-and-not) + * [Parameter and null handling](../../../querying/filtering-query-results/conditional-filtering.mdx#parameter-and-null-handling) + * [Type handling](../../../querying/filtering-query-results/conditional-filtering.mdx#type-handling) + * [Logical expressions in the condition](../../../querying/filtering-query-results/conditional-filtering.mdx#logical-expressions-in-the-condition) + * [Using `in` and `all in` in the condition](../../../querying/filtering-query-results/conditional-filtering.mdx#using-in-and-all-in-in-the-condition) + * [Restrictions](../../../querying/filtering-query-results/conditional-filtering.mdx#restrictions) + * [Syntax](../../../querying/filtering-query-results/conditional-filtering.mdx#syntax) + + + + + +* Use conditional filtering when one query needs to support several **optional** search criteria, + such as title, country, status, or date range. + +* **Without `when`**, the client often builds the RQL dynamically, adding predicates only for parameters that were supplied. + Doing so creates many query-text variations and makes the query harder to read, test, and maintain. + +* **With `when`**, the client can pass all optional values as query parameters and keep one stable RQL query. + Each optional predicate is applied only when its corresponding condition is `true`. + +* Conditional filtering is useful when missing or `null` parameters should simply skip the specified predicate, + rather than require a different query string. + + + + + +* The following query searches employees and conditionally applies two predicates: + one for _title_ and one for _country_. + +* The values are passed as query parameters. + In RQL, `$title` uses the value passed by `.AddParameter("title", title)`, + and `$country` uses the value passed by `.AddParameter("country", country)`. + +* The query text stays the same for every request. Only the parameter values change. + + + + ```csharp + string? title = "Sales Representative"; // Use null to skip the title predicate. + string? country = "USA"; // Use null to skip the country predicate. + + List employees = session.Advanced + .RawQuery(@" + from Employees + where when($title != null, Title == $title) + and when($country != null, Address.Country == $country)") + .AddParameter("title", title) + .AddParameter("country", country) + .ToList(); + ``` + + + ```csharp + string? title = "Sales Representative"; // Use null to skip the title predicate. + string? country = "USA"; // Use null to skip the country predicate. + + List employees = await asyncSession.Advanced + .AsyncRawQuery(@" + from Employees + where when($title != null, Title == $title) + and when($country != null, Address.Country == $country)") + .AddParameter("title", title) + .AddParameter("country", country) + .ToListAsync(); + ``` + + + ```sql + from Employees + where when($title != null, Title == $title) + and when($country != null, Address.Country == $country) + + { "title": "Sales Representative", "country": "USA" } + ``` + + + +* **When both parameters are non-null**, both predicates are applied and the query behaves like: + + ```sql + from Employees + where Title == $title and Address.Country == $country + ``` + +* **When only `$country` is non-null**, the _title_ predicate is skipped and the query behaves like: + + ```sql + from Employees + where Address.Country == $country + ``` + +* **When only `$title` is non-null**, the _country_ predicate is skipped and the query behaves like: + + ```sql + from Employees + where Title == $title + ``` + +* **When both parameters are `null` or missing**, + both conditional predicates are skipped and the query returns all employees, i.e.: + + ```sql + from Employees + ``` + + + + + +* A `when` clause can be combined with standard `and`, `or`, and `not` operators. + +* **When the condition is `true`** - + the predicate inside `when` participates in the surrounding boolean expression. + + **When the condition is `false`** - + that predicate is skipped. + +* The following table summarizes the resulting behavior: + + | Expression | When condition is true | When condition is false | + |--------------------------------------------------|---------------------------------|---------------------------| + | `when(condition, Predicate1) and Predicate2` | `Predicate1 and Predicate2` | `Predicate2` | + | `when(condition, Predicate1) or Predicate2` | `Predicate1 or Predicate2` | `Predicate2` | + | `when(condition, Predicate1) and not Predicate2` | `Predicate1 and not Predicate2` | `not Predicate2` | + | `Predicate2 and not when(condition, Predicate1)` | `Predicate2 and not Predicate1` | `Predicate2` | + +* **If every predicate in the `where` clause is skipped** - + the query behaves as if it has no `where` clause and returns all documents from the queried collection or index. + +* **Example** - combine optional search criteria with a required predicate using `and`: + + + + ```csharp + List products = session.Advanced + .RawQuery(@" + from Products + where when($onlyInStock == true, UnitsInStock > 0) + and Discontinued == false") + .AddParameter("onlyInStock", true) + .ToList(); + ``` + + + ```csharp + List products = await asyncSession.Advanced + .AsyncRawQuery(@" + from Products + where when($onlyInStock == true, UnitsInStock > 0) + and Discontinued == false") + .AddParameter("onlyInStock", true) + .ToListAsync(); + ``` + + + ```sql + from Products + where when($onlyInStock == true, UnitsInStock > 0) + and Discontinued == false + + { "onlyInStock": true } + ``` + + + + * When `$onlyInStock` is `true`: + the `UnitsInStock > 0` predicate is applied, so the query returns non-discontinued products that are in stock. + + * When `$onlyInStock` is `false`: + the `UnitsInStock > 0` predicate is skipped, so the query returns all non-discontinued products. + + + + + +* The condition compares **query parameters** (e.g. `$title`, `$country`) against literal values. + The left-hand side of each comparison must be a parameter reference (a `$`-prefixed name). + +* **Missing parameters are treated as `null`.** + A parameter that was never supplied behaves exactly like a parameter explicitly set to `null`. + +* You can test for parameters that are `null` or missing with `== null` and `!= null`: + + ```sql + // Apply the Title predicate only when $title is non-null: + from Employees + where when($title != null, Title == $title) + + // Apply a "no title set" predicate only when $title is null or missing: + from Employees + where when($title == null, Title == null) + ``` + +* When a parameter is `null` (or missing) and it is compared against a **non-null** value, + only `!=` evaluates to `true`; all other operators evaluate to `false`. + For example, if `$p` is missing: `$p != 5` is `true`, while `$p == 5`, `$p > 5`, etc. are all `false`. + + + + + +* Comparisons are evaluated based on the type of the literal value being compared against, + and the parameter value is converted to match when a sensible conversion exists. + When no such conversion exists, the query fails with `RavenException`. + +* **Comparing against a number literal**: + When comparing against a number literal, RavenDB converts between `Long` and `Double` + (1 and 1.0 compare as equal), and all operators (`==`, `!=`, `<`, `<=`, `>`, `>=`) are supported. + A string parameter that holds a numeric value (e.g. "10") is parsed and compared as a number. + A parameter that cannot be converted to a number (a non-numeric string or a boolean) throws `RavenException`. + + ```sql + // Valid - RavenDB converts between Long and Double: + where when($longParam >= 10, ...) + + // Valid - a numeric string parameter is compared as a number: + where when($numericStringParam == 10, ...) + ``` + +* **Comparing against a string literal**: + When comparing against a string literal, the parameter must be a string. + A number or boolean parameter compared against a string literal throws `RavenException`. + String value comparisons are case-insensitive (ordinal, ignore-case). + + ```sql + // Valid - strings compare case-insensitively: + where when($stringParam == 'active', ...) + + // Throws RavenException - + // string literal requires comparing against a string param + where when($numberParam == 'ten', ...) + ``` + +* **Comparing against a boolean literal**: + When comparing against a boolean literal, only the equality operators `==` and `!=` are supported. + Using an inequality operator (`<`, `<=`, `>`, `>=`) throws `RavenException`. + The parameter must also be a boolean; a non-boolean parameter throws `RavenException`. + + ```sql + // Valid - booleans with == / !=: + where when($booleanParam == false, ...) + + // Throws RavenException - inequality on a boolean: + where when($booleanParam > false, ...) + ``` + +* **Comparing against `null`**: + A parameter is `null` if it is explicitly `null` or not supplied. + + When comparing against a `null` literal, only `==` and `!=` are supported (other operators return `false`): + `$param == null` matches a null/missing parameter; + `$param != null` matches any non-null value. + + When comparing against a **non-null** literal, a null/missing parameter makes `!=` return `true` + and every other operator (including `==`) return `false` - it never raises a type error. + + + + + +* The condition can combine several parameter checks with `and`, `or`, `not`, and parentheses. + Use parentheses to make the intended grouping explicit. + + + + ```csharp + List employees = session.Advanced + .RawQuery(@" + from Employees + where when(($title == 'Sales Representative' or $title == 'Sales Manager') + and not ($includeAllTitles == true), + Title == $title)") + .AddParameter("title", "Sales Representative") + .AddParameter("includeAllTitles", false) + .ToList(); + ``` + + + ```csharp + List employees = await asyncSession.Advanced + .AsyncRawQuery(@" + from Employees + where when(($title == 'Sales Representative' or $title == 'Sales Manager') + and not ($includeAllTitles == true), + Title == $title)") + .AddParameter("title", "Sales Representative") + .AddParameter("includeAllTitles", false) + .ToListAsync(); + ``` + + + ```sql + from Employees + where when(($title == 'Sales Representative' or $title == 'Sales Manager') + and not ($includeAllTitles == true), + Title == $title) + + { "title": "Sales Representative", "includeAllTitles": false } + ``` + + + + * When `$title` is `"Sales Representative"` OR `"Sales Manager"` AND `$includeAllTitles` is `false`: + the `Title == $title` predicate is **applied**. + + * When `$includeAllTitles` is `true`, OR `$title` has another value: + the `Title == $title` predicate is **skipped**. + + + + + +* The condition can also use `in` and `all in` to test a parameter against a set of values. + +* **`in`** - + the condition is `true` when the parameter value matches **any** of the listed values. + When the parameter is an array, the condition is `true` when the array shares **at least one** value with the list. + +* **`all in`** - + when the parameter is an array, the condition is `true` only when **every** value + in the parameter array is contained in the listed values. + +* **Example** - apply an in-stock predicate only when the requested categories include `"Beverages"` or `"Condiments"`: + + + + ```csharp + List products = session.Advanced + .RawQuery(@" + from Products + where when($categories in ('Beverages', 'Condiments'), + UnitsInStock > 0)") + .AddParameter("categories", new[] { "Beverages", "Seafood" }) + .ToList(); + ``` + + + ```csharp + List products = await asyncSession.Advanced + .AsyncRawQuery(@" + from Products + where when($categories in ('Beverages', 'Condiments'), + UnitsInStock > 0)") + .AddParameter("categories", new[] { "Beverages", "Seafood" }) + .ToListAsync(); + ``` + + + ```sql + from Products + where when($categories in ('Beverages', 'Condiments'), + UnitsInStock > 0) + + { "categories": ["Beverages", "Seafood"] } + ``` + + + + * When `$categories` contains `"Beverages"` or `"Condiments"`: + the `UnitsInStock > 0` predicate is **applied**. + + * Otherwise: + the `UnitsInStock > 0` predicate is **skipped**, and all products are returned. + +* As with scalar string comparisons, string value matching in `in` and `all in` conditions is **case-insensitive**. + +* `in` and `all in` compare formatted string values, so some mixed parameter and list value types can match. + For example, a parameter value of `"1"` can match `in (1, 2, 3)`, + and a parameter value of `1` can match `in ('1', '2', '3')`. + + The comparison is textual: each value is formatted as a string before comparison. + Values that are numerically equal but have different string representations, + such as the string `"1.0"` and the number `1` (formatted as `"1"`), are not considered a match. + + + + + +* **A `when` condition cannot contain another `when`** + The condition (the first argument) of `when(condition, predicate)` cannot include another `when(...)` call. + Attempting to use `when` inside the condition, for example, `when(when($b == 2, Number == 2), Number == 1)`, raises a parse error. + +* **The condition operates on parameters, not document fields** + The left-hand side of each comparison in the condition must be a query parameter (a `$`-prefixed name). + The condition is evaluated once, at query-planning time, against the supplied parameters - it does not inspect document data. + Document matching is done by the second argument, the predicate. + +* **AI agent parameter names** + When the query is issued by an [AI agent](../../../ai-integration/ai-agents/overview.mdx), + parameter names in the condition are **case-sensitive**. + This applies to parameter names only; string comparisons remain case-insensitive. + + + + + + + +```sql +when(, ) +``` + + + +| Parameter | Description | +|-----------------|-------------| +| **condition** | A boolean expression evaluated server-side against **query parameters**.
Each comparison has the form `$param value`.
Supports `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `all in`, combined with `and`, `or`, `not`, and parentheses. | +| **predicate** | A regular query predicate, matched against the **documents**.
Applied when `condition` is `true`; skipped when `condition` is `false`. | + +--- + +* The function name is **case-insensitive** in RQL: + `when(...)` and `WHEN(...)` are equivalent. + +* **Operator support per type**: + + | Type | Supported operators | Notes | + |---------|--------------------------------------------------|-------| + | Number | `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `all in` | RavenDB converts between `Long` and `Double` (`1 == 1.0`). | + | String | `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `all in` | String value matching is **case-insensitive**
(ordinal, ignore-case). | + | Boolean | `==`, `!=` | Using inequality operators throws a `RavenException`. | + | Null | `== null`, `!= null` | A missing parameter is treated as `null`. | + +