Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ final class HqlText {

private HqlText(String hql) {
text = hql.trim();
String group = firstQueryGroup(text);
HqlText analyzed = group.equals(text) ? null : new HqlText(group);

String lower = lowerCaseKeepingLength(text);
SqlText scan = SqlText.hql(lower);
selectIndex = scan.indexOfTopLevel(SELECT + " ", 0);
Expand All @@ -82,15 +85,13 @@ private HqlText(String hql) {
clauseOriginal = clauseOriginal.substring(prefix.length()).trim();
clauseLower = clauseLower.substring(prefix.length()).trim();
}
projectionCount = count;
selectClauseLower = clauseLower;
rootAlias = readRootAlias(lower);
String alias = readRootAlias(lower);

// the normalized form: an unaliased root gets the alias e, and the projection is qualified with it
String rewritten = text;
String rewrittenClause = clauseLower;
String rewrittenRootAlias = rootAlias;
if (selectIndex >= 0 && fromIndex >= 0) {
String rewrittenRootAlias = alias;
if (analyzed == null && selectIndex >= 0 && fromIndex >= 0) {
int afterFrom = fromIndex + FROM.length() + 2;
int entityEnd = afterFrom;
while (entityEnd < text.length() && !Character.isWhitespace(text.charAt(entityEnd))) entityEnd++;
Expand All @@ -103,11 +104,39 @@ private HqlText(String hql) {
rewrittenRootAlias = SYNTHETIC_ALIAS;
}
}
if (analyzed != null) {
count = analyzed.projectionCount;
clauseLower = analyzed.selectClauseLower;
alias = analyzed.rootAlias;
rewritten = text;
rewrittenClause = analyzed.normalizedClause;
rewrittenRootAlias = analyzed.normalizedRootAlias;
}
projectionCount = count;
selectClauseLower = clauseLower;
rootAlias = alias;
normalized = rewritten;
normalizedClause = rewrittenClause;
normalizedRootAlias = rewrittenRootAlias;
}

/**
* The interior of a leading parenthesized group, unwrapped while the text starts with one. The projections
* and the result type are read from that group, whether a set operator, {@code limit} or anything else follows
* it. An unclosed group is left as written. This is not the text sent to Hibernate.
*/
private static String firstQueryGroup(String text) {
String current = text;
while (current.startsWith("(")) {
int close = SqlText.hql(current).closingParen(0);
if (close < 0) {
return current;
}
current = current.substring(1, close).trim();
}
return current;
}

/**
* Lowercases the text character by character, so that every index of the lowercase text is the same index of
* the original: {@code String.toLowerCase} can lengthen a text, as it does for the dotted capital I of a
Expand Down Expand Up @@ -156,7 +185,8 @@ int projectionCount() {
* {@code select Person from Person} becomes {@code select e from Person e}. Only the select and from of the
* query itself are read, not those of a subquery, and what precedes the select, such as a comment or the
* {@code with} clause of a common table expression, is kept. A query that already aliases its root, or has no
* select or from, is returned as it is.
* select or from, is returned as it is. A query that starts with a parenthesized group is returned as written:
* the group is read for the result type, and a later select is left unchanged.
*/
String normalized() {
return normalized;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ int indexOfTopLevel(String keyword, int fromIndex) {
return -1;
}

/**
* Returns the index of the {@code )} that closes the {@code (} at {@code openIndex}. The two parentheses of a
* pair carry the same depth, and a parenthesis inside a string literal, a quoted identifier or a comment is not
* part of a pair.
*
* @return the index, or -1 when {@code openIndex} is not a plain {@code (} or the group is not closed
*/
int closingParen(int openIndex) {
if (openIndex < 0 || openIndex >= text.length() || kind[openIndex] != PLAIN || text.charAt(openIndex) != '(') {
return -1;
}
int opened = depth[openIndex];
for (int i = openIndex + 1; i < text.length(); i++) {
if (kind[i] == PLAIN && text.charAt(i) == ')' && depth[i] == opened) {
return i;
}
}
return -1;
}

/**
* Splits the text at the separators outside string literals, quoted identifiers, comments and parentheses, and
* trims each part. A text without the separator is one part.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,42 @@ class HqlSelectResultTypeSpec extends HibernateGormDatastoreSpec {
books*.title == ['Emma', 'Persuasion', 'The Hobbit']
books.every { it instanceof HqlResultTypeBook }
}

void 'executeQuery infers scalar results from a parenthesized root query: #hql'() {
expect:
List results = HqlResultTypeBook.executeQuery(hql)
results.sort() == expected
results.every { resultType.isInstance(it) }

where:
hql | expected | resultType
'(select b.title from HqlResultTypeBook b)' | ['Emma', 'Persuasion', 'The Hobbit'] | String
'((select b.title from HqlResultTypeBook b))' | ['Emma', 'Persuasion', 'The Hobbit'] | String
'( select b.title from HqlResultTypeBook b )' | ['Emma', 'Persuasion', 'The Hobbit'] | String
"(select b.title from HqlResultTypeBook b where b.title <> ')')" | ['Emma', 'Persuasion', 'The Hobbit'] | String
'(/* ignore ) and ( */ select b.title from HqlResultTypeBook b)' | ['Emma', 'Persuasion', 'The Hobbit'] | String
'(select b.title from HqlResultTypeBook b where b.author.id in (select a.id from HqlResultTypeAuthor a))' | ['Emma', 'Persuasion', 'The Hobbit'] | String
'(select b.title from HqlResultTypeBook b) union (select a.name from HqlResultTypeAuthor a)' | ['Austen', 'Emma', 'Persuasion', 'The Hobbit', 'Tolkien'] | String
"(select b.title from HqlResultTypeBook b) union select 'extra' from HqlResultTypeBook" | ['Emma', 'Persuasion', 'The Hobbit', 'extra'] | String
}

void 'parenthesized root queries retain entity aggregate and tuple results'() {
expect:
HqlResultTypeBook.executeQuery('(select b from HqlResultTypeBook b order by b.title)')*.title == ['Emma', 'Persuasion', 'The Hobbit']
HqlResultTypeBook.executeQuery('(select count(b) from HqlResultTypeBook b)') == [3L]
HqlResultTypeBook.executeQuery('(select b.title, b.author.name from HqlResultTypeBook b order by b.title)')
.collect { it as List } == [['Emma', 'Austen'], ['Persuasion', 'Austen'], ['The Hobbit', 'Tolkien']]
}

void 'executeQuery infers a scalar from a parenthesized query followed by limit'() {
when:
List results = HqlResultTypeBook.executeQuery('(select b.title from HqlResultTypeBook b) limit 1')

then:
results.size() == 1
results[0] instanceof String
['Emma', 'Persuasion', 'The Hobbit'].contains(results[0])
}
}

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ class HqlQueryContextSpec extends Specification {
"select title, (select count(*) from Author) from Book" | "select title, (select count(*) from Author) from Book e" | Object[]
"/* the book's (title) */ select title from Book" | "/* the book's (title) */ select e.title from Book e" | Object
"SELECT DISTINCT title FROM Book" | "select distinct e.title from Book e" | Object
'(select b.title from Book b)' | '(select b.title from Book b)' | Object
'(select b.title from Book b) union (select a.name from Author a)' | '(select b.title from Book b) union (select a.name from Author a)' | Object
"(select b.title from Book b) union select 'extra' from Book" | "(select b.title from Book b) union select 'extra' from Book" | Object
}

@Unroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ class HqlTextSpec extends Specification {
'select b from Book b /* , not a projection */' | 1 | 'b' | 'b' | 'select b from Book b /* , not a projection */' | Book
'select m[1], b from Book b' | 2 | 'm[1], b' | 'b' | 'select m[1], b from Book b' | Object[]
'from Book b where b.height--1 > 0 select b.width' | 1 | 'b.width' | 'b' | 'from Book b where b.height--1 > 0 select b.width' | Object
'(select b.title from Book b)' | 1 | 'b.title' | 'b' | '(select b.title from Book b)' | Object
'((select b.title from Book b))' | 1 | 'b.title' | 'b' | '((select b.title from Book b))' | Object
'( select b.title from Book b )' | 1 | 'b.title' | 'b' | '( select b.title from Book b )' | Object
'(select b.title from Book b) union (select a.name from Author a)' | 1 | 'b.title' | 'b' | '(select b.title from Book b) union (select a.name from Author a)' | Object
"(select b.title from Book b) union select 'extra' from Book" | 1 | 'b.title' | 'b' | "(select b.title from Book b) union select 'extra' from Book" | Object
'(select b.title from Book b) union all (select a.name from Author a)' | 1 | 'b.title' | 'b' | '(select b.title from Book b) union all (select a.name from Author a)' | Object
'((select b.title from Book b) union all (select a.name from Author a))' | 1 | 'b.title' | 'b' | '((select b.title from Book b) union all (select a.name from Author a))' | Object
'(select b.title from Book b) /* ) select ignored from ignored */ union (select a.name from Author a)' | 1 | 'b.title' | 'b' | '(select b.title from Book b) /* ) select ignored from ignored */ union (select a.name from Author a)' | Object
"(select b.title from Book b where b.title <> ')')" | 1 | 'b.title' | 'b' | "(select b.title from Book b where b.title <> ')')" | Object
"(select 'it''s ), select x from y' from Book b)" | 1 | "'it''s ), select x from y'" | 'b' | "(select 'it''s ), select x from y' from Book b)" | Object
'(select b."title), from ignored" from Book b)' | 1 | 'b."title), from ignored"' | 'b' | '(select b."title), from ignored" from Book b)' | Object
'(select b.`title), from ignored` from Book b)' | 1 | 'b.`title), from ignored`' | 'b' | '(select b.`title), from ignored` from Book b)' | Object
'(/* ignore ) and ( select x from y */ select b.title from Book b)' | 1 | 'b.title' | 'b' | '(/* ignore ) and ( select x from y */ select b.title from Book b)' | Object
'(select b.title /* ), ignored */ from Book b)' | 1 | 'b.title /* ), ignored */' | 'b' | '(select b.title /* ), ignored */ from Book b)' | Object
'(select b.height--1 from Book b)' | 1 | 'b.height--1' | 'b' | '(select b.height--1 from Book b)' | Object
'(select b.title, b.author from Book b)' | 2 | 'b.title, b.author' | 'b' | '(select b.title, b.author from Book b)' | Object[]
'(select count(b) from Book b)' | 1 | 'count(b)' | 'b' | '(select count(b) from Book b)' | null
'(select distinct b from Book b)' | 1 | 'b' | 'b' | '(select distinct b from Book b)' | Book
'(select Book from Book)' | 1 | 'book' | null | '(select Book from Book)' | Book
'(select b from Book b where b.id in (select a.id from Author a))' | 1 | 'b' | 'b' | '(select b from Book b where b.id in (select a.id from Author a))' | Book
'(select b.title from Book b) limit 1' | 1 | 'b.title' | 'b' | '(select b.title from Book b) limit 1' | Object
'(select b.title from Book b) unionized (select a.name from Author a)' | 1 | 'b.title' | 'b' | '(select b.title from Book b) unionized (select a.name from Author a)' | Object
'' | 0 | null | null | '' | Book
}

Expand Down
Loading