Skip to content

Commit b548d62

Browse files
committed
Addressing the comments from the PR revision
1 parent 3057707 commit b548d62

11 files changed

Lines changed: 533 additions & 888 deletions

File tree

sandbox/plugins/dsl-query-executor/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
testImplementation project(':test:framework')
3939
testImplementation "org.mockito:mockito-core:${versions.mockito}"
4040
testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
41+
testImplementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson_annotations}"
4142

4243
internalClusterTestImplementation project(':server')
4344
internalClusterTestImplementation project(':test:framework')
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# DSL Golden File Tests
2+
3+
## Overview
4+
5+
Golden-file-based test framework for the `dsl-query-executor` plugin. The framework validates two conversion paths:
6+
7+
1. **RelNode Generation** (forward path): DSL (`SearchSourceBuilder`) → Calcite `RelNode` logical plan via `SearchSourceConverter.convert()`
8+
2. **SearchResponse Generation** (reverse path): Mock `ExecutionResult` rows → `SearchResponse` via `SearchResponseBuilder.build()`
9+
10+
Each golden file is a self-contained JSON document encoding the input DSL, expected RelNode plan, mock result rows, and expected output DSL. Tests auto-discover all `.json` files in `src/test/resources/golden/`, so adding a new test case requires only adding a new JSON file — no Java code changes needed.
11+
12+
The framework runs as pure unit tests with zero cluster dependency. It constructs Calcite infrastructure (RelOptCluster, type factory, catalog reader) directly from the golden file's `indexMapping`, mirroring the pattern in `TestUtils`.
13+
14+
## Architecture
15+
16+
```mermaid
17+
graph TD
18+
subgraph Golden File on Disk
19+
GF["golden/*.json"]
20+
end
21+
22+
subgraph Test Classes
23+
SSC_TEST["SearchSourceConverterTests<br/>testGoldenFileRelNodeGeneration()"]
24+
SRB_TEST["SearchResponseBuilderTests<br/>testGoldenFileSearchResponseGeneration()"]
25+
end
26+
27+
subgraph Shared Utilities
28+
LOAD["GoldenFileLoader<br/>parse JSON → GoldenTestCase"]
29+
INFRA["CalciteTestInfra<br/>indexMapping → Calcite schema"]
30+
end
31+
32+
subgraph Production Code Under Test
33+
SSC["SearchSourceConverter.convert()"]
34+
SRB["SearchResponseBuilder.build()"]
35+
end
36+
37+
GF -->|auto-discovered| SSC_TEST
38+
GF -->|auto-discovered| SRB_TEST
39+
SSC_TEST --> LOAD
40+
SRB_TEST --> LOAD
41+
SSC_TEST --> INFRA
42+
SRB_TEST --> INFRA
43+
SSC_TEST -->|invokes| SSC
44+
SRB_TEST -->|invokes| SSC
45+
SRB_TEST -->|invokes| SRB
46+
```
47+
48+
The architecture has three layers:
49+
50+
1. **Data Layer**`GoldenTestCase` POJO and `GoldenFileLoader` handle JSON parsing. `CalciteTestInfra` builds Calcite schemas from golden file mappings.
51+
2. **Forward Path**`SearchSourceConverterTests.testGoldenFileRelNodeGeneration()` auto-discovers all golden files, converts each `inputDsl` via `SearchSourceConverter`, and asserts the `RelNode.explain()` output matches `expectedRelNodePlan`. Also validates that RelNode field names match `mockResultFieldNames`.
52+
3. **Reverse Path**`SearchResponseBuilderTests.testGoldenFileSearchResponseGeneration()` auto-discovers all golden files, builds an `ExecutionResult` from mock rows, invokes `SearchResponseBuilder.build()`, and asserts the serialized response matches `expectedOutputDsl` (ignoring non-deterministic fields like `took`, `_shards`, `_score`).
53+
54+
### Key Design Decisions
55+
56+
- **Auto-discovery over per-file test methods**: Tests loop over all `golden/*.json` files. Failures are collected with file names for traceability, then reported together.
57+
- **Integration into existing test classes**: Forward path tests live in `SearchSourceConverterTests`, reverse path in `SearchResponseBuilderTests` — no separate test class needed, reducing duplication.
58+
- **JSON golden files**: JSON is natively supported by OpenSearch's `XContentParser` and `SearchSourceBuilder.fromXContent()`, avoiding extra dependencies.
59+
- **Deterministic RelNode serialization**: Uses `RelNode.explain()` to produce a stable, human-readable plan string.
60+
- **Schema from golden file, not from cluster**: Each golden file carries an `indexMapping` field used to construct a Calcite `RelDataType` directly, eliminating any need for a live cluster.
61+
- **Plan as array of strings**: `expectedRelNodePlan` is a JSON array (one string per line) rather than a `\n`-delimited string, improving readability in golden files.
62+
63+
## Components
64+
65+
### GoldenTestCase
66+
67+
POJO representing a single parsed golden file:
68+
69+
```java
70+
public class GoldenTestCase {
71+
private String testName;
72+
private String indexName;
73+
private Map<String, String> indexMapping; // field name → SQL type name
74+
private Map<String, Object> inputDsl; // raw JSON map for SearchSourceBuilder
75+
private List<String> expectedRelNodePlan; // expected RelNode.explain() lines
76+
private List<String> mockResultFieldNames; // column names for mock result rows
77+
private List<List<Object>> mockResultRows; // simulated result rows
78+
private Map<String, Object> expectedOutputDsl; // expected SearchResponse JSON
79+
private String planType; // "HITS" or "AGGREGATION"
80+
}
81+
```
82+
83+
### GoldenFileLoader
84+
85+
Parses and validates golden files:
86+
87+
```java
88+
public class GoldenFileLoader {
89+
/** Parses a single golden file by name from src/test/resources/golden/ */
90+
public static GoldenTestCase load(String goldenFileName);
91+
92+
/** Parses a single golden file from a file-system path */
93+
public static GoldenTestCase load(Path goldenFilePath);
94+
}
95+
```
96+
97+
### CalciteTestInfra
98+
99+
Builds Calcite planning infrastructure from a golden file's index mapping:
100+
101+
```java
102+
public class CalciteTestInfra {
103+
/** Builds a RelOptCluster, schema, and catalog reader from indexMapping */
104+
public static InfraResult buildFromMapping(String indexName, Map<String, String> indexMapping);
105+
106+
public record InfraResult(RelOptCluster cluster, RelOptTable table, SchemaPlus schema) {}
107+
}
108+
```
109+
110+
### Interaction Flow
111+
112+
```mermaid
113+
sequenceDiagram
114+
participant Test as SearchSourceConverterTests / SearchResponseBuilderTests
115+
participant Loader as GoldenFileLoader
116+
participant Infra as CalciteTestInfra
117+
participant SSC as SearchSourceConverter
118+
participant SRB as SearchResponseBuilder
119+
120+
Note over Test: Auto-discover golden/*.json files
121+
Test->>Loader: load(fileName)
122+
Loader-->>Test: GoldenTestCase
123+
124+
Test->>Infra: buildFromMapping(indexName, indexMapping)
125+
Infra-->>Test: InfraResult(cluster, table, schema)
126+
127+
Note over Test: RelNode Generation (forward path)
128+
Test->>SSC: convert(searchSource, indexName)
129+
SSC-->>Test: QueryPlans
130+
Test->>Test: relNode.explain() → assert matches expectedRelNodePlan
131+
Test->>Test: assert relNode fieldNames == mockResultFieldNames
132+
133+
Note over Test: SearchResponse Generation (reverse path)
134+
Test->>Test: construct ExecutionResult from mockResultRows
135+
Test->>SRB: build(results, tookInMillis)
136+
SRB-->>Test: SearchResponse
137+
Test->>Test: serialize → assert matches expectedOutputDsl
138+
```
139+
140+
## Golden File JSON Schema
141+
142+
```json
143+
{
144+
"testName": "term_query_hits",
145+
"indexName": "test-index",
146+
"indexMapping": {
147+
"name": "VARCHAR",
148+
"price": "INTEGER",
149+
"brand": "VARCHAR",
150+
"rating": "DOUBLE"
151+
},
152+
"planType": "HITS",
153+
"inputDsl": {
154+
"query": {
155+
"term": { "name": { "value": "laptop" } }
156+
},
157+
"size": 10
158+
},
159+
"expectedRelNodePlan": [
160+
"LogicalSort(fetch=[10])",
161+
" LogicalProject(name=[$0], price=[$1], brand=[$2], rating=[$3])",
162+
" LogicalFilter(condition=[=($0, 'laptop')])",
163+
" LogicalTableScan(table=[[test-index]])"
164+
],
165+
"mockResultFieldNames": ["name", "price", "brand", "rating"],
166+
"mockResultRows": [
167+
["laptop", 999, "BrandA", 4.5],
168+
["laptop", 1299, "BrandB", 4.8]
169+
],
170+
"expectedOutputDsl": {
171+
"hits": {
172+
"total": { "value": 2, "relation": "eq" },
173+
"hits": [
174+
{ "_source": { "name": "laptop", "price": 999, "brand": "BrandA", "rating": 4.5 } },
175+
{ "_source": { "name": "laptop", "price": 1299, "brand": "BrandB", "rating": 4.8 } }
176+
]
177+
}
178+
}
179+
}
180+
```
181+
182+
### SQL Type Mapping
183+
184+
The `indexMapping` field uses Calcite `SqlTypeName` strings:
185+
186+
| Golden File Type | SqlTypeName | Java Type |
187+
|---|---|---|
188+
| `VARCHAR` | `SqlTypeName.VARCHAR` | `String` |
189+
| `INTEGER` | `SqlTypeName.INTEGER` | `Integer` |
190+
| `BIGINT` | `SqlTypeName.BIGINT` | `Long` |
191+
| `DOUBLE` | `SqlTypeName.DOUBLE` | `Double` |
192+
| `FLOAT` | `SqlTypeName.FLOAT` | `Float` |
193+
| `BOOLEAN` | `SqlTypeName.BOOLEAN` | `Boolean` |
194+
| `DATE` | `SqlTypeName.DATE` | `Date` |
195+
| `TIMESTAMP` | `SqlTypeName.TIMESTAMP` | `Timestamp` |
196+
197+
All fields are created as nullable (matching `OpenSearchSchemaBuilder` behavior).
198+
199+
## File Organization
200+
201+
```
202+
sandbox/plugins/dsl-query-executor/
203+
├── src/test/
204+
│ ├── README.md
205+
│ ├── java/org/opensearch/dsl/
206+
│ │ ├── converter/
207+
│ │ │ └── SearchSourceConverterTests.java ← forward path golden file tests
208+
│ │ ├── result/
209+
│ │ │ └── SearchResponseBuilderTests.java ← reverse path golden file tests
210+
│ │ └── golden/
211+
│ │ ├── GoldenTestCase.java ← POJO
212+
│ │ ├── GoldenFileLoader.java ← parser + validator
213+
│ │ └── CalciteTestInfra.java ← Calcite schema builder
214+
│ └── resources/golden/
215+
│ ├── match_all_hits.json
216+
│ └── terms_with_avg_aggregation.json
217+
```
218+
219+
## Error Handling
220+
221+
### Golden File Loading Errors
222+
223+
| Error Condition | Behavior |
224+
|---|---|
225+
| Golden file contains invalid JSON | `GoldenFileLoader` throws `IllegalArgumentException` with file path and parse error details |
226+
| Required field missing from golden file | `GoldenFileLoader.validate()` throws `IllegalArgumentException` naming the missing field and file path |
227+
| `indexMapping` contains unsupported SQL type | `CalciteTestInfra.buildFromMapping()` throws `IllegalArgumentException` naming the unsupported type |
228+
| `planType` is invalid | `GoldenFileLoader.validate()` throws `IllegalArgumentException` with the invalid value |
229+
230+
### Test Failure Reporting
231+
232+
| Error Condition | Behavior |
233+
|---|---|
234+
| RelNode plan mismatch | Failure collected with file name, expected and actual plan strings |
235+
| Field names mismatch | Failure collected with file name, expected and actual field lists |
236+
| SearchResponse output mismatch | Failure collected with file name, expected and actual JSON |
237+
| Any exception during a golden file | Failure collected with file name, exception class and message |
238+
| One or more failures collected | Single `fail()` at end with all failures listed |
239+
240+
## Build Integration
241+
242+
Tests run as part of the standard test source set:
243+
- `gradle test` runs all golden file tests alongside existing unit tests
244+
- No cluster required — all tests are pure unit tests
245+
- Non-deterministic fields (`took`, `timed_out`, `_shards`, `_score`) are stripped before comparison
246+
- Aggregation bucket order is normalized (sorted by key) before comparison

0 commit comments

Comments
 (0)