diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..b7340b4 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,84 @@ +# API Reference + +The Expression DSL library has a concise API. This document highlights the most important classes and methods you will interact with. + +For a complete, detailed reference, please consult the Javadoc for the library. + +## Core Classes + +### `com.aerospike.dsl.api.DSLParser` + +This is the main interface and entry point for the library. + +* **`ParsedExpression parseExpression(ExpressionContext input)`** + * **Description**: The primary method used to parse a DSL string when no secondary indexes are provided. It returns a `ParsedExpression` object that contains the compiled result, which can be reused. +* **`ParsedExpression parseExpression(ExpressionContext input, IndexContext indexContext)`** + * **Description**: The primary method used to parse a DSL string. It returns a `ParsedExpression` object that contains the compiled result, which can be reused. + * **Parameters**: + * `input`: An `ExpressionContext` object containing the DSL string and any placeholder values. + * **Description**: The primary method used to parse a DSL string. It returns a `ParsedExpression` object that contains the compiled result, which can be reused. + * **Parameters**: + * `input`: An `ExpressionContext` object containing the DSL string and any placeholder values. + * `indexContext`: An optional `IndexContext` object containing a list of available secondary indexes for query optimization. Can be `null`. + * **Returns**: A `ParsedExpression` object representing the compiled expression tree. + +### `com.aerospike.dsl.ExpressionContext` + +This class is a container for the DSL string and any values to be substituted for placeholders. + +* **`static ExpressionContext of(String dslString)`**: Creates a context for a DSL string without placeholders. +* **`static ExpressionContext of(String dslString, PlaceholderValues values)`**: Creates a context for a DSL string that uses `?` placeholders, providing the values to be substituted. + +### `com.aerospike.dsl.ParsedExpression` + +This object represents the compiled, reusable result of a parsing operation. It is thread-safe. + +* **`ParseResult getResult()`**: Returns the final `ParseResult` for an expression that does not contain placeholders. +* **`ParseResult getResult(PlaceholderValues values)`**: Returns the final `ParseResult` by substituting the given placeholder values into the compiled expression tree. This is highly efficient as it bypasses the parsing step. + +### `com.aerospike.dsl.ParseResult` + +This class holds the final, concrete outputs of the parsing and substitution process. + +* **`Filter getFilter()`**: Returns an Aerospike `Filter` object if the parser was able to optimize a portion of the DSL string into a secondary index query. Returns `null` if no optimization was possible. +* **`com.aerospike.client.exp.Expression.Exp getExp()`**: Returns the Aerospike `Exp` object representing the DSL filter logic. This is the part of the expression that will be executed on the server for records that pass the secondary index filter. If the entire DSL string was converted into a `Filter`, this may be `null`. + +### `com.aerospike.dsl.IndexContext` + +A container for the information required for automatic secondary index optimization. + +* **`static IndexContext of(String namespace, Collection indexes)`**: Creates a context. + * `namespace`: The namespace the query will be run against. + * `indexes`: A collection of `Index` objects representing the available secondary indexes for that namespace. + +## Example API Flow + +Here is a recap of how the classes work together in a typical use case: + +```java +// 1. Get a parser instance +DSLParser parser = new DSLParserImpl(); + +// 2. Define the context for the expression and placeholders +String dsl = "$.age > ?0"; +ExpressionContext context = ExpressionContext.of(dsl, PlaceholderValues.of(30)); + +// (Optional) Define the index context for optimization +IndexContext indexContext = IndexContext.of("namespace", availableIndexes); + +// 3. Parse the expression once to get a reusable object +ParsedExpression parsedExpression = parser.parseExpression(context, indexContext); + +// 4. Get the final result by substituting values +// This step can be repeated many times with different values +ParseResult result = parsedExpression.getResult(); + +// 5. Extract the Filter and Expression for use in a QueryPolicy +Filter siFilter = result.getFilter(); +Expression filterExp = Exp.build(result.getExp()); + +QueryPolicy policy = new QueryPolicy(); +policy.filterExp = filterExp; +// Note: The Java client does not have a separate field for the secondary index filter. +// The filter is applied by the client before sending the query. +``` \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..8da2f59 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,146 @@ +# Getting Started with Aerospike Expression DSL + +Welcome to the Aerospike Expression DSL for Java! Let's walk you through the first steps, from setup to running your first query. + +## What is the Expression DSL? + +The Aerospike Expression DSL is a Java library that provides a simple, string-based language to create powerful server-side filters. Instead of building complex filter objects in Java, you can write an intuitive expression string, which the library translates into a native Aerospike Expression. + +For example, instead of writing this in Java: + +```java +Expression exp = Exp.build( + Exp.and( + Exp.gt(Exp.intBin("age"), Exp.val(30)), + Exp.eq(Exp.stringBin("country"), Exp.val("US")) + ) +); +``` + +You can simply write this string: + +``` +"$.age > 30 and $.country == 'US'" +``` + +This makes your filter logic easier to write, read, and even store as configuration. + +## Quickstart: Your First Filtered Query + +Let's build and run a complete example. + +### Prerequisites + +1. **Java 17+**: Ensure you have a compatible JDK installed. +2. **Maven or Gradle**: For managing dependencies. +3. **Aerospike Database**: An Aerospike server instance must be running. The easiest way to get one is with Docker: + ```sh + docker run -d --name aerospike -p 3000:3000 -p 3001:3001 -p 3002:3002 aerospike/aerospike-server-enterprise + ``` + +### 1. Project Setup + +Add the Expression DSL and the Aerospike Java Client as dependencies to your project. + +**Maven (`pom.xml`):** +```xml + + + com.aerospike + aerospike-expression-dsl + 0.1.0 + + + com.aerospike + aerospike-client-jdk8 + 8.1.1 + + +``` + +### 2. Write the Code + +Here is a Java example. It connects to a local Aerospike instance, writes a few sample records, and then uses a DSL expression to query for a subset of that data. + +```java +import com.aerospike.client.*; +import com.aerospike.client.exp.Exp; +import com.aerospike.client.exp.Expression; +import com.aerospike.client.policy.QueryPolicy; +import com.aerospike.client.query.RecordSet; +import com.aerospike.client.query.Statement; +import com.aerospike.dsl.ExpressionContext; +import com.aerospike.dsl.ParsedExpression; +import com.aerospike.dsl.api.DSLParser; +import com.aerospike.dsl.impl.DSLParserImpl; + +public class DslQuickstart { + + public static void main(String[] args) { + // 1. Connect to Aerospike + try (AerospikeClient client = new AerospikeClient("127.0.0.1", 3000)) { + // 2. Write some sample data + writeSampleData(client); + + // 3. Define and Parse the DSL Expression + DSLParser parser = new DSLParserImpl(); + String dslString = "$.age > 30 and $.city == 'New York'"; + System.out.println("Using DSL Expression: " + dslString); + + ParsedExpression parsedExpression = parser.parseExpression(ExpressionContext.of(dslString)); + Expression filterExpression = Exp.build(parsedExpression.getResult().getExp()); + + // 4. Create and Execute the Query + QueryPolicy queryPolicy = new QueryPolicy(); + queryPolicy.filterExp = filterExpression; + + Statement stmt = new Statement(); + stmt.setNamespace("test"); + stmt.setSetName("users"); + + System.out.println("\nQuery Results:"); + try (RecordSet rs = client.query(queryPolicy, stmt)) { + while (rs.next()) { + System.out.println(rs.getRecord()); + } + } + } + } + + private static void writeSampleData(AerospikeClient client) { + String namespace = "test"; + String setName = "users"; + + client.put(null, new Key(namespace, setName, "user1"), + new Bin("name", "Alice"), new Bin("age", 28), new Bin("city", "San Francisco")); + client.put(null, new Key(namespace, setName, "user2"), + new Bin("name", "Bob"), new Bin("age", 35), new Bin("city", "New York")); + client.put(null, new Key(namespace, setName, "user3"), + new Bin("name", "Charlie"), new Bin("age", 42), new Bin("city", "New York")); + client.put(null, new Key(namespace, setName, "user4"), + new Bin("name", "Diana"), new Bin("age", 29), new Bin("city", "Chicago")); + + System.out.println("Sample data written."); + } +} +``` + +### 3. Run and Verify + +When you run this code, you will see the following output. Notice that only the two records matching the DSL filter (`age > 30` AND `city == 'New York'`) are returned. + +``` +Sample data written. +Using DSL Expression: $.age > 30 and $.city == 'New York' + +Query Results: +(gen:1),(exp:486523),(bins:(name:Bob),(age:35),(city:New York)) +(gen:1),(exp:486523),(bins:(name:Charlie),(age:42),(city:New York)) +``` + +Congratulations! You've successfully used the Expression DSL to filter records in Aerospike. + +### Next Steps + +* Explore the **Core Concepts in How-To Guides** to learn about more advanced filtering capabilities. +* Check out the **Installation & Setup Guide** for detailed configuration options. diff --git a/docs/guides/01-writing-your-first-expression.md b/docs/guides/01-writing-your-first-expression.md new file mode 100644 index 0000000..1c859e3 --- /dev/null +++ b/docs/guides/01-writing-your-first-expression.md @@ -0,0 +1,136 @@ +# Guide: Writing Your First Expression + +This guide covers the fundamental syntax of the Aerospike Expression DSL. You will learn how to filter records based on bin values and combine multiple conditions. + +## Anatomy of the DSL + +The Expression DSL is a functional language for applying predicates to Aerospike bin data and record metadata. Here’s a breakdown of a simple example: + +``` +$.binName > 100 + ^ ^ ^ ^ + | | | | + | | | +---- Value (can be integer, float, or 'string') + | | +------- Comparison Operator (==, !=, >, <, etc.) + | +----------- Bin Name and / or Function + +---------------- Path Operator (always starts with $.) +``` + +### Path Operator (`$.`) + +All expressions start with `$.` to signify the root of the record. You follow it with the name of a bin (e.g., `$.name`) or a metadata function (e.g., `$.lastUpdate()`). + +### Operators + +The DSL supports a rich set of operators. + +* **Comparison**: `==`, `!=`, `>`, `>=`, `<`, `<=` +* **Logical**: `and`, `or`, `not()`, `exclusive()` +* **Arithmetic**: `+`, `-`, `*`, `/`, `%` + +### Values + +* **Integers**: `100`, `-50` +* **Floats**: `123.45` +* **Strings**: Must be enclosed in single quotes, e.g., `'hello'`, `'US'`. +* **Booleans**: `true`, `false` + +## Filtering on Bin Values + +Here are some examples of basic filters on different data types. + +### Numeric Bins + +To filter on a bin containing an integer or float, use standard comparison operators. + +**DSL String:** +``` +"$.age >= 30" +``` + +**Java Usage:** +```java +ExpressionContext context = ExpressionContext.of("$.age >= 30"); +ParsedExpression parsed = parser.parseExpression(context); +QueryPolicy queryPolicy = new QueryPolicy(); +queryPolicy.filterExp = Exp.build(parsed.getResult().getExp()); +``` + +### String Bins + +Remember to enclose string literals in single quotes. + +**DSL String:** +``` +"$.country == 'US'" +``` + +**Java Usage:** +```java +ExpressionContext context = ExpressionContext.of("$.country == 'US'"); +// ... +``` + +### Boolean Bins + +**DSL String:** +``` +"$.active == true" +``` + +## Combining Conditions with Logical Operators + +You can build complex filters by combining conditions with `and` and `or`. Use parentheses `()` to control the order of evaluation. + +### `and` Operator + +Returns records that match **all** conditions. + +**DSL String:** +``` +"$.age > 30 and $.country == 'US'" +``` + +### `or` Operator + +Returns records that match **at least one** of the conditions. + +**DSL String:** +``` +"$.tier == 'premium' or $.logins > 100" +``` + +### `not()` Operator + +Negates a condition. + +**DSL String:** +``` +"not($.country == 'US')" +``` + +### `exclusive()` Operator + +Creates an expression that returns true if only one of its parts is true. + +**DSL String:** +``` +"exclusive($.x < '5', $.x > '5')" +``` + +### Controlling Precedence with Parentheses + +Just like in mathematics, you can use parentheses to group expressions and define the order of operations. The `and` operator has a higher precedence than `or`. + +Consider this expression: +``` +"$.age > 65 or $.age < 18 and $.isStudent == true" +``` + +This is evaluated as `$.age > 65 or ($.age < 18 and $.isStudent == true)`. + +To get the intended logic, use parentheses: +``` +"($.age > 65 or $.age < 18) and $.isStudent == true" +``` +This expression correctly filters for users who are either over 65 or under 18, and who are also students. \ No newline at end of file diff --git a/docs/guides/02-working-with-lists-and-maps.md b/docs/guides/02-working-with-lists-and-maps.md new file mode 100644 index 0000000..90b395e --- /dev/null +++ b/docs/guides/02-working-with-lists-and-maps.md @@ -0,0 +1,218 @@ +# Guide: Working with Lists and Maps + +The Expression DSL provides a powerful and intuitive syntax for filtering on Complex Data Types (CDTs), such as Lists and Maps. This guide will show you how to query these structures. + +The syntax allows to work with complex data filtering in a readable manner, leading to highly efficient queries. + +## Accessing Map Values + +You can access values within a map bin using standard dot notation. + +### Filtering by Map Key + +Let's say you have a `user` bin that is a map containing profile information. + +**Record Data:** +```json +{ + "user": { + "name": "Alice", + "email": "alice@example.com", + "logins": 150 + } +} +``` + +**DSL String:** +To find users with more than 100 logins, you can write: +``` +"$.user.logins > 100" +``` + +**Java Usage:** +```java +ExpressionContext context = ExpressionContext.of("$.user.logins > 100"); +// ... +``` + +### Accessing with Non-Standard Keys + +If a map key contains spaces or special characters, you can use single quotes. + +**Record Data:** +```json +{ + "metrics": { + "daily logins": 25 + } +} +``` + +**DSL String:** +``` +"$.metrics.'daily logins' > 20" +``` + +### Filtering by Map Index + +Assuming we have the same map containing profile information: + +**Record Data:** +```json +{ + "user": { + "name": "Alice", + "email": "alice@example.com", + "logins": 150 + } +} +``` + +Indexes are 0-based. To access the first element, use `[0]`. + +**DSL String:** +To find records with element indexed at 0 having value `Alice`, you can write it in a short form: + +``` +"$.user.{0} == 'Alice'" +``` + +Or use the full form of such DSL string if needed: + +``` +"$.user.{0}.get(type: STRING, return: VALUE) == 'Alice'" +``` + +### Filtering by Map Value + +**DSL String:** +To find records having element with value 150, you can write: +``` +"$.user.{=150}" +``` + +For instance, by using a counting function you can find if there are multiple records where user has value 150: +``` +"$.user.{=150}.count() > 1" +``` + +### Filtering by Map Rank + +Assuming we have an ordered map containing user preferences information: + +**Record Data:** +```json +{ + "user": { + "setting1": 15, + "setting2": 150, + "setting3": 25 + } +} +``` +**DSL String:** +To find records having value of an element with rank 2 larger than 20, you can write: +``` +"$.user.{#2} > 20" +``` + +Or in full form: +``` +"$.user.{#2} > 20".get(type: INT, return: VALUE) +``` + +## Accessing List Elements + +### Filtering by List Index + +Indexes are 0-based. To access the first element, use `[0]`. + +**Record Data:** +Let's say you have a `scores` list bin containing test scores. +```json +{ + "scores": [88, 95, 72] +} +``` + +**DSL String:** +To find records where the first score is greater than 90: +``` +"$.scores.[0] > 90" +``` + +### Filtering by List Value + +**DSL String:** +To find records with a scores element equal to 90: +``` +"$.scores.[=90]" +``` + +For instance, you can use counting function to find if there are multiple records with value 90: +``` +"$.scores.[=90].count() > 1" +``` + +### Filtering by List Rank + +**DSL String:** +To find records where the value of scores element with rank 2 is larger than 30: +``` +"$.scores.[#2] > 30" +``` + +## Querying Nested Structures + +The real power of the DSL shines when you combine these accessors to query deeply nested data. + +### Map containing a List + +Imagine a `user` bin where one of the map values is a list of roles. + +**Record Data:** +```json +{ + "user": { + "name": "Bob", + "roles": ["admin", "editor"] + } +} +``` + +**DSL String:** +To find a user whose first role is "admin": +``` +"$.user.roles.[0] == 'admin'" +``` + +### List containing a Map + +Now consider a list of `devices`, where each element is a map. + +**Record Data:** +```json +{ + "devices": [ + { "type": "phone", "os": "iOS" }, + { "type": "laptop", "os": "linux" } + ] +} +``` + +**DSL String:** +To find records where the second device is a laptop: +``` +"$.devices.[1].type == 'laptop'" +``` + +### Functions on CDTs + +You can call functions on CDT bins, such as counting the size. + +**DSL String:** +To find records where the `devices` list contains more than 1 item: +``` +"$.devices.count() > 1" +``` +You can also see this written as `$.devices.[].count() > 1`, which is equivalent. \ No newline at end of file diff --git a/docs/guides/03-filtering-on-record-metadata.md b/docs/guides/03-filtering-on-record-metadata.md new file mode 100644 index 0000000..c6ca770 --- /dev/null +++ b/docs/guides/03-filtering-on-record-metadata.md @@ -0,0 +1,115 @@ +# Guide: Filtering on Record Metadata + +Aerospike tracks several metadata fields for each record, such as its time-to-live (TTL), last update time and so on. The Expression DSL provides special functions to use this metadata in your filter expressions. + +All metadata functions are called on the record root, using the `$.` prefix. + +## Time-To-Live (TTL) + +The TTL is the remaining life of a record in seconds. You can use it to find records that are about to expire or records that are permanent. + +### `$.ttl()` + +Returns the remaining TTL of the record in seconds. + +**Use Case:** Find all records that will expire in the next 24 hours. + +**DSL String:** +``` +"$.ttl() < 86400" // 86400 seconds = 24 hours +``` + +**Use Case:** Find all records that are set to never expire. The server represents this with a TTL of 0, but special care should be taken depending on server version. A common convention might be to check for a very large TTL if your application sets them. For records that are created without a TTL and the namespace has a default TTL, `ttl()` will reflect that. A record explicitly set to never expire (TTL -1 on write) will have a void time of 0, and its TTL will be calculated from that. On server versions 4.2+, a TTL of -1 can be used to signify "never expire". + +**DSL String:** +To find records that will not expire (assuming server 4.2+ and TTL set to -1 on write): +``` +"$.ttl() == -1" +``` + +## Last Update Time + +You can filter records based on when they were last modified. + +### `$.lastUpdate()` + +Returns the timestamp of when the record was last updated, in nanoseconds since the Unix epoch (January 1, 1970). + +**Use Case:** Find records updated before the year 2023. + +**DSL String:** +``` +// Timestamp for 2023-01-01T00:00:00Z in nanoseconds +"$.lastUpdate() < 1672531200000000000" +``` + +### `$.sinceUpdate()` + +Returns the number of milliseconds that have passed since the record was last updated. This is often more convenient than `lastUpdate()`. + +**Use Case:** Find all records that have not been modified in the last 7 days. + +**DSL String:** +``` +"$.sinceUpdate() > 604800000" // 7 * 24 * 60 * 60 * 1000 milliseconds +``` + +## Record Storage Size + +You can filter records based on how much storage they consume. + +### `$.deviceSize()` + +Returns the amount of storage the record occupies on disk, in bytes. + +**Use Case:** Find "large" records that consume more than 1 megabyte of disk space. + +**DSL String:** +``` +"$.deviceSize() > 1048576" // 1024 * 1024 bytes +``` + +### `$.memorySize()` + +Returns the amount of storage the record occupies in memory, in bytes. This is relevant for hybrid storage namespaces (data in memory). + +**DSL String:** +``` +"$.memorySize() > 131072" // 128 KB +``` + +## Other Metadata Functions + +### `$.isTombstone()` + +Returns `true` if the record is a tombstone (i.e., it has been deleted but not yet cleaned up by the server). + +**Use Case:** Find records that have been deleted but are still occupying space. + +**DSL String:** +``` +"$.isTombstone() == true" +``` + +### `$.setName()` + +Returns the name of the set the record belongs to. + +**Use Case:** Find records that are in either the 'customers' or 'prospects' set. + +**DSL String:** +``` +"$.setName() == 'customers' or $.setName() == 'prospects'" +``` + +### `$.digestModulo(value)` + +Returns the record's digest (its unique ID) modulo some integer value. This is a powerful function for distributing work across multiple clients. + +**Use Case:** Process 1/4 of the records in a batch job. + +**DSL String:** +This expression will be true for roughly 25% of your records. +``` +"$.digestModulo(4) == 0" +``` \ No newline at end of file diff --git a/docs/guides/04-arithmetic-expressions.md b/docs/guides/04-arithmetic-expressions.md new file mode 100644 index 0000000..5c559b9 --- /dev/null +++ b/docs/guides/04-arithmetic-expressions.md @@ -0,0 +1,104 @@ +# Advanced Topic: Arithmetic Expressions + +The Expression DSL allows you to perform certain simple arithmetic operations directly on bin values within your expressions. This enables you to push mathematical computations to the Aerospike server, avoiding the need to pull data to the client for processing. + +This is useful for a wide range of scenarios, such as dynamic price calculations, scoring, or checking computed thresholds. + +## Supported Arithmetic Operators + +The DSL supports the standard set of arithmetic operators: + +* `+` (Addition) +* `-` (Subtraction) +* `*` (Multiplication) +* `/` (Division) +* `%` (Modulo) + +You can use these operators on numeric bin values and literal numeric values. + +## Use Case: Dynamic Thresholds + +Imagine an e-commerce application where you want to find all orders that have a `quantity` of at least 5 and for which the `order_total` is greater than `quantity * price_per_item * 0.9` (representing a 10% discount threshold). + +### DSL String + +You can write this complex logic as a single, clear expression: + +``` +"$.quantity >= 5 and $.order_total > ($.quantity * $.price_per_item * 0.9)" +``` + +### How it Works + +For each record being scanned, the server will: +1. Evaluate the first condition: `$.quantity >= 5`. If `false`, the record is skipped. +2. If `true`, it evaluates the arithmetic part: + a. It reads the value from the `quantity` bin. + b. It reads the value from the `price_per_item` bin. + c. It multiplies them together, and then multiplies by `0.9`. +3. It then compares the `order_total` bin's value with the computed result. +4. If the condition is met, the record is returned. + +This entire computation happens on the server, which is efficient. + +**Java Usage:** +```java +// Find orders with quantity larger than 5 and arithmetical condition on order_total +String dsl = "$.quantity >= 5 and $.order_total > ($.quantity * $.price_per_item * 0.9)"; +ExpressionContext context = ExpressionContext.of(dsl); + +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); + +// Setting the resulting Expression as query filter +QueryPolicy queryPolicy = new QueryPolicy(); +queryPolicy.filterExp = filter; +// ... execute query +``` + +## Combining with Placeholders + +Arithmetic expressions can be combined with placeholders to make them even more flexible. + +### Use Case: Finding recent high-value activity + +Let's say you want to find users whose `login_streak` (number of consecutive days logged in) is greater than their `account_age` (in days) divided by a configurable factor. + +**DSL String with Placeholders:** +``` +"$.login_streak > ($.account_age / ?0)" +``` + +**Java Usage:** +```java +// Find users whose streak is greater than their account age divided by 7 +String dsl = "$.login_streak > ($.account_age / ?0)"; +PlaceholderValues values = PlaceholderValues.of(7); +ExpressionContext context = ExpressionContext.of(dsl, values); + +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); + +// Setting the resulting Expression as query filter +QueryPolicy queryPolicy = new QueryPolicy(); +queryPolicy.filterExp = filter; +// ... execute query +``` + +## Operator Precedence + +The DSL follows standard mathematical operator precedence. `*`, `/`, and `%` have higher precedence than `+` and `-`. You can use parentheses `()` to explicitly control the order of evaluation. + +**Example:** +This expression: +``` +"$.val1 + $.val2 * 2 > 100" +``` +is evaluated as `$.val1 + ($.val2 * 2) > 100`. + +To perform the addition first, use parentheses: +``` +"($.val1 + $.val2) * 2 > 100" +``` + +By leveraging server-side arithmetic, you can build more powerful and efficient queries that are tailored to your application's business logic. \ No newline at end of file diff --git a/docs/guides/05-using-placeholders.md b/docs/guides/05-using-placeholders.md new file mode 100644 index 0000000..608eb54 --- /dev/null +++ b/docs/guides/05-using-placeholders.md @@ -0,0 +1,110 @@ +# Guide: Using Placeholders for Security and Performance + +Placeholders allow you to create parameterized DSL expressions. Instead of embedding literal values directly into your DSL string, you use special markers (starting with `?`) that are replaced with actual values at runtime. + +This practice is recommended due to performance enhancement. It allows the DSL parser to compile the expression *once* and reuse the result many times with different values, which is much faster than re-parsing the string for every query. + +## The Cost of Parsing + +When you provide the `DSLParser` with a string, it performs several steps: +1. **Lexing**: Breaks the string into a stream of tokens (e.g., `$.`, `age`, `>`, `100`). +2. **Parsing**: Builds an Abstract Syntax Tree (AST) representing the logical structure of the expression. +3. **Compilation**: Traverses the AST to create a template for the final result. + +This process has a small but non-zero CPU cost. If you are parsing the same string inside a tight loop (e.g., for every incoming web request), this cost can add up. + +## Placeholder Syntax + +Placeholders are denoted by a question mark followed by a zero-based index: `?0`, `?1`, `?2`, and so on. + +**DSL String with Placeholders:** +``` +"$.age > ?0 and $.city == ?1" +``` + +Here, `?0` is the first placeholder, and `?1` is the second. + +## Providing Placeholder Values + +To use an expression with placeholders, you must provide the corresponding values when you parse it. This is done using the `ExpressionContext` and `PlaceholderValues` classes. + +### Java Usage Example + +```java +import com.aerospike.dsl.ExpressionContext; +import com.aerospike.dsl.PlaceholderValues; +import com.aerospike.dsl.api.DSLParser; +import com.aerospike.dsl.impl.DSLParserImpl; +// ... other imports + +DSLParser parser = new DSLParserImpl(); + +// The DSL string with indexed placeholders +String dsl = "$.age > ?0 and $.city == ?1"; + +// Create a PlaceholderValues object with the values to substitute. +// The order of values must match the placeholder indexes. +PlaceholderValues values = PlaceholderValues.of(30, "New York"); + +// Create the ExpressionContext +ExpressionContext context = ExpressionContext.of(dsl, values); + +// Parse the expression +ParsedExpression parsedExpression = parser.parseExpression(context); + +// Now you can get the result and use it in a query +Expression filter = Exp.build(parsedExpression.getResult().getExp()); + +QueryPolicy queryPolicy = new QueryPolicy(); +queryPolicy.filterExp = filter; +``` + +### Type Handling + +The library automatically handles different data types for placeholders, including `Integer`, `Long`, `Double`, `String`, and `byte[]`. The type of the value you provide in `PlaceholderValues` will be translated into the final Aerospike Expression. + +**Example with different types:** +``` +String dsl = "$.lastLogin > ?0 and $.name == ?1"; + +// Provide a Long for the timestamp and a String for the name +PlaceholderValues values = PlaceholderValues.of(1672531200000L, "Alice"); + +ExpressionContext context = ExpressionContext.of(dsl, values); +// ... +``` + +## Reusing Parsed Expressions + +The most significant performance benefit of placeholders comes from reusing the `ParsedExpression` object. The parsing process (translating the string into an internal structure) only needs to happen once. After that, you can efficiently substitute new values. + +### High-Performance Example + +Imagine an application that needs to query for users by age and city repeatedly. + +```java +// --- One-time setup --- +DSLParser parser = new DSLParserImpl(); +String dsl = "$.age > ?0 and $.city == ?1"; +ExpressionContext initialContext = ExpressionContext.of(dsl); // No values needed at first + +// Parse the expression once and cache the result +ParsedExpression cachedParsedExpression = parser.parseExpression(initialContext); + + +// --- In your application's request-handling logic (called many times) --- + +public void findUsers(int age, String city) { + // Create PlaceholderValues for the current request + PlaceholderValues currentValues = PlaceholderValues.of(age, city); + + // Get the result by substituting new values. This is very fast! + ParseResult result = cachedParsedExpression.getResult(currentValues); + + Expression filter = Exp.build(result.getExp()); + + // Execute query with the filter... +} +``` + +By following this pattern, you minimize parsing overhead and create more efficient applications. \ No newline at end of file diff --git a/docs/guides/06-using-secondary-indexes.md b/docs/guides/06-using-secondary-indexes.md new file mode 100644 index 0000000..bf2acf5 --- /dev/null +++ b/docs/guides/06-using-secondary-indexes.md @@ -0,0 +1,106 @@ +# Guide: Automatic Secondary Index Optimization + +One of the most powerful features of the Expression DSL is its ability to automatically leverage accessible Aerospike secondary indexes (SI). + +When you provide the parser with a list of available indexes, it can analyze your DSL string and transform a part of it into a more performant secondary index filter. + +## Why is this important? + +* **Performance**: A secondary index query is significantly faster than a full scan with a filter expression. An SI query allows the database to jump directly to the records that might match, whereas a filter expression requires the server to read every single record in the set and evaluate the expression against it. +* **Simplicity**: You don't need to manually decide which parts of your query should use an index. You can write a single, logical DSL string, and the parser will perform the optimization for you given that you + +## How it Works + +When you call `parser.parseExpression()`, you can optionally provide an `IndexContext`. This object tells the parser which namespace you are querying and which secondary indexes are available. + +The parser then does the following: +1. It analyzes the `and` components of your DSL expression if there are any. +2. It compares each component against the list of available indexes. +3. If it finds a component that can be satisfied by a numeric or string range query on an indexed bin, it converts that component into an Aerospike secondary index `Filter` object. +4. It marks that component in order not to use it when building `Expression`. +5. The rest of the DSL string is converted into an `Expression`. +6. The `ParseResult` then contains either one of the following entities or **both**: +* `Filter` (for the SI query) - given that the correct `IndexContext` was provided, and that the given DSL expression can be converted to a secondary index filter +* `Expression` (for the scan expression filter) + +> **Note:** Only one secondary index can be used per query. The index will be chosen based on cardinality (preferring indexes with a higher `binValuesRatio`), otherwise alphabetically. + +## Usage Example + +Let's assume you have a secondary index named `idx_users_city` on the `city` bin in the `users` set. + +### 1. Define the Index Information + +First, you need to represent your available index in code. + +```java +import com.aerospike.client.query.IndexType; +import com.aerospike.dsl.Index; +import com.aerospike.dsl.IndexContext; +import java.util.List; + +// Describe the available secondary index +Index cityIndex = Index.builder() + .name("idx_users_city") + .namespace("test") + .bin("city") + .indexType(IndexType.STRING) + .binValuesRatio(1) // Cardinality can be retrieved from the Aerospike DB or set manually + .build(); + +// Create index context +IndexContext indexContext = IndexContext.of("namespace", List.of(cityIndex)); +``` + +### 2. Parse the Expression using IndexContext + +Now, provide the `indexContext` when you parse your DSL string. + +**DSL String:** +``` +"$.city == 'New York' and $.age > 30" +``` + +**Java Code:** +```java +DSLParser parser = new DSLParserImpl(); +String dsl = "$.city == 'New York' and $.age > 30"; +ExpressionContext context = ExpressionContext.of(dsl); + +// Provide the IndexContext to enable using SI filter +ParsedExpression parsed = parser.parseExpression(context, indexContext); +ParseResult result = parsed.getResult(); +``` + +### 3. Extract Both Filter and Expression +```java + +// 3. Extract Both Filter and Expression +Filter siFilter = result.getFilter(); // This will be non-null if indexes are correct and DSL string input allows building SI filter, like in this example +Expression filterExpression = Exp.build(result.getExp()); // This will contain the scan expression filter + +// The parser has split the query: +// siFilter is now a Filter.equal("city", "New York") +// filterExpression is now Exp.gt(Exp.intBin("age"), Exp.val(30)) +``` +### 4. Execute the Query + +When you execute the query, you need to use both the `Filter` and the `Expression`. The Java client handles this by applying the secondary index `Filter` first to select the initial set of records, and then applying the `filterExp` on the server to those results. + +```java +Statement stmt = new Statement(); +stmt.setNamespace("test"); +stmt.setSetName("users"); + +// Apply the secondary index filter +stmt.setFilter(siFilter); + +// Apply the remaining filter expression +QueryPolicy queryPolicy = new QueryPolicy(); +queryPolicy.filterExp = filterExpression; + +// Execute the highly optimized query +client.query(queryPolicy, stmt); +``` + +By providing the `IndexContext`, you have allowed the DSL parser to transform a potentially slow scan into a fast, targeted query, without changing your original DSL logic. \ No newline at end of file diff --git a/docs/guides/07-conditional-logic-control-structures.md b/docs/guides/07-conditional-logic-control-structures.md new file mode 100644 index 0000000..e029817 --- /dev/null +++ b/docs/guides/07-conditional-logic-control-structures.md @@ -0,0 +1,204 @@ +# Conditional Logic: Control Structures `with` and `when` + +The Expression DSL supports conditional logic through `when` and `with` control structures, similar to a `CASE` statement in SQL. This allows you to build sophisticated expressions that can return different values based on a set of conditions. + +This is particularly useful for server-side data transformation or implementing complex business rules. + +## Control Structure `when` + +The `when` structure enables you to push complex conditional logic directly to the server, reducing the need to pull data to the client for evaluation and minimizing data transfer. + +The basic structure of a `when` expression is a series of `condition => result` pairs, optionally ending with a `default` clause: + +``` +when( + condition1 => result1, + condition2 => result2, + ..., + default => defaultResult +) +``` + +The server evaluates the conditions in order and returns the result for the *first* condition that evaluates to `true`. If no conditions are true, the `default` result is returned. + +## Use Case: Tiered Logic + +Imagine you want to categorize users into different tiers based on their `rank` bin, and then check if their `tier` bin matches that calculated category. + +**Business Rules:** +* If `rank` > 90, tier is "gold" +* If `rank` > 70, tier is "silver" +* If `rank` > 40, tier is "bronze" +* Otherwise, the tier is "basic" + +### DSL String + +You can express this logic in a single DSL expression to verify a user's `tier`. + +``` +"$.tier == when($.rank > 90 => 'gold', $.rank > 70 => 'silver', $.rank > 40 => 'bronze', default => 'basic')" +``` + +Let's break this down: +1. `when(...)` evaluates the inner logic first. If a record has `rank: 95`, the `when` block returns the string `'gold'`. +2. The outer expression then becomes `$.tier == 'gold'`. +3. The entire expression will return `true` if the `tier` bin for that record is indeed set to "gold", and `false` otherwise. + +### Using Static DSL String in Java + +```java +String dslString = "$.tier == when($.rank > 90 => 'gold', $.rank > 70 => 'silver', $.rank > 40 => 'bronze', default => 'basic')"; + +ExpressionContext context = ExpressionContext.of(dslString); +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); + +// Using Aerospike Java client +QueryPolicy queryPolicy = new QueryPolicy(); + +// Setting the resulting Expression as query filter +queryPolicy.filterExp = filter; +// This query will now return only the records where the tier bin is correctly set according to the rank. +``` + +### Using DSL String with Placeholders in Java + +We can also use placeholders within a `when` expression for greater flexibility. Placeholders mark the places where values provided separately are matched by indexes. +This way the same DSL String can be used multiple times with different values for the same placeholders. + +For example, let's add placeholders to our previous DSL expression and use the same API for generating an `Expression`: + +```java +String dsl = "$.tier == when($.rank > ?0 => ?1, $.rank > ?2 => ?3, default => ?4)"; + +PlaceholderValues values = PlaceholderValues.of( + 90, "gold", + 70, "silver", + 40, "bronze", + "basic" +); + +ExpressionContext context = ExpressionContext.of(dsl, values); +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); +// ... +``` + +The `when` structure enables you to push complex conditional logic directly to the server, reducing the need to pull data to the client for evaluation and minimizing data transfer. + +## Control Structure `with` + +The basic structure of a with expression allows you to declare temporary variables and use them within a subsequent expression: + +``` +with ( + var1 = val1, + var2 = val2, + ... +) do (expression) + +``` + +The server evaluates the variable assignments in order, making each variable available for use in later assignments and in the final expression after the `do` keyword. + +## Simple Example + +For a simpler illustration of variable usage and calculations: + +``` +"with (x = 1, y = ${x} + 1) do (${x} + ${y})" +``` + +This expression: + +1. Defines variable x with value 1 +2. Defines variable y with value ${x} + 1 (which evaluates to 2) +3. Returns the result of ${x} + ${y} (which evaluates to 3) + +The `with` structure enables us to create more readable and maintainable expressions by breaking complex logic into named variables. This is especially valuable when the same intermediate calculation is used multiple times in an expression or when the expression logic is complex. + +## Use Case: More Complex Calculations + +Imagine we want to calculate a user's eligibility score based on multiple factors like account `age`, `transaction history`, and `credit score`, then determine if they qualify for a premium service. + +**Business Rules:** +* Calculate a base score from account age +* Add bonus points based on transaction count +* Apply a multiplier based on credit score +* User qualifies if final score exceeds threshold + +### DSL String + +We can use the `with` construct to make this complex calculation more readable and maintainable: + +``` +"with ( + baseScore = $.accountAgeMonths * 0.5, + transactionBonus = $.transactionCount > 100 ? 25 : 0, + creditMultiplier = $.creditScore > 700 ? 1.5 : 1.0, + finalScore = (${baseScore} + ${transactionBonus}) * ${creditMultiplier} +) do (${finalScore} >= 75 && $.premiumEligible == true)" +``` + +Let's break this down: + +1. We first calculate `baseScore` based on account age +2. We determine `transactionBonus` based on transaction count +3. We set `creditMultiplier` based on credit score +4. We calculate the `finalScore` using the previous variables +5. The final expression checks if the `finalScore` is at least 75 and if `premiumEligible` is true + + +### Using Static DSL String in Java + +```java +String dslString = "with (" + + "baseScore = $.accountAgeMonths * 0.5, " + + "transactionBonus = $.transactionCount > 100 ? 25 : 0, " + + "creditMultiplier = $.creditScore > 700 ? 1.5 : 1.0, " + + "finalScore = (${baseScore} + ${transactionBonus}) * ${creditMultiplier}" + + ") do (${finalScore} >= 75 && $.premiumEligible == true)"; + +ExpressionContext context = ExpressionContext.of(dslString); +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); + +// Using Aerospike Java client +QueryPolicy queryPolicy = new QueryPolicy(); + +// Setting the resulting Expression as query filter +queryPolicy.filterExp = filter; +// This query will return only records of users who qualify for premium service +``` + +### Using DSL String with Placeholders in Java + +You can also use placeholders within a with expression for greater flexibility. Placeholders mark the places where values provided separately are matched by indexes. +This way the same DSL String can be used multiple times with different values for the same placeholders. + +For example, let's add placeholders to our previous DSL expression and use the same API for generating an `Expression`: + +```java +String dsl = "with (" + + "baseScore = $.accountAgeMonths * ?0, " + + "transactionThreshold = ?1, " + + "transactionBonus = $.transactionCount > ${transactionThreshold} ? ?2 : 0, " + + "creditThreshold = ?3, " + + "creditMultiplier = $.creditScore > ${creditThreshold} ? ?4 : 1.0, " + + "finalScore = (${baseScore} + ${transactionBonus}) * ${creditMultiplier}" + + ") do (${finalScore} >= ?5 && $.premiumEligible == true)"; + +PlaceholderValues values = PlaceholderValues.of( + 0.5, // Age multiplier + 100, // Transaction threshold + 25, // Transaction bonus + 700, // Credit score threshold + 1.5, // Credit multiplier + 75 // Minimum score threshold +); + +ExpressionContext context = ExpressionContext.of(dsl, values); +ParsedExpression parsed = parser.parseExpression(context); +Expression filter = Exp.build(parsed.getResult().getExp()); +// ... +``` \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..298bad9 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,85 @@ +# Installation & Setup + +This guide provides detailed instructions for adding the Aerospike Expression DSL library to your project and configuring it. + +## Prerequisites + +* **Java 17 or later** +* **Aerospike Server 5.7 or later** +* A build tool such as **Maven** or **Gradle**. + +## Library Installation + +To use the Expression DSL, you need to add two dependencies to your project: `aerospike-expression-dsl` and the core `aerospike-client-jdk8`. + +### Maven + +Add the following dependencies to your `pom.xml` file: + +```xml + + + + com.aerospike + aerospike-expression-dsl + 0.1.0 + + + + + + com.aerospike + aerospike-client-jdk8 + 8.1.1 + + +``` + +### Gradle + +Add the following to your `build.gradle` file's `dependencies` block: + +```groovy +dependencies { + // The Expression DSL Library + implementation 'com.aerospike:aerospike-expression-dsl:0.1.0' + + // The core Aerospike Java Client + // Ensure this version is compatible with your server version + implementation 'com.aerospike:aerospike-client-jdk8:8.1.1' +} +``` + +## Initializing the Parser + +The main entry point for the library is the `DSLParser` interface. To get started, simply instantiate the default implementation: + +```java +import com.aerospike.dsl.api.DSLParser; +import com.aerospike.dsl.impl.DSLParserImpl; + +// Create a reusable parser instance +DSLParser parser = new DSLParserImpl(); +``` + +This `parser` object is thread-safe and can be reused across your application to parse different DSL expression strings. + +## Compatibility Matrix + +It is important to ensure the versions of the DSL library, Java client, and Aerospike Server are compatible. + +| `aerospike-expression-dsl` | `aerospike-client-jdk8` | Aerospike Server | +| :--- | :--- |:-----------------| +| 0.1.0 | 8.0.0+ | 5.7+ | + +## Building from Source (Optional) + +If you need to build the library from source, you will need to regenerate the ANTLR sources first. + +The grammar file is located at `src/main/antlr4/com/aerospike/dsl/Condition.g4`. + +Run the following Maven command to re-generate the Java parser classes: + +```sh +mvn clean generate-sources compile +``` \ No newline at end of file diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..3a1317f --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,45 @@ +# Troubleshooting & FAQ + +This guide provides solutions to common problems and answers frequently asked questions about the Aerospike Expression DSL. + +## Common Errors and Solutions + +### `DslParseException` + +This is the most common exception thrown by the library, indicating a problem with the DSL string itself. + +| Cause | Example | Solution | +|:----------------------------| :--- |:--------------------------------------------------------------------------------------------------------------------------------------------| +| **Mismatched Parentheses** | `($.age > 30` | Ensure every opening parenthesis `(` has a corresponding closing parenthesis `)`. | +| **Invalid Operator** | `$.age ** 30` | Check the spelling of all operators. Valid operators include `and`, `or`, `not`, `==`, `!=`, `>`, `>=`, `<`, `<=`, `+`, `-`, `*`, `/`, `%`. | +| **Unquoted String Literal** | `$.city == New York` | Enclose all string literals in single quotes: `$.city == 'New York'`. | +| **Incomplete Expression** | `$.age >` | Ensure every operator has the correct number of operands. | +| **Incompatible Types** | `($.apples.get(type: STRING) + 5) > 10` | Cannot compare STRING to INT | + +### `IllegalArgumentException: Missing value for placeholder` + +This error occurs when your DSL string contains placeholders (`?0`, `?1`, etc.), but you did not provide a corresponding value in the `PlaceholderValues` object. + +**Example DSL:** `$.age > ?0 and $.city == ?1` + +**Incorrect Code:** +```java +// Only one value provided, but two are needed +ExpressionContext.of(dsl, PlaceholderValues.of(30)); +``` + +**Solution:** Ensure you provide a value for every placeholder in the correct order. + +```java +ExpressionContext.of(dsl, PlaceholderValues.of(30, "New York")); +``` + +### Query is Slow or Scans the Entire Dataset + +If your filtered query is not performing as expected, it may be because it is not leveraging a secondary index. + +* **Symptom**: A query on a highly selective field (e.g., a unique user ID) takes a long time. +* **Cause**: The query is using a filter expression to scan all records on the server instead of using a secondary index to jump directly to the candidate records. +* **Solution**: + 1. Ensure a secondary index exists on the filter predicate. + 2. Provide an `IndexContext` to the `parseExpression` method. This enables the parser to perform automatic query optimization. See the **"Leveraging Secondary Indexes Automatically"** guide for a detailed walkthrough. \ No newline at end of file diff --git a/src/test/java/com/aerospike/dsl/expression/ControlStructuresTests.java b/src/test/java/com/aerospike/dsl/expression/ControlStructuresTests.java index e65b314..7e45860 100644 --- a/src/test/java/com/aerospike/dsl/expression/ControlStructuresTests.java +++ b/src/test/java/com/aerospike/dsl/expression/ControlStructuresTests.java @@ -22,6 +22,9 @@ void whenWithASingleDeclaration() { // different spacing style TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("when($.who == 1 => \"bob\", default => \"other\")"), expected); + // alternative quotation + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("when($.who == 1 => 'bob', default => 'other')"), + expected); } @Test diff --git a/src/test/java/com/aerospike/dsl/expression/LogicalExpressionsTests.java b/src/test/java/com/aerospike/dsl/expression/LogicalExpressionsTests.java index 9d32f72..cb19931 100644 --- a/src/test/java/com/aerospike/dsl/expression/LogicalExpressionsTests.java +++ b/src/test/java/com/aerospike/dsl/expression/LogicalExpressionsTests.java @@ -61,6 +61,10 @@ void binLogicalExclusive() { Exp.exclusive( Exp.eq(Exp.stringBin("hand"), Exp.val("hook")), Exp.eq(Exp.stringBin("leg"), Exp.val("peg")))); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("exclusive($.hand == 'hook', $.leg == 'peg')"), + Exp.exclusive( + Exp.eq(Exp.stringBin("hand"), Exp.val("hook")), + Exp.eq(Exp.stringBin("leg"), Exp.val("peg")))); // More than 2 expressions exclusive TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("exclusive($.a == \"aVal\", $.b == \"bVal\", " + diff --git a/src/test/java/com/aerospike/dsl/expression/MapExpressionsTests.java b/src/test/java/com/aerospike/dsl/expression/MapExpressionsTests.java index 4399d7c..a9a527d 100644 --- a/src/test/java/com/aerospike/dsl/expression/MapExpressionsTests.java +++ b/src/test/java/com/aerospike/dsl/expression/MapExpressionsTests.java @@ -112,6 +112,22 @@ void quotedStringInExpressionPath() { expected); TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.'127.0.0.1'.bcc.get(type: INT) > 200"), expected); + + + expected = Exp.gt( + MapExp.getByKey( + MapReturnType.VALUE, + Exp.Type.INT, + Exp.val("bcc"), + Exp.mapBin("mapBin1"), + // Map key with spaces in it + CTX.mapKey(Value.get("127 0 0 1")) + ), + Exp.val(200)); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.\"127 0 0 1\".bcc.get(type: INT) > 200"), + expected); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.'127 0 0 1'.bcc.get(type: INT) > 200"), + expected); } @Test @@ -199,6 +215,19 @@ void mapByIndex() { TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.{0}.get(type: INT, return: VALUE) == 100"), expected); TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.{0}.asInt() == 100"), expected); + + Exp expected2 = Exp.eq( + MapExp.getByIndex( + MapReturnType.VALUE, + Exp.Type.STRING, + Exp.val(0), + Exp.mapBin("mapBin1") + ), + Exp.val("value")); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.{0} == 'value'"), expected2); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.{0}.get(type: STRING) == 'value'"), expected2); + TestUtils.parseFilterExpressionAndCompare(ExpressionContext.of("$.mapBin1.{0}.get(type: STRING, return: VALUE) == 'value'"), + expected2); } @Test