Skip to content

Commit

Permalink
Bug: DelayedLoadRelations uses inconsistent quoting for string-litera…
Browse files Browse the repository at this point in the history
…ls. (#95)

* Bug: DelayedLoadRelations uses inconsistent quoting for string-literals.

If using HQL on an Entity that is implicitly joined to other Entities via @OnetoOne, @onetomany, @manytoone, or @manytomany,
and an ID value has a single-quote (') in it, the HQL-escaping ('') is not unescaped, and it gets escaped into the dialect,
resulting in (\\'\\'), and a query that fails to join.

Example debug statements:
===
[trace] session.d:1207:delayedLoadRelations unknownKeys = [Bucky O'Hare]
[trace] session.d:1208:delayedLoadRelations createCommaSeparatedKeyList(unknownKeys) = 'Bucky O''Hare'
                                                               extra quote is being added here ^
[trace] session.d:1290:listObjects SQL: SELECT _t1.contract_code, ... FROM finance_contract_refs AS _t1 WHERE _t1.contract_code IN ( 'Bucky O\\'\\'Hare')
===

The fix is to modify the HQL Parser to unescape ('') as ('), and then let specific dialects re-escape the ('). PostgreSQL, by default,
does not support C-style escape characters unless quoted as E'hello\nthere\n' (note the leading (E)). However, all dialects support
the SQL standard ('') syntax.

* Remove debug output.

* Add another quoting test with the string literal directly in the query rather than a parameter.
  • Loading branch information
vnayar authored Nov 7, 2024
1 parent 7f23495 commit 223a71f
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 4 deletions.
19 changes: 19 additions & 0 deletions hdtest/source/generaltest.d
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,24 @@ class GeneralTest : HibernateTest {
assert(allUsers.length == 2); // Should only be 2 users now
}
}

@Test("quote escape test")
void quoteEscapeTest() {
Session sess = sessionFactory.openSession();
scope(exit) sess.close();

auto a1 = new Asset();
a1.name = "Bucky O'Hare";
int id = sess.save(a1).get!int;

auto result = sess.createQuery("FROM Asset WHERE name=:Name")
.setParameter("Name", "Bucky O'Hare")
.list!Asset();
assert(result.length == 1);

result = sess.createQuery("FROM Asset WHERE name='Bucky O''Hare'")
.list!Asset();
assert(result.length == 1);
}
}

1 change: 0 additions & 1 deletion source/hibernated/annotations.d
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ struct ManyToMany {
immutable string joinColumn2;
}


unittest {
@Entity
@Table("user")
Expand Down
3 changes: 2 additions & 1 deletion source/hibernated/dialect.d
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ abstract class Dialect {
string res = "'";
foreach(ch; s) {
switch(ch) {
case '\'': res ~= "\\\'"; break;
// All dialects support the SQL-standard way of escaping a (') character via ('').
case '\'': res ~= "''"; break;
case '\"': res ~= "\\\""; break;
case '\\': res ~= "\\\\"; break;
case '\0': res ~= "\\n"; break;
Expand Down
9 changes: 7 additions & 2 deletions source/hibernated/query.d
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,12 @@ class Token {

}

/**
* Converts an HQL string into a series of tokens to be later converted to SQL.
*
* HQL follows the Jakarta Persistence specification, which requires escaping (') characters via ('').
* See: https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#literals
*/
Token[] tokenize(string s) {
Token[] res;
int startpos = 0;
Expand Down Expand Up @@ -1521,11 +1527,10 @@ Token[] tokenize(string s) {
// string constant
i++;
for(int j=i; j<len; j++) {
// In SQL, (') characters are quoted as ('').
// In HQL, (') characters are quoted as (''), so undo that escaping.
if (s[j] == '\'' && j+1 < len && s[j+1] == '\'') {
text ~= s[j];
j++;
text ~= s[j];
}
else if (s[j] != '\'') {
text ~= s[j];
Expand Down

0 comments on commit 223a71f

Please sign in to comment.