A standalone Java library for parsing HQL/JPQL, extracting query metadata, and converting queries to PostgreSQL SQL.
Published to Maven Central as com.raditha:hql-parser.
Built for standalone use: This library brings JPQL/HQL parsing, query analysis, metadata extraction, and PostgreSQL SQL conversion into a plain Java dependency that can be used in tools, CLIs, CI pipelines, migration utilities, and backend services without needing to boot a full application.
- Build query-aware tooling: power CLIs, linters, analyzers, and migration helpers with HQL/JPQL parsing
- Understand queries programmatically: extract entities, fields, aliases, and parameters for validation or automation
- Translate ORM queries for downstream systems: convert HQL/JPQL into PostgreSQL SQL using explicit mappings
- Run outside application runtime: use it in batch jobs, CI, developer tooling, and backend utilities as a plain Java dependency
- Adopt incrementally: start with syntax validation or metadata extraction, then add conversion where needed
- ✅ Complete HQL/JPQL Grammar Support: Parse SELECT, UPDATE, DELETE, and INSERT statements
- ✅ Query Analysis: Extract entities, fields, parameters, and aliases from queries
- ✅ PostgreSQL Conversion: Convert HQL/JPQL queries to PostgreSQL SQL
- ✅ Entity Detection: Identify all Java entity classes involved in a query
- ✅ Field Mapping: Track entity fields referenced in queries
- ✅ Parameter Extraction: Identify both named (:param) and positional (?1) parameters
- ✅ Join Support: Handle INNER, LEFT, and RIGHT joins
- ✅ Aggregate Functions: Support for COUNT, SUM, AVG, MAX, MIN
- ✅ Query Validation: Check if queries are syntactically correct
- Java 21 or higher
- Maven 3.6+
IntelliJ IDEA Users: If you're getting "Cannot find symbol" errors for ANTLR-generated classes, see INTELLIJ_SETUP.md for configuration instructions.
Add the dependency to your pom.xml:
<dependency>
<groupId>com.raditha</groupId>
<artifactId>hql-parser</artifactId>
<version>1.0.0</version>
</dependency>No additional repository configuration is required when consuming the library from Maven Central.
git clone <repository-url>
cd hql-parser
mvn clean installimport com.raditha.hql.parser.HQLParser;
HQLParser parser = new HQLParser();
String query = "SELECT u FROM User u WHERE u.age > 18";
if (parser.isValid(query)) {
System.out.println("Query is valid!");
}Extract entities, fields, and parameters from HQL queries:
import com.raditha.hql.parser.HQLParser;
import com.raditha.hql.model.MetaData;
HQLParser parser = new HQLParser();
String query = "SELECT u.name, u.email FROM User u WHERE u.active = true AND u.age > :minAge";
MetaData analysis = parser.analyze(query);
System.out.println("Query Type: " + analysis.getQueryType());
System.out.println("Entities: " + analysis.getEntityNames());
System.out.println("Fields: " + analysis.getEntityFields());
System.out.println("Parameters: " + analysis.getParameters());Output:
Query Type: SELECT
Entities: [User]
Fields: {User=[name, email, active, age]}
Parameters: [minAge]
Convert HQL/JPQL queries to PostgreSQL SQL:
import com.raditha.hql.parser.HQLParser;
import com.raditha.hql.model.MetaData;
import com.raditha.hql.converter.HQLToPostgreSQLConverter;
HQLParser parser = new HQLParser();
HQLToPostgreSQLConverter converter = new HQLToPostgreSQLConverter();
// Register entity-to-table mappings
converter.registerEntityMapping("User", "users");
converter.registerEntityMapping("Order", "orders");
// Register field-to-column mappings
converter.registerFieldMapping("User", "userName", "user_name");
converter.registerFieldMapping("User", "firstName", "first_name");
String hql = "SELECT u.userName FROM User u WHERE u.active = true";
// First analyze the query
MetaData analysis = parser.analyze(hql);
// Then convert using both the query and analysis
String sql = converter.convert(hql, analysis);
System.out.println("SQL: " + sql);Output:
SQL: SELECT u.user_name FROM users u WHERE u.active = true
HQLParser parser = new HQLParser();
String query = "SELECT u.name, o.total " +
"FROM User u " +
"INNER JOIN u.orders o " +
"WHERE u.active = true AND o.total > 100 " +
"ORDER BY o.total DESC";
MetaData analysis = parser.analyze(query);
System.out.println("Entities: " + analysis.getEntityNames());
System.out.println("Aliases: " + analysis.getAliases());HQLParser parser = new HQLParser();
// Constructor expressions allow creating DTOs directly in queries
String query = "SELECT NEW com.finance.dto.AccountDTO(a.accountNumber, a.balance) " +
"FROM Account a WHERE a.status = 'ACTIVE'";
MetaData analysis = parser.analyze(query);
System.out.println("Query Type: " + analysis.getQueryType()); // SELECT
System.out.println("Entities: " + analysis.getEntityNames()); // [Account]
System.out.println("Fields: " + analysis.getEntityFields()); // {Account=[accountNumber, balance, status]}
// Works with BETWEEN clause and parameters
String query2 = "SELECT NEW dto.TransactionDTO(t.id, t.amount) " +
"FROM Transaction t WHERE t.transactionDate BETWEEN :start AND :end";
MetaData analysis2 = parser.analyze(query2);
System.out.println("Parameters: " + analysis2.getParameters()); // [start, end]HQLParser parser = new HQLParser();
String query = "UPDATE User SET active = false WHERE lastLogin < :cutoffDate";
MetaData analysis = parser.analyze(query);
System.out.println("Query Type: " + analysis.getQueryType()); // UPDATE
System.out.println("Parameters: " + analysis.getParameters()); // [cutoffDate]// DELETE without alias
String query = "DELETE FROM User WHERE age < 18";
MetaData analysis = parser.analyze(query);
System.out.println("Query Type: " + analysis.getQueryType()); // DELETE
// DELETE with alias (for complex WHERE clauses)
String query2 = "DELETE FROM Purchase p WHERE p.status = 'CANCELLED' AND p.createdDate < :cutoffDate";
MetaData analysis2 = parser.analyze(query2);
System.out.println("Fields: " + analysis2.getEntityFields()); // {Purchase=[status, createdDate]}- SELECT statements with projection
- Constructor expressions with
SELECT NEW ClassName(args...) - UPDATE statements (with/without alias)
- DELETE statements (with/without alias)
- INSERT ... SELECT statements (full conversion support)
- SELECT with DISTINCT
- Constructor expressions for creating DTOs directly in queries
- FROM with entity aliases
- WHERE with complex predicates
- GROUP BY
- HAVING
- ORDER BY (ASC/DESC, NULLS FIRST/LAST)
- INNER JOIN
- LEFT [OUTER] JOIN
- RIGHT [OUTER] JOIN
- FETCH joins
- Comparison: =, !=, <>, <, <=, >, >=
- Logical: AND, OR, NOT
- Arithmetic: +, -, *, /, %
- Special: BETWEEN, IN, LIKE, IS NULL, MEMBER OF, EXISTS
- Aggregate: COUNT, SUM, AVG, MAX, MIN
- String: UPPER, LOWER, TRIM, LENGTH, CONCAT, SUBSTRING
- Date/Time: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP
- Math: ABS, SQRT, MOD
- Other: COALESCE, NULLIF, CAST, SIZE
Note: The parser supports all these functions in the grammar, and the PostgreSQL converter implements conversion for all listed functions. The SIZE() function and MEMBER OF operator produce HQL-specific SQL that may require application-level handling since they don't have direct PostgreSQL equivalents.
- Named parameters:
:paramName(SQL keywords such as:from,:to,:inare supported as parameter names) - Positional parameters:
?1,?2, etc.
Main parser class for HQL/JPQL queries.
Methods:
ParseTree parse(String query)- Parse query and return parse treeMetaData analyze(String query)- Analyze query and extract metadataboolean isValid(String query)- Validate query syntax
Contains analysis results of a parsed query.
Methods:
QueryType getQueryType()- Get query type (SELECT, UPDATE, DELETE, INSERT)Set<String> getEntityNames()- Get all entity names referencedMap<String, Set<String>> getEntityFields()- Get fields for each entityList<String> getAliases()- Get all aliases usedSet<String> getParameters()- Get all parametersMap<String, String> getAliasToEntity()- Get mapping of aliases to entity namesString getEntityForAlias(String alias)- Get entity name for a given alias
Converts HQL/JPQL to PostgreSQL SQL.
Methods:
void registerEntityMapping(String entityName, String tableName)- Map entity to tablevoid registerFieldMapping(String entityName, String fieldName, String columnName)- Map field to columnString convert(String hqlQuery, MetaData analysis)- Convert HQL to SQL using query analysis
hql-parser/
├── src/
│ ├── main/
│ │ ├── java/com/raditha/hql/
│ │ │ ├── converter/ # SQL conversion
│ │ │ ├── examples/ # Usage examples
│ │ │ ├── model/ # Data models
│ │ │ └── parser/ # Core parser and analysis
│ │ └── antlr4/com/raditha/hql/grammar/
│ │ └── HQL.g4 # ANTLR grammar definition
│ └── test/
│ └── java/com/raditha/hql/
│ ├── HQLParserTest.java
│ ├── AdvancedHQLParserTest.java
│ ├── PostgreSQLConverterTest.java
│ ├── AdvancedConverterTest.java
│ └── JoinAnalysisTest.java
├── pom.xml
└── README.md
mvn testhql-parser is available on Maven Central under:
- Group ID:
com.raditha - Artifact ID:
hql-parser - Current version:
1.0.0
mvn compile exec:java -Dexec.mainClass="com.raditha.hql.examples.UsageExamples"# Compile and generate parser from grammar
mvn clean compile
# Run tests
mvn test
# Create JAR
mvn package-
Parameter Naming: Avoid using HQL keywords (like
end,and,or) as parameter names. Use descriptive names like:endDate,:startDateinstead. -
Subquery Limitations: While subqueries are parsed, entity/field extraction from deeply nested subqueries may be incomplete.
-
Collection Join Analysis: For implicit joins (e.g.,
u.orders), the parser infers entity names using simple heuristics (singularization + capitalization). This may not work for irregular plurals or custom naming.
-
Implicit Join ON Clauses: HQL allows implicit joins without ON clauses (using JPA metadata). The converter cannot generate ON clauses automatically - you must provide explicit ON clauses for SQL compatibility.
// HQL (implicit ON clause based on metadata) "FROM User u INNER JOIN u.orders o" // SQL requires explicit ON clause "FROM users u INNER JOIN orders o ON o.user_id = u.id"
-
Parameter Format: The converter keeps HQL parameter format (
:param,?1) rather than converting to PostgreSQL format ($1,$2). You'll need to handle parameter binding separately. -
FETCH Joins: FETCH joins are HQL-specific for eager loading. The converter ignores the FETCH keyword, converting them to regular joins.
-
Entity Metadata Requirements: The converter requires explicit entity-to-table and field-to-column mappings. It does not introspect JPA annotations or Hibernate configuration.
-
Nested Paths: Paths like
u.address.cityare parsed but may not convert correctly if intermediate relationships aren't mapped. -
Collection Functions: HQL-specific functions like
SIZE(),MEMBER OFmay not have direct PostgreSQL equivalents. -
Function Support: All grammar-defined HQL/JPQL functions are converted to their PostgreSQL equivalents, including aggregate functions (COUNT, SUM, AVG, MAX, MIN), string functions (UPPER, LOWER, TRIM, LENGTH, CONCAT, SUBSTRING), date/time functions (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP), math functions (ABS, SQRT, MOD), and type functions (COALESCE, NULLIF, CAST). HQL-specific functions such as
SIZE()andMEMBER OFare passed through as-is and may require application-level handling.
-
UPDATE/DELETE Statements:
- With alias (e.g.,
UPDATE User u SET u.active = false): Fields referenced with alias are extracted - Without alias (e.g.,
UPDATE User SET active = false): Unqualified fields are extracted and mapped in conversion
- With alias (e.g.,
-
Unqualified Fields: In UPDATE/DELETE statements without aliases, unqualified field names are mapped to columns using registered field mappings. This is the expected behavior but differs from SELECT queries where fields are typically qualified with aliases.
While the parser supports most common HQL/JPQL features, some advanced features are not yet implemented:
TREAT()operator for polymorphic queriesINDEX()function for indexed collectionsKEY()andVALUE()functions for map collectionsTYPE()operator for inheritance queries- Bulk INSERT with VALUES clause (only INSERT ... SELECT supported)
The HQL/JPQL grammar is defined in src/main/antlr4/com/raditha/hql/grammar/HQL.g4 using ANTLR4.
The grammar supports:
- Case-insensitive keywords
- Line and block comments
- String literals with escape sequences
- Numeric literals (integers and decimals)
- Identifiers and paths
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the Apache License 2.0.
Raditha Dissanayake
- Built with ANTLR4
- Implements HQL/JPQL grammar based on Hibernate and JPA specifications