| name | add-hql-to-mql-translation |
| description | Use when designing or planning support for a new HQL construct in the MongoDB MQL translator (AbstractMqlTranslator). Covers domain-specific investigation steps and conventions that augment the standard brainstorming and writing-plans superpowers skills. |
Adding HQL-to-MQL Translation Support
This guidance covers domain-specific investigation steps and conventions for adding support for a new HQL construct in AbstractMqlTranslator. It augments the standard brainstorming → writing-plans → subagent-driven-development workflow; it does not replace it.
Phase 1: Investigate Before Brainstorming
Do this before brainstorming.
1a. Nail the MongoDB mechanism
- What aggregation stage(s) implement this construct? What are all the supported forms?
- Which shapes map to a simple stage form vs. a complex form requiring
$expr or nested pipelines?
- Write 2–3 concrete pipeline examples covering the key shapes. These become the spec's "Concrete Examples" section.
- If translation is not obvious, verify the mechanism by creating test data and executing pipelines in
mongosh
1b. Investigate the Hibernate SQL AST
Extract source from the Hibernate sources JAR at ~/.gradle/caches/.../hibernate-core-<version>-sources.jar (version from gradle/libs.versions.toml).
Read AbstractSqlAstTranslator — find the method(s) that handle this construct. Every branch the SQL translator handles is a shape you must either translate or explicitly throw for.
Read the SQL AST node class(es) for the construct to understand what fields/methods are available.
Known gotchas — patterns that are easy to miss when translating Hibernate's SQL AST:
isVirtual() table groups — virtual table groups are embeddable wrappers that don't render to a JOIN themselves; the SQL translator descends into their sub-joins without emitting a JOIN clause. Do the same: skip emitting a stage but recurse into sub-joins.
For any new unsupported shape: confirm it is reachable from HQL (not just handcrafted SQL AST) so negative tests can be written through HQL. Check SemanticQueryBuilder and the HQL grammar in HqlParser.java to verify the syntax.
1c. Enumerate all shapes
Build a complete table: every combination of the construct that's syntactically valid in HQL. Mark each ✅ (translatable) or ❌ (throw). For ❌ shapes, identify which ticket will track implementation.
File Jira tickets for every ❌ shape before writing any code. Use real ticket numbers in throw messages from day one — no literal HIBERNATE-NNN placeholders where NNN hasn't been replaced with an actual number. File the Jira tickets first, then reference them in the code. The codebase convention uses a TODO- prefix on these strings to signal the feature is not yet implemented; follow that convention for new throws (e.g. "TODO-HIBERNATE-161 https://jira.mongodb.org/browse/HIBERNATE-161"). File against project HIBERNATE at https://jira.mongodb.org/browse/HIBERNATE using the jira CLI.
Phase 2: Brainstorm and Spec
The spec should follow this structure:
- MongoDB Mechanism — concrete pipeline examples for each supported shape
- Pipeline Structure — where new stages appear relative to
$match → $sort → $skip/$limit → $project
- Implementation Approach — new AST node classes (if any), exact changes to
AbstractMqlTranslator
- Supported and Unsupported Shapes — table with Jira ticket for each ❌
- Tests — table listing each positive and negative test with what it covers
Phase 3: Write the Plan
The plan must follow this task order:
- New AST node(s) — unit test (
assertRendering) then implementation, one task per record class; TDD
- Integration test skeleton — create test class with entities, seed data, ONE positive test, ONE negative test (both failing); commit
- Core translator changes — implement; run first positive test; run existing integration tests for regressions; commit
- Remaining positive tests — all positive cases; run full positive suite; commit
- Coverage verification (do not skip) — add temporary
System.err.println markers to all non-trivial code paths (loops, recursion, branches); run targeted tests; confirm each path is actually exercised; remove logging; re-run suite. Report and investigate if any path is uncovered before proceeding.
- Negative tests — all
FeatureNotSupportedException cases; run full suite; commit
- Final build —
./gradlew build + full integration test run
Test conventions:
- Positive tests:
assertSelectionQuery(hql, resultType, expectedMql, expectedResults, expectedAffectedCollections) — always assert the full MQL pipeline string, the full result set, and the full set of affected collections
- When the
$project fields are unknown upfront (e.g. JOIN FETCH): use assertActualCommandsInOrder with /* FILL IN after first test run */; upgrade to full assertSelectionQuery once the actual MQL is known
- Negative tests:
assertSelectQueryFailure(hql, resultType, FeatureNotSupportedException.class, "TODO-HIBERNATE-161 ...") — use the real ticket number, following the TODO-HIBERNATE-NNN codebase convention
- Positive tests live in a descriptively named
@Nested class with @BeforeEach seed data; negative tests live at the outer class level for one-off cases or in @Nested class Unsupported when there are several.
- Every non-trivial code path (loop iterations, recursive calls, each instanceof branch) must be covered by a positive or negative test
Phase 4: Implement
Follow the subagent-driven-development workflow: fresh subagent per task, spec compliance review then code quality review after each task.