feature(ci): validate PPL linter compatibility across grammar surfaces - #5678
feature(ci): validate PPL linter compatibility across grammar surfaces#5678Hanyu-W wants to merge 5 commits into
Conversation
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 38144b1.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
4858b84 to
4381c28
Compare
RyanL1997
left a comment
There was a problem hiding this comment.
This PR needs a reconstruction:
- first we need to clearify the scope of this testing: it should be catching the grammar changes that impacts the rules we are having for linter in OSD
- the CI workflows should handle the version dynamically - e.g. at the future versions this change will be in the next release version 3.9 and when that happen, we should automatically picking up both SQL version and OSD version from that branch instead of main
- No need to implement more ITs for the existing grammar this is out of the scope of this change. It should be ONLY focusing on catching the linter related grammar change by the OSD validator, not the SQL plugin itself.
9737781 to
9b93621
Compare
PR Reviewer Guide 🔍(Review updated until commit b360c80)Here are some key observations to aid the review process:
|
|
Persistent review updated to latest commit 9b93621 |
Signed-off-by: Hanyu Wei <weihanyu@amazon.com>
9b93621 to
38144b1
Compare
|
Persistent review updated to latest commit 38144b1 |
PR Code Suggestions ✨Latest suggestions up to b360c80 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit b360c80
Suggestions up to commit 38144b1
|
| if (api.buildTree) { | ||
| const parse = api.buildTree(grammarCase.query, grammar); | ||
| if (!hasParseTree(parse)) throw new Error('candidate parser produced no parse tree'); | ||
| if (hasErrorNode(parseTreeOf(parse))) { |
There was a problem hiding this comment.
This asserts a contract OSD deliberately does not hold. buildRuntimeTree in headless_ppl_lint.ts documents the opposite: it keeps error-recovered trees on purpose because "the lint rules are written to walk it best-effort", citing eval x = <field> + 1 as a query the engine accepts but the runtime ATN cannot fully parse.
So a grammar change where the parser recovers but every lint rule still behaves correctly fails here, carrying no information about linter breakage. The diagnostic-count comparison below is the real signal — I would drop this gate or make it non-fatal.
Also node.constructor?.name === 'ErrorNode' is name-based type detection, fragile across antlr4ng versions and under bundling.
There was a problem hiding this comment.
Removed the parse-cleanliness gate and the ErrorNode name check. The adapter now relies only on lintQueryWithBundle() diagnostic behavior, so an error-recovered tree passes when the production OSD rules still behave correctly.
| timeout-minutes: 25 | ||
| run: | | ||
| set -euo pipefail | ||
| ./gradlew :opensearch-sql-plugin:run --no-daemon > ppl-grammar-cluster.log 2>&1 & |
There was a problem hiding this comment.
The cluster is not needed. PPLGrammarBundleBuilder.build() is a pure static function over the generated ANTLR classes — no Settings, no NodeClient, no cluster state, and getBundle() is a static singleton. The bundle is a build artifact of the .g4 files.
Booting the plugin (25-minute step timeout, 120x5s readiness poll, port-9200 guard, PID traps, cluster log) to curl static JSON is the biggest cost and flake surface in the PR. A small JavaExec in :ppl that writes ppl-grammar-bundle.json replaces all of it, deletes ~150 lines of bash across this file and the wrapper script, and takes the job from ~45 min to a few. It also moots the analyzer findings about PID reuse and trap handling.
There was a problem hiding this comment.
Replaced the development cluster and REST capture with :ppl:exportPplGrammarBundle, backed directly by PPLGrammarBundleBuilder.getBundle(). The workflow and local wrapper no longer use port 9200, polling, curl, PID traps, or cluster logs.
| run: | | ||
| set -euo pipefail | ||
| module=.ci/OpenSearch-Dashboards/src/plugins/data/public/antlr/opensearch_ppl/headless_ppl_lint | ||
| if [[ -f "${module}.ts" || -f "${module}.js" ]]; then |
There was a problem hiding this comment.
Weakest of three disagreeing capability probes — this is the false-green path.
This accepts only .ts and .js. The wrapper (ppl-lint-rule-validation.sh:386-391) also accepts bare and .mjs. The adapter (validate-osd-grammar.mjs:127-130) accepts bare via existsSync, which additionally matches a directory.
So if OSD refactors headless_ppl_lint.ts into headless_ppl_lint/index.ts, this returns false, the adapter writes skipReason: osd-headless-grammar-api-unavailable, and the job exits 0 — green, indistinguishable from the legitimate old-branch skip, while the API is present and working.
Suggest deleting this step and letting the adapter be the single source of truth; it already handles the absent-module case at :437-440 and returns 0 with the skip report.
There was a problem hiding this comment.
Deleted the workflow-level file probe and its conditional skip path. Every run now delegates capability and validation to the adapter, so the workflow cannot independently produce a false-green absence decision.
| } | ||
| } | ||
|
|
||
| function modulePath(root, name) { |
There was a problem hiding this comment.
Third variant of the capability probe: bare via existsSync (matches a directory), .js, .ts — no .mjs.
This is the most permissive of the three and, being colocated with the code that actually loads the module, the only one that can be correct by construction. Make it the only one — see the comment on ppl-lint-grammar-compatibility.yml:211.
There was a problem hiding this comment.
Consolidated resolution and loading in the adapter. Only a true failure to resolve the headless entry point becomes a legacy skip; import failures, transitive dependency failures, and invalid exports now fail structurally.
| ) | ||
|
|
||
| headless_module_exists() { | ||
| [[ -f "$OSD_ROOT/$HEADLESS_MODULE" || |
There was a problem hiding this comment.
Second variant of the capability probe: bare, .ts, .js, .mjs — but all via -f, so unlike the adapter it never matches a directory.
Three probes, three different answers for the same OSD checkout. A .mjs module is available here and unavailable to the workflow; a directory module is available to the adapter and unavailable to both of these. Collapse to the adapter's check.
There was a problem hiding this comment.
Removed the shell capability probe as well. The local wrapper now follows the same path as CI and always delegates the availability decision to the adapter's resolver/loader.
| function loadCases(file, catalogIds) { | ||
| const document = readJson(file, 'grammar cases'); | ||
| const rawCases = Array.isArray(document) ? document : object(document, 'case document').cases; | ||
| if (!Array.isArray(rawCases) || !rawCases.length) fail('At least one grammar case is required.'); |
There was a problem hiding this comment.
No coverage floor. This requires at least one case, and the loop below requires trigger+control per selected rule — but nothing requires a minimum number of rules.
Truncate grammar-cases.json to a single rule and the report reads rules: {selected: 1, passed: 1} and goes green. Combined with the missing reverse-coverage check against the OSD catalog, deleting a failing case is a valid way to make CI pass.
Asserting the covered set against catalogIds minus an explicit allow-list fixes both at once.
There was a problem hiding this comment.
Fixed in a7e3b2f. The adapter now enforces coveredRuleIds == catalogIds - excludedRuleIds, with a required reason for every exclusion. Deleting a required rule’s cases, adding an unclassified catalog rule, leaving a stale exclusion, overlapping covered/excluded sets, or omitting a trigger/control pair now fails structurally. The required set is the default-enabled catalog rules from OSD #12526.
Signed-off-by: Hanyu Wei <weihanyu@amazon.com>
Signed-off-by: Hanyu Wei <weihanyu@amazon.com>
|
@RyanL1997 Thank you for the feedback. I have pushed changes to address the review, please re-review when you get a chance. Github Actions are currently down preventing me from testing the ci, will update this comment with CI testing results once Github Actions is fixed. EDIT: Github Actions is up, this is what the CI looks like now: |
Signed-off-by: Hanyu Wei <weihanyu@amazon.com>
Signed-off-by: Hanyu Wei <weihanyu@amazon.com>
|
Persistent review updated to latest commit b360c80 |
1 similar comment
|
Persistent review updated to latest commit b360c80 |

Description
Adds
[Linter] PPL grammar compatibility, which validates the SQL candidateruntime grammar with the paired OpenSearch Dashboards production headless
linter.
mainorX.Ywith the same OSD branch and records both SHAs.:ppl; no cluster or backend execution.OSD #12526.
coveredRuleIds == catalogRuleIds - excludedRuleIds. The fourdefault-off headless rules and two explain-backed rules are excluded with
reasons; missing or stale classifications fail.
command-suggestionis the thirteenth default-enabled check, but it is asyntax-channel feature rather than a catalog detector and is out of scope.
Supports #5405.
Testing
node --test scripts/ppl-lint/__tests__/*.test.mjs(37 passing)./gradlew :ppl:test --tests org.opensearch.sql.ppl.autocomplete.PPLGrammarBundleExporterTestactionlint .github/workflows/ppl-lint-grammar-compatibility.ymlgit diff --checkCheck List