A comprehensive Java parser for Hibernate Query Language (HQL) and Java Persistence Query Language (JPQL) with PostgreSQL conversion support.
- ✅ 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 11 or higher (project targets Java 11, developed with Java 21)
- 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 to your pom.xml:
<dependency>
<groupId>com.raditha</groupId>
<artifactId>hql-parser</artifactId>
<version>0.0.2</version>
</dependency>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 (grammar support only - parsing works but SQL conversion not yet implemented)
- 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, but the PostgreSQL converter currently only implements conversion for: COUNT, SUM, AVG, MAX, MIN, UPPER, LOWER, LENGTH, CONCAT, COALESCE, SIZE, ABS, SQRT, CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP. Functions like TRIM, SUBSTRING, NULLIF, CAST, and MOD are parsed but not yet converted to SQL.
- Named parameters:
:paramName(avoid using HQL keywords like:end,:andas 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 testmvn 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. -
Incomplete Function Support: While the parser grammar supports all HQL/JPQL functions, the converter currently only implements conversion for: COUNT, SUM, AVG, MAX, MIN, UPPER, LOWER, LENGTH, CONCAT, COALESCE, SIZE, ABS, SQRT, CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP. Functions like TRIM, SUBSTRING, NULLIF, CAST, and MOD are parsed but passed through as-is without proper conversion.
-
INSERT Statement Conversion: While INSERT ... SELECT statements can be parsed, the converter does not yet implement SQL conversion for INSERT statements. Only SELECT, UPDATE, and DELETE statements are fully supported for conversion.
-
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 MIT License.
Raditha Dissanayake
- Built with ANTLR4
- Implements HQL/JPQL grammar based on Hibernate and JPA specifications