From aa6fb4d6ddbdd2723bab3f2845af6331a6f1b44f Mon Sep 17 00:00:00 2001 From: Mike Mannion Date: Sat, 16 Aug 2025 08:21:16 +0200 Subject: [PATCH 1/5] HHH-19704 Added handling of LISTAGG function variants. --- .../main/java/org/hibernate/sql/Template.java | 48 ++- .../hibernate/orm/test/sql/TemplateTest.java | 358 ++++++++++++++++++ 2 files changed, 405 insertions(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 1d97702b483a..947a402702b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -78,7 +78,8 @@ public final class Template { "minus", "except", "intersect", - "partition"); + "partition", + "within"); private static final Set BEFORE_TABLE_KEYWORDS = Set.of("from", "join"); private static final Set FUNCTION_KEYWORDS @@ -93,6 +94,13 @@ public final class Template { = Set.of("first", "next"); private static final Set CURRENT_BIGRAMS = Set.of("date", "time", "timestamp"); + // Ordered-set aggregate function names we want to recognize + private static final Set ORDERED_SET_AGGREGATES + = Set.of("listagg", "percentile_cont", "percentile_disc", "mode"); + // Soft keywords that are only treated as keywords in the LISTAGG extension immediately + // following the argument list and up to and including GROUP + private static final Set LISTAGG_EXTENSION_KEYWORDS + = Set.of("on", "overflow", "error", "truncate", "without", "count", "within", "with", "group"); private static final String PUNCTUATION = "=> columnNames = Template.collectColumnNames( + "SELECT name, age FROM users WHERE id = ?", + factory.getJdbcServices().getDialect(), + factory.getTypeConfiguration() + ); + assertEquals(3, columnNames.size()); // name, age, and id are unqualified identifiers + + // Test collectColumnNames with template that has qualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, {@}.age FROM users WHERE {@}.id = ?"); + assertEquals(3, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("age", columnNames.get(1)); + assertEquals("id", columnNames.get(2)); + + // Test with mixed qualified and unqualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, age, {@}.address FROM users"); + assertEquals(2, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("address", columnNames.get(1)); + + // Test with no template placeholders + columnNames = Template.collectColumnNames("SELECT name, age FROM users"); + assertEquals(0, columnNames.size()); + + // Test with template at end of string + columnNames = Template.collectColumnNames("SELECT name FROM users WHERE id = {@}.value"); + assertEquals(1, columnNames.size()); + assertEquals("value", columnNames.get(0)); + } + + @Test + public void testNamedParameters(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test named parameters are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name = :name", + "SELECT * FROM users WHERE {@}.name = :name", factory); + assertWhereStringTemplate("SELECT * FROM users WHERE age > :minAge AND age < :maxAge", + "SELECT * FROM users WHERE {@}.age > :minAge AND {@}.age < :maxAge", factory); + + // Test named parameters mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE id = :userId AND status = active", + "SELECT {@}.name FROM users WHERE {@}.id = :userId AND {@}.status = {@}.active", factory); + } + + @Test + public void testBooleanLiterals(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test boolean literals are converted to dialect-specific values + assertWhereStringTemplate("SELECT * FROM users WHERE active = true", + "SELECT * FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true), factory); + assertWhereStringTemplate("SELECT * FROM users WHERE deleted = false", + "SELECT * FROM users WHERE {@}.deleted = " + dialect.toBooleanValueString(false), factory); + + // Test boolean literals in expressions + assertWhereStringTemplate("SELECT * FROM users WHERE (active = true) AND (verified = false)", + "SELECT * FROM users WHERE ({@}.active = " + dialect.toBooleanValueString(true) + ") AND ({@}.verified = " + dialect.toBooleanValueString(false) + ")", factory); + + // Test boolean literals mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE active = true AND status = pending", + "SELECT {@}.name FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true) + " AND {@}.status = {@}.pending", factory); + } + + @Test + public void testCurrentDateTimeExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP expressions + assertWhereStringTemplate("SELECT * FROM users WHERE created_date > CURRENT_DATE", + "SELECT * FROM users WHERE {@}.created_date > CURRENT_DATE", factory); + assertWhereStringTemplate("SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", + "SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE event_time < CURRENT_TIME", + "SELECT * FROM events WHERE {@}.event_time < CURRENT_TIME", factory); + + // Test current expressions in complex queries + assertWhereStringTemplate("SELECT name FROM users WHERE last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", + "SELECT {@}.name FROM users WHERE {@}.last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", factory); + } + + @Test + public void testFromClauseHandling(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that identifiers in FROM clause are not qualified + assertWhereStringTemplate("SELECT name FROM users WHERE id = 1", + "SELECT {@}.name FROM users WHERE {@}.id = 1", factory); + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test FROM clause with aliases + assertWhereStringTemplate("SELECT name FROM users AS u WHERE u.id = 1", + "SELECT {@}.name FROM users AS u WHERE u.id = 1", factory); + + // Test complex FROM clause with multiple tables + assertWhereStringTemplate("SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", + "SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", factory); + } + + @Test + public void testCastExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CAST expressions with type names + assertWhereStringTemplate("SELECT CAST(price AS DECIMAL(10,2)) FROM products", + "SELECT CAST({@}.price AS DECIMAL(10,2)) FROM products", factory); + assertWhereStringTemplate("SELECT CAST(id AS VARCHAR) FROM users", + "SELECT CAST({@}.id AS VARCHAR) FROM users", factory); + + // Test CAST with complex expressions + assertWhereStringTemplate("SELECT CAST(price * 1.1 AS INTEGER) FROM products", + "SELECT CAST({@}.price * 1.1 AS INTEGER) FROM products", factory); + } + + @Test + public void testFunctionCalls(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test regular function calls + assertWhereStringTemplate("SELECT UPPER(name) FROM users", + "SELECT UPPER({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT LOWER(email) FROM users", + "SELECT LOWER({@}.email) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT(*) FROM users", + "SELECT COUNT(*) FROM users", factory); + + // Test function calls with multiple parameters + assertWhereStringTemplate("SELECT CONCAT(first_name, ' ', last_name) FROM users", + "SELECT CONCAT({@}.first_name, ' ', {@}.last_name) FROM users", factory); + + // Test nested function calls + assertWhereStringTemplate("SELECT UPPER(LOWER(name)) FROM users", + "SELECT UPPER(LOWER({@}.name)) FROM users", factory); + } + + @Test + public void testExtractAndTrimFunctions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test EXTRACT function with FROM keyword + assertWhereStringTemplate("SELECT EXTRACT(YEAR FROM created_date) FROM users", + "SELECT EXTRACT(YEAR FROM {@}.created_date) FROM users", factory); + assertWhereStringTemplate("SELECT EXTRACT(MONTH FROM birth_date) FROM users", + "SELECT EXTRACT(MONTH FROM {@}.birth_date) FROM users", factory); + + // Test TRIM function with FROM keyword + assertWhereStringTemplate("SELECT TRIM(LEADING ' ' FROM name) FROM users", + "SELECT TRIM(LEADING ' ' FROM {@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT TRIM(TRAILING 'x' FROM code) FROM products", + "SELECT TRIM(TRAILING 'x' FROM {@}.code) FROM products", factory); + } + + @Test + public void testQuotedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test backtick-quoted identifiers + assertWhereStringTemplate("SELECT `user name` FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + assertWhereStringTemplate("SELECT `order-id` FROM orders", + "SELECT {@}." + dialect.quote("`order-id`") + " FROM orders", factory); + + // Test dialect-specific quoted identifiers + char openQuote = dialect.openQuote(); + char closeQuote = dialect.closeQuote(); + assertWhereStringTemplate("SELECT " + openQuote + "user name" + closeQuote + " FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + } + + @Test + public void testQualifiedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that already-qualified identifiers are not re-qualified + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test mixed qualified and unqualified identifiers + assertWhereStringTemplate("SELECT u.name, status FROM users u WHERE u.id = 1", + "SELECT u.name, {@}.status FROM users u WHERE u.id = 1", factory); + + // Test table-qualified identifiers + assertWhereStringTemplate("SELECT users.name, users.email FROM users", + "SELECT users.name, users.email FROM users", factory); + } + + @Test + public void testKeywordsAndIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that SQL keywords are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name IS NOT NULL", + "SELECT * FROM users WHERE {@}.name IS NOT NULL", factory); + assertWhereStringTemplate("SELECT DISTINCT name FROM users", + "SELECT DISTINCT {@}.name FROM users", factory); + assertWhereStringTemplate("SELECT name FROM users ORDER BY name", + "SELECT {@}.name FROM users ORDER BY {@}.name", factory); + + // Test soft keywords that can be column names + assertWhereStringTemplate("SELECT date, time FROM events", + "SELECT {@}.date, {@}.time FROM events", factory); + + // Test soft keywords in CURRENT expressions + assertWhereStringTemplate("SELECT * FROM events WHERE event_date > CURRENT_DATE", + "SELECT * FROM events WHERE {@}.event_date > CURRENT_DATE", factory); + } + + @Test + public void testComplexExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test complex expressions with multiple operators + assertWhereStringTemplate("SELECT name FROM users WHERE age >= 18 AND age <= 65", + "SELECT {@}.name FROM users WHERE {@}.age >= 18 AND {@}.age <= 65", factory); + assertWhereStringTemplate("SELECT price FROM products WHERE price BETWEEN 10 AND 100", + "SELECT {@}.price FROM products WHERE {@}.price BETWEEN 10 AND 100", factory); + + // Test expressions with parentheses + assertWhereStringTemplate("SELECT name FROM users WHERE (age > 18) AND (status = 'active')", + "SELECT {@}.name FROM users WHERE ({@}.age > 18) AND ({@}.status = 'active')", factory); + + // Test expressions with arithmetic operators + assertWhereStringTemplate("SELECT name FROM products WHERE price * 1.1 > 100", + "SELECT {@}.name FROM products WHERE {@}.price * 1.1 > 100", factory); + } + + @Test + public void testOrderedSetAggregationStateTransitions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that state transitions work correctly after ordered-set aggregation + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id) FROM users WHERE active = true", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id) FROM users WHERE {@}.active = true", factory); + + // Test that identifiers after GROUP are qualified again + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id), status FROM users", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id), {@}.status FROM users", factory); + + // Test non-LISTAGG ordered-set aggregates + assertWhereStringTemplate("SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary), department FROM employees", + "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {@}.salary), {@}.department FROM employees", factory); + } + + @Test + public void testLookaheadFunctionality(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test lookahead for function calls with whitespace + assertWhereStringTemplate("SELECT UPPER (name) FROM users", + "SELECT UPPER ({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT ( * ) FROM users", + "SELECT COUNT ( * ) FROM users", factory); + + // Test lookahead for CURRENT expressions with whitespace + assertWhereStringTemplate("SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", + "SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE date > CURRENT DATE", + "SELECT * FROM events WHERE {@}.date > CURRENT DATE", factory); + } + private static void assertWhereStringTemplate(String sql, SessionFactoryImplementor sf) { assertEquals( sql, Template.renderWhereStringTemplate( From abe60069f400260eaf7ef4a2797b956e6a9c19eb Mon Sep 17 00:00:00 2001 From: Mike Mannion Date: Sat, 16 Aug 2025 08:21:16 +0200 Subject: [PATCH 2/5] HHH-19704 Added handling of LISTAGG function. --- .../main/java/org/hibernate/sql/Template.java | 48 ++- .../hibernate/orm/test/sql/TemplateTest.java | 359 ++++++++++++++++++ 2 files changed, 406 insertions(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 1d97702b483a..947a402702b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -78,7 +78,8 @@ public final class Template { "minus", "except", "intersect", - "partition"); + "partition", + "within"); private static final Set BEFORE_TABLE_KEYWORDS = Set.of("from", "join"); private static final Set FUNCTION_KEYWORDS @@ -93,6 +94,13 @@ public final class Template { = Set.of("first", "next"); private static final Set CURRENT_BIGRAMS = Set.of("date", "time", "timestamp"); + // Ordered-set aggregate function names we want to recognize + private static final Set ORDERED_SET_AGGREGATES + = Set.of("listagg", "percentile_cont", "percentile_disc", "mode"); + // Soft keywords that are only treated as keywords in the LISTAGG extension immediately + // following the argument list and up to and including GROUP + private static final Set LISTAGG_EXTENSION_KEYWORDS + = Set.of("on", "overflow", "error", "truncate", "without", "count", "within", "with", "group"); private static final String PUNCTUATION = "=> columnNames = Template.collectColumnNames( + "SELECT name, age FROM users WHERE id = ?", + factory.getJdbcServices().getDialect(), + factory.getTypeConfiguration() + ); + assertEquals(3, columnNames.size()); // name, age, and id are unqualified identifiers + + // Test collectColumnNames with template that has qualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, {@}.age FROM users WHERE {@}.id = ?"); + assertEquals(3, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("age", columnNames.get(1)); + assertEquals("id", columnNames.get(2)); + + // Test with mixed qualified and unqualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, age, {@}.address FROM users"); + assertEquals(2, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("address", columnNames.get(1)); + + // Test with no template placeholders + columnNames = Template.collectColumnNames("SELECT name, age FROM users"); + assertEquals(0, columnNames.size()); + + // Test with template at end of string + columnNames = Template.collectColumnNames("SELECT name FROM users WHERE id = {@}.value"); + assertEquals(1, columnNames.size()); + assertEquals("value", columnNames.get(0)); + } + + @Test + public void testNamedParameters(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test named parameters are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name = :name", + "SELECT * FROM users WHERE {@}.name = :name", factory); + assertWhereStringTemplate("SELECT * FROM users WHERE age > :minAge AND age < :maxAge", + "SELECT * FROM users WHERE {@}.age > :minAge AND {@}.age < :maxAge", factory); + + // Test named parameters mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE id = :userId AND status = active", + "SELECT {@}.name FROM users WHERE {@}.id = :userId AND {@}.status = {@}.active", factory); + } + + @Test + public void testBooleanLiterals(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test boolean literals are converted to dialect-specific values + assertWhereStringTemplate("SELECT * FROM users WHERE active = true", + "SELECT * FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true), factory); + assertWhereStringTemplate("SELECT * FROM users WHERE deleted = false", + "SELECT * FROM users WHERE {@}.deleted = " + dialect.toBooleanValueString(false), factory); + + // Test boolean literals in expressions + assertWhereStringTemplate("SELECT * FROM users WHERE (active = true) AND (verified = false)", + "SELECT * FROM users WHERE ({@}.active = " + dialect.toBooleanValueString(true) + ") AND ({@}.verified = " + dialect.toBooleanValueString(false) + ")", factory); + + // Test boolean literals mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE active = true AND status = pending", + "SELECT {@}.name FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true) + " AND {@}.status = {@}.pending", factory); + } + + @Test + public void testCurrentDateTimeExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP expressions + assertWhereStringTemplate("SELECT * FROM users WHERE created_date > CURRENT_DATE", + "SELECT * FROM users WHERE {@}.created_date > CURRENT_DATE", factory); + assertWhereStringTemplate("SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", + "SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE event_time < CURRENT_TIME", + "SELECT * FROM events WHERE {@}.event_time < CURRENT_TIME", factory); + + // Test current expressions in complex queries + assertWhereStringTemplate("SELECT name FROM users WHERE last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", + "SELECT {@}.name FROM users WHERE {@}.last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", factory); + } + + @Test + public void testFromClauseHandling(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that identifiers in FROM clause are not qualified + assertWhereStringTemplate("SELECT name FROM users WHERE id = 1", + "SELECT {@}.name FROM users WHERE {@}.id = 1", factory); + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test FROM clause with aliases + assertWhereStringTemplate("SELECT name FROM users AS u WHERE u.id = 1", + "SELECT {@}.name FROM users AS u WHERE u.id = 1", factory); + + // Test complex FROM clause with multiple tables + assertWhereStringTemplate("SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", + "SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", factory); + } + + @Test + public void testCastExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CAST expressions with type names + assertWhereStringTemplate("SELECT CAST(price AS DECIMAL(10,2)) FROM products", + "SELECT CAST({@}.price AS DECIMAL(10,2)) FROM products", factory); + assertWhereStringTemplate("SELECT CAST(id AS VARCHAR) FROM users", + "SELECT CAST({@}.id AS VARCHAR) FROM users", factory); + + // Test CAST with complex expressions + assertWhereStringTemplate("SELECT CAST(price * 1.1 AS INTEGER) FROM products", + "SELECT CAST({@}.price * 1.1 AS INTEGER) FROM products", factory); + } + + @Test + public void testFunctionCalls(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test regular function calls + assertWhereStringTemplate("SELECT UPPER(name) FROM users", + "SELECT UPPER({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT LOWER(email) FROM users", + "SELECT LOWER({@}.email) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT(*) FROM users", + "SELECT COUNT(*) FROM users", factory); + + // Test function calls with multiple parameters + assertWhereStringTemplate("SELECT CONCAT(first_name, ' ', last_name) FROM users", + "SELECT CONCAT({@}.first_name, ' ', {@}.last_name) FROM users", factory); + + // Test nested function calls + assertWhereStringTemplate("SELECT UPPER(LOWER(name)) FROM users", + "SELECT UPPER(LOWER({@}.name)) FROM users", factory); + } + + @Test + public void testExtractAndTrimFunctions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test EXTRACT function with FROM keyword + assertWhereStringTemplate("SELECT EXTRACT(YEAR FROM created_date) FROM users", + "SELECT EXTRACT(YEAR FROM {@}.created_date) FROM users", factory); + assertWhereStringTemplate("SELECT EXTRACT(MONTH FROM birth_date) FROM users", + "SELECT EXTRACT(MONTH FROM {@}.birth_date) FROM users", factory); + + // Test TRIM function with FROM keyword + assertWhereStringTemplate("SELECT TRIM(LEADING ' ' FROM name) FROM users", + "SELECT TRIM(LEADING ' ' FROM {@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT TRIM(TRAILING 'x' FROM code) FROM products", + "SELECT TRIM(TRAILING 'x' FROM {@}.code) FROM products", factory); + } + + @Test + public void testQuotedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test backtick-quoted identifiers + assertWhereStringTemplate("SELECT `user name` FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + assertWhereStringTemplate("SELECT `order-id` FROM orders", + "SELECT {@}." + dialect.quote("`order-id`") + " FROM orders", factory); + + // Test dialect-specific quoted identifiers + char openQuote = dialect.openQuote(); + char closeQuote = dialect.closeQuote(); + assertWhereStringTemplate("SELECT " + openQuote + "user name" + closeQuote + " FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + } + + @Test + public void testQualifiedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that already-qualified identifiers are not re-qualified + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test mixed qualified and unqualified identifiers + assertWhereStringTemplate("SELECT u.name, status FROM users u WHERE u.id = 1", + "SELECT u.name, {@}.status FROM users u WHERE u.id = 1", factory); + + // Test table-qualified identifiers + assertWhereStringTemplate("SELECT users.name, users.email FROM users", + "SELECT users.name, users.email FROM users", factory); + } + + @Test + public void testKeywordsAndIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that SQL keywords are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name IS NOT NULL", + "SELECT * FROM users WHERE {@}.name IS NOT NULL", factory); + assertWhereStringTemplate("SELECT DISTINCT name FROM users", + "SELECT DISTINCT {@}.name FROM users", factory); + assertWhereStringTemplate("SELECT name FROM users ORDER BY name", + "SELECT {@}.name FROM users ORDER BY {@}.name", factory); + + // Test soft keywords that can be column names + assertWhereStringTemplate("SELECT date, time FROM events", + "SELECT {@}.date, {@}.time FROM events", factory); + + // Test soft keywords in CURRENT expressions + assertWhereStringTemplate("SELECT * FROM events WHERE event_date > CURRENT_DATE", + "SELECT * FROM events WHERE {@}.event_date > CURRENT_DATE", factory); + } + + @Test + public void testComplexExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test complex expressions with multiple operators + assertWhereStringTemplate("SELECT name FROM users WHERE age >= 18 AND age <= 65", + "SELECT {@}.name FROM users WHERE {@}.age >= 18 AND {@}.age <= 65", factory); + assertWhereStringTemplate("SELECT price FROM products WHERE price BETWEEN 10 AND 100", + "SELECT {@}.price FROM products WHERE {@}.price BETWEEN 10 AND 100", factory); + + // Test expressions with parentheses + assertWhereStringTemplate("SELECT name FROM users WHERE (age > 18) AND (status = 'active')", + "SELECT {@}.name FROM users WHERE ({@}.age > 18) AND ({@}.status = 'active')", factory); + + // Test expressions with arithmetic operators + assertWhereStringTemplate("SELECT name FROM products WHERE price * 1.1 > 100", + "SELECT {@}.name FROM products WHERE {@}.price * 1.1 > 100", factory); + } + + @Test + public void testOrderedSetAggregationStateTransitions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that state transitions work correctly after ordered-set aggregation + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id) FROM users WHERE active = true", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id) FROM users WHERE {@}.active = true", factory); + + // Test that identifiers after GROUP are qualified again + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id), status FROM users", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id), {@}.status FROM users", factory); + + // Test non-LISTAGG ordered-set aggregates + assertWhereStringTemplate("SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary), department FROM employees", + "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {@}.salary), {@}.department FROM employees", factory); + } + + @Test + public void testLookaheadFunctionality(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test lookahead for function calls with whitespace + assertWhereStringTemplate("SELECT UPPER (name) FROM users", + "SELECT UPPER ({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT ( * ) FROM users", + "SELECT COUNT ( * ) FROM users", factory); + + // Test lookahead for CURRENT expressions with whitespace + assertWhereStringTemplate("SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", + "SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE date > CURRENT DATE", + "SELECT * FROM events WHERE {@}.date > CURRENT DATE", factory); + } + private static void assertWhereStringTemplate(String sql, SessionFactoryImplementor sf) { assertEquals( sql, Template.renderWhereStringTemplate( From 1bac432e9591340c54b13c8b3fbc53ebf325d51d Mon Sep 17 00:00:00 2001 From: Mike Mannion Date: Sun, 17 Aug 2025 11:03:39 +0200 Subject: [PATCH 3/5] HHH-19704 Removed redundant param. --- .../src/test/java/org/hibernate/orm/test/sql/TemplateTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java index f6e8f3f2050f..cc403a670eec 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java @@ -206,7 +206,7 @@ SELECT LISTAGG({@}.employee_name, ', ') ON OVERFLOW TRUNCATE WITHOUT COUNT WITHI } @Test - public void testRenderTransformerReadFragment(SessionFactoryScope scope) { + public void testRenderTransformerReadFragment() { // Test the renderTransformerReadFragment method String fragment = "SELECT name, age FROM users WHERE id = ?"; String result = Template.renderTransformerReadFragment(fragment, "name", "age"); @@ -217,6 +217,7 @@ public void testRenderTransformerReadFragment(SessionFactoryScope scope) { assertEquals(fragment, result); // Test with empty column names array + //noinspection RedundantArrayCreation result = Template.renderTransformerReadFragment(fragment, new String[0]); assertEquals(fragment, result); From 40843a14307a28b32c76ea932e0a77a8aa968879 Mon Sep 17 00:00:00 2001 From: Mike Mannion Date: Sat, 16 Aug 2025 08:21:16 +0200 Subject: [PATCH 4/5] HHH-19704 Added handling of LISTAGG function. --- .../main/java/org/hibernate/sql/Template.java | 48 ++- .../hibernate/orm/test/sql/TemplateTest.java | 359 ++++++++++++++++++ 2 files changed, 406 insertions(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 1d97702b483a..947a402702b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -78,7 +78,8 @@ public final class Template { "minus", "except", "intersect", - "partition"); + "partition", + "within"); private static final Set BEFORE_TABLE_KEYWORDS = Set.of("from", "join"); private static final Set FUNCTION_KEYWORDS @@ -93,6 +94,13 @@ public final class Template { = Set.of("first", "next"); private static final Set CURRENT_BIGRAMS = Set.of("date", "time", "timestamp"); + // Ordered-set aggregate function names we want to recognize + private static final Set ORDERED_SET_AGGREGATES + = Set.of("listagg", "percentile_cont", "percentile_disc", "mode"); + // Soft keywords that are only treated as keywords in the LISTAGG extension immediately + // following the argument list and up to and including GROUP + private static final Set LISTAGG_EXTENSION_KEYWORDS + = Set.of("on", "overflow", "error", "truncate", "without", "count", "within", "with", "group"); private static final String PUNCTUATION = "=> columnNames = Template.collectColumnNames( + "SELECT name, age FROM users WHERE id = ?", + factory.getJdbcServices().getDialect(), + factory.getTypeConfiguration() + ); + assertEquals(3, columnNames.size()); // name, age, and id are unqualified identifiers + + // Test collectColumnNames with template that has qualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, {@}.age FROM users WHERE {@}.id = ?"); + assertEquals(3, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("age", columnNames.get(1)); + assertEquals("id", columnNames.get(2)); + + // Test with mixed qualified and unqualified identifiers + columnNames = Template.collectColumnNames("SELECT {@}.name, age, {@}.address FROM users"); + assertEquals(2, columnNames.size()); + assertEquals("name", columnNames.get(0)); + assertEquals("address", columnNames.get(1)); + + // Test with no template placeholders + columnNames = Template.collectColumnNames("SELECT name, age FROM users"); + assertEquals(0, columnNames.size()); + + // Test with template at end of string + columnNames = Template.collectColumnNames("SELECT name FROM users WHERE id = {@}.value"); + assertEquals(1, columnNames.size()); + assertEquals("value", columnNames.get(0)); + } + + @Test + public void testNamedParameters(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test named parameters are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name = :name", + "SELECT * FROM users WHERE {@}.name = :name", factory); + assertWhereStringTemplate("SELECT * FROM users WHERE age > :minAge AND age < :maxAge", + "SELECT * FROM users WHERE {@}.age > :minAge AND {@}.age < :maxAge", factory); + + // Test named parameters mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE id = :userId AND status = active", + "SELECT {@}.name FROM users WHERE {@}.id = :userId AND {@}.status = {@}.active", factory); + } + + @Test + public void testBooleanLiterals(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test boolean literals are converted to dialect-specific values + assertWhereStringTemplate("SELECT * FROM users WHERE active = true", + "SELECT * FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true), factory); + assertWhereStringTemplate("SELECT * FROM users WHERE deleted = false", + "SELECT * FROM users WHERE {@}.deleted = " + dialect.toBooleanValueString(false), factory); + + // Test boolean literals in expressions + assertWhereStringTemplate("SELECT * FROM users WHERE (active = true) AND (verified = false)", + "SELECT * FROM users WHERE ({@}.active = " + dialect.toBooleanValueString(true) + ") AND ({@}.verified = " + dialect.toBooleanValueString(false) + ")", factory); + + // Test boolean literals mixed with identifiers + assertWhereStringTemplate("SELECT name FROM users WHERE active = true AND status = pending", + "SELECT {@}.name FROM users WHERE {@}.active = " + dialect.toBooleanValueString(true) + " AND {@}.status = {@}.pending", factory); + } + + @Test + public void testCurrentDateTimeExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP expressions + assertWhereStringTemplate("SELECT * FROM users WHERE created_date > CURRENT_DATE", + "SELECT * FROM users WHERE {@}.created_date > CURRENT_DATE", factory); + assertWhereStringTemplate("SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", + "SELECT * FROM logs WHERE timestamp > CURRENT_TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE event_time < CURRENT_TIME", + "SELECT * FROM events WHERE {@}.event_time < CURRENT_TIME", factory); + + // Test current expressions in complex queries + assertWhereStringTemplate("SELECT name FROM users WHERE last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", + "SELECT {@}.name FROM users WHERE {@}.last_login < CURRENT_TIMESTAMP - INTERVAL '1 day'", factory); + } + + @Test + public void testFromClauseHandling(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that identifiers in FROM clause are not qualified + assertWhereStringTemplate("SELECT name FROM users WHERE id = 1", + "SELECT {@}.name FROM users WHERE {@}.id = 1", factory); + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test FROM clause with aliases + assertWhereStringTemplate("SELECT name FROM users AS u WHERE u.id = 1", + "SELECT {@}.name FROM users AS u WHERE u.id = 1", factory); + + // Test complex FROM clause with multiple tables + assertWhereStringTemplate("SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", + "SELECT p.name, c.name FROM products p, categories c WHERE p.category_id = c.id", factory); + } + + @Test + public void testCastExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test CAST expressions with type names + assertWhereStringTemplate("SELECT CAST(price AS DECIMAL(10,2)) FROM products", + "SELECT CAST({@}.price AS DECIMAL(10,2)) FROM products", factory); + assertWhereStringTemplate("SELECT CAST(id AS VARCHAR) FROM users", + "SELECT CAST({@}.id AS VARCHAR) FROM users", factory); + + // Test CAST with complex expressions + assertWhereStringTemplate("SELECT CAST(price * 1.1 AS INTEGER) FROM products", + "SELECT CAST({@}.price * 1.1 AS INTEGER) FROM products", factory); + } + + @Test + public void testFunctionCalls(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test regular function calls + assertWhereStringTemplate("SELECT UPPER(name) FROM users", + "SELECT UPPER({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT LOWER(email) FROM users", + "SELECT LOWER({@}.email) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT(*) FROM users", + "SELECT COUNT(*) FROM users", factory); + + // Test function calls with multiple parameters + assertWhereStringTemplate("SELECT CONCAT(first_name, ' ', last_name) FROM users", + "SELECT CONCAT({@}.first_name, ' ', {@}.last_name) FROM users", factory); + + // Test nested function calls + assertWhereStringTemplate("SELECT UPPER(LOWER(name)) FROM users", + "SELECT UPPER(LOWER({@}.name)) FROM users", factory); + } + + @Test + public void testExtractAndTrimFunctions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test EXTRACT function with FROM keyword + assertWhereStringTemplate("SELECT EXTRACT(YEAR FROM created_date) FROM users", + "SELECT EXTRACT(YEAR FROM {@}.created_date) FROM users", factory); + assertWhereStringTemplate("SELECT EXTRACT(MONTH FROM birth_date) FROM users", + "SELECT EXTRACT(MONTH FROM {@}.birth_date) FROM users", factory); + + // Test TRIM function with FROM keyword + assertWhereStringTemplate("SELECT TRIM(LEADING ' ' FROM name) FROM users", + "SELECT TRIM(LEADING ' ' FROM {@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT TRIM(TRAILING 'x' FROM code) FROM products", + "SELECT TRIM(TRAILING 'x' FROM {@}.code) FROM products", factory); + } + + @Test + public void testQuotedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + Dialect dialect = factory.getJdbcServices().getDialect(); + + // Test backtick-quoted identifiers + assertWhereStringTemplate("SELECT `user name` FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + assertWhereStringTemplate("SELECT `order-id` FROM orders", + "SELECT {@}." + dialect.quote("`order-id`") + " FROM orders", factory); + + // Test dialect-specific quoted identifiers + char openQuote = dialect.openQuote(); + char closeQuote = dialect.closeQuote(); + assertWhereStringTemplate("SELECT " + openQuote + "user name" + closeQuote + " FROM users", + "SELECT {@}." + dialect.quote("`user name`") + " FROM users", factory); + } + + @Test + public void testQualifiedIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that already-qualified identifiers are not re-qualified + assertWhereStringTemplate("SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", + "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id", factory); + + // Test mixed qualified and unqualified identifiers + assertWhereStringTemplate("SELECT u.name, status FROM users u WHERE u.id = 1", + "SELECT u.name, {@}.status FROM users u WHERE u.id = 1", factory); + + // Test table-qualified identifiers + assertWhereStringTemplate("SELECT users.name, users.email FROM users", + "SELECT users.name, users.email FROM users", factory); + } + + @Test + public void testKeywordsAndIdentifiers(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that SQL keywords are not qualified + assertWhereStringTemplate("SELECT * FROM users WHERE name IS NOT NULL", + "SELECT * FROM users WHERE {@}.name IS NOT NULL", factory); + assertWhereStringTemplate("SELECT DISTINCT name FROM users", + "SELECT DISTINCT {@}.name FROM users", factory); + assertWhereStringTemplate("SELECT name FROM users ORDER BY name", + "SELECT {@}.name FROM users ORDER BY {@}.name", factory); + + // Test soft keywords that can be column names + assertWhereStringTemplate("SELECT date, time FROM events", + "SELECT {@}.date, {@}.time FROM events", factory); + + // Test soft keywords in CURRENT expressions + assertWhereStringTemplate("SELECT * FROM events WHERE event_date > CURRENT_DATE", + "SELECT * FROM events WHERE {@}.event_date > CURRENT_DATE", factory); + } + + @Test + public void testComplexExpressions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test complex expressions with multiple operators + assertWhereStringTemplate("SELECT name FROM users WHERE age >= 18 AND age <= 65", + "SELECT {@}.name FROM users WHERE {@}.age >= 18 AND {@}.age <= 65", factory); + assertWhereStringTemplate("SELECT price FROM products WHERE price BETWEEN 10 AND 100", + "SELECT {@}.price FROM products WHERE {@}.price BETWEEN 10 AND 100", factory); + + // Test expressions with parentheses + assertWhereStringTemplate("SELECT name FROM users WHERE (age > 18) AND (status = 'active')", + "SELECT {@}.name FROM users WHERE ({@}.age > 18) AND ({@}.status = 'active')", factory); + + // Test expressions with arithmetic operators + assertWhereStringTemplate("SELECT name FROM products WHERE price * 1.1 > 100", + "SELECT {@}.name FROM products WHERE {@}.price * 1.1 > 100", factory); + } + + @Test + public void testOrderedSetAggregationStateTransitions(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test that state transitions work correctly after ordered-set aggregation + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id) FROM users WHERE active = true", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id) FROM users WHERE {@}.active = true", factory); + + // Test that identifiers after GROUP are qualified again + assertWhereStringTemplate("SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY id), status FROM users", + "SELECT LISTAGG({@}.name, ', ') WITHIN GROUP (ORDER BY {@}.id), {@}.status FROM users", factory); + + // Test non-LISTAGG ordered-set aggregates + assertWhereStringTemplate("SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary), department FROM employees", + "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {@}.salary), {@}.department FROM employees", factory); + } + + @Test + public void testLookaheadFunctionality(SessionFactoryScope scope) { + SessionFactoryImplementor factory = scope.getSessionFactory(); + + // Test lookahead for function calls with whitespace + assertWhereStringTemplate("SELECT UPPER (name) FROM users", + "SELECT UPPER ({@}.name) FROM users", factory); + assertWhereStringTemplate("SELECT COUNT ( * ) FROM users", + "SELECT COUNT ( * ) FROM users", factory); + + // Test lookahead for CURRENT expressions with whitespace + assertWhereStringTemplate("SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", + "SELECT * FROM events WHERE timestamp > CURRENT TIMESTAMP", factory); + assertWhereStringTemplate("SELECT * FROM events WHERE date > CURRENT DATE", + "SELECT * FROM events WHERE {@}.date > CURRENT DATE", factory); + } + private static void assertWhereStringTemplate(String sql, SessionFactoryImplementor sf) { assertEquals( sql, Template.renderWhereStringTemplate( From e09c805d0371becf18d61fd966c83bcedd22aee5 Mon Sep 17 00:00:00 2001 From: Mike Mannion Date: Wed, 20 Aug 2025 14:23:58 +0200 Subject: [PATCH 5/5] HHH-19704 redundant KW --- .../main/java/org/hibernate/sql/Template.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index fb637ee58789..4f3d2a589575 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -78,8 +78,7 @@ public final class Template { "minus", "except", "intersect", - "partition", - "within"); + "partition"); private static final Set BEFORE_TABLE_KEYWORDS = Set.of("from", "join"); private static final Set FUNCTION_KEYWORDS @@ -230,8 +229,8 @@ else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) { isOpenQuote = false; } if ( isOpenQuote - && !inFromClause // don't want to append alias to tokens inside the FROM clause - && !endsWithDot( previousToken ) ) { + && !inFromClause // don't want to append alias to tokens inside the FROM clause + && !endsWithDot( previousToken ) ) { result.append( alias ).append( '.' ); } } @@ -370,8 +369,8 @@ else if ( isAliasableIdentifier( token, lcToken, nextToken, //Yuck: if ( inFromClause - && KEYWORDS.contains( lcToken ) // "as" is not in KEYWORDS - && !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) { + && KEYWORDS.contains( lcToken ) // "as" is not in KEYWORDS + && !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) { inFromClause = false; } } @@ -385,8 +384,8 @@ private static boolean isAliasableIdentifier( boolean wasAfterCurrent, Dialect dialect, TypeConfiguration typeConfiguration) { return isUnqualifiedIdentifier( token ) - && !isKeyword( lcToken, wasAfterCurrent, dialect, typeConfiguration ) - && !isLiteral( lcToken, nextToken, sql, symbols, tokens ); + && !isKeyword( lcToken, wasAfterCurrent, dialect, typeConfiguration ) + && !isLiteral( lcToken, nextToken, sql, symbols, tokens ); } private static boolean isFunctionCall( @@ -406,13 +405,13 @@ private static boolean isCurrent( String lcToken, String nextToken, String sql, String symbols, StringTokenizer tokens) { return "current".equals( lcToken ) - && nextToken.isBlank() - && lookPastBlankTokens( sql, symbols, tokens, 1, CURRENT_BIGRAMS::contains ); + && nextToken.isBlank() + && lookPastBlankTokens( sql, symbols, tokens, 1, CURRENT_BIGRAMS::contains ); } private static boolean isFetch(Dialect dialect, String lcToken) { return "fetch".equals( lcToken ) - && dialect.getKeywords().contains( "fetch" ); + && dialect.getKeywords().contains( "fetch" ); } private static boolean endsWithDot(String token) { @@ -431,9 +430,9 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) { // to find the first non-blank token return lookPastBlankTokens( sqlWhereString, symbols, tokens, 1, nextToken -> "'".equals(nextToken) - || lcToken.equals("time") && "with".equals(nextToken) - || lcToken.equals("timestamp") && "with".equals(nextToken) - || lcToken.equals("time") && "zone".equals(nextToken) ); + || lcToken.equals("time") && "with".equals(nextToken) + || lcToken.equals("timestamp") && "with".equals(nextToken) + || lcToken.equals("time") && "zone".equals(nextToken) ); } else { return "'".equals(next); @@ -525,9 +524,9 @@ private static boolean isKeyword( } else { return KEYWORDS.contains( lcToken ) - || isType( lcToken, typeConfiguration ) - || dialect.getKeywords().contains( lcToken ) - || FUNCTION_KEYWORDS.contains( lcToken ); + || isType( lcToken, typeConfiguration ) + || dialect.getKeywords().contains( lcToken ) + || FUNCTION_KEYWORDS.contains( lcToken ); } } @@ -538,8 +537,8 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio private static boolean isUnqualifiedIdentifier(String token) { final char initialChar = token.charAt( 0 ); return initialChar == '`' // allow any identifier quoted with backtick - || isLetter( initialChar ) // only recognizes identifiers beginning with a letter - && token.indexOf( '.' ) < 0; // don't qualify already-qualified identifiers + || isLetter( initialChar ) // only recognizes identifiers beginning with a letter + && token.indexOf( '.' ) < 0; // don't qualify already-qualified identifiers } private static boolean isBoolean(String lcToken) {