Compare commits

...
2 Commits
Author SHA1 Message Date
robin.bygrave 56894400f4 Fix tests TestInsertCheckUnique for Oracle and SQL Server 2026-07-14 09:35:25 +12:00
robin.bygrave 32931d6db0 #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0
Fix #3848: exists() generates invalid scalar SQL on SQL Server and Oracle

Query.exists() generated `select exists(<subquery>)`, which is valid on
Postgres/H2/MySQL but rejected by SQL Server and Oracle since both only
support EXISTS as a predicate, not as a directly selectable scalar boolean
expression.                                                                                                                                                                              ┃
                                                                                                                                                                                            ┃
Add DatabasePlatform.existsWithCaseWhen/existsFromClause capability flags
and use them in CQueryBuilder.wrapSelectExists() to generate
`select case when exists(<subquery>) then 1 else 0 end` on platforms that                                                                                                                ┃
need it, with an additional ` from dual` suffix for Oracle (which requires                                                                                                               ┃
a FROM clause on every select). CQueryExists reads the boolean result via                                                                                                                ┃
ResultSet.getBoolean(1), which correctly interprets the resulting 0/1 int.
2026-07-14 09:15:41 +12:00
8 changed files with 88 additions and 9 deletions
@@ -162,6 +162,18 @@ public class DatabasePlatform {
protected boolean selectCountWithAlias;
protected boolean selectCountWithColumnAlias;
/**
* Set true for platforms where {@code exists(...)} can only be used as a predicate
* and not as a directly selectable scalar boolean expression (e.g. SQL Server, Oracle).
*/
protected boolean existsWithCaseWhen;
/**
* Clause appended after the {@code case when exists(...) then 1 else 0 end} exists query
* for platforms that require a FROM clause on every select (e.g. {@code from dual} on Oracle).
*/
protected String existsFromClause = "";
/**
* If set then use the FORWARD ONLY hint when creating ResultSets for
* findIterate() and findVisit().
@@ -660,6 +672,21 @@ public class DatabasePlatform {
return selectCountWithColumnAlias;
}
/**
* Return true if a scalar boolean {@code exists(...)} expression is not supported
* as a select expression and needs to be wrapped as {@code case when exists(...) then 1 else 0 end}.
*/
public boolean existsWithCaseWhen() {
return existsWithCaseWhen;
}
/**
* Return the clause to append after the exists case-when wrapping (e.g. {@code from dual} on Oracle).
*/
public String existsFromClause() {
return existsFromClause;
}
public String completeSql(String sql, Query<?> query) {
if (query.isForUpdate()) {
@@ -336,7 +336,10 @@ final class CQueryBuilder {
return sql;
}
private String wrapSelectExists(String sql) {
static String wrapSelectExists(String sql, boolean existsWithCaseWhen, String existsFromClause) {
if (existsWithCaseWhen) {
return "select case when exists(" + sql + ") then 1 else 0 end" + existsFromClause;
}
return "select exists(" + sql + ")";
}
@@ -366,7 +369,7 @@ final class CQueryBuilder {
}
SqlLimitResponse s = buildSql("select 1", request, predicates, sqlTree);
String sql = wrapSelectExists(s.getSql());
String sql = wrapSelectExists(s.getSql(), dbPlatform.existsWithCaseWhen(), dbPlatform.existsFromClause());
queryPlan = new CQueryPlan(request, sql, sqlTree.plan(), predicates.logWhereSql());
request.putQueryPlan(queryPlan);
@@ -117,6 +117,31 @@ class CQueryBuilderTest {
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
}
@Test
void wrapSelectExists_default_usesScalarExists() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", false, "");
assertThat(sql).isEqualTo("select exists(select 1 from o_order t0 where t0.id > ?)");
}
/**
* SQL Server does not support exists(...) as a directly selectable scalar
* boolean expression - see https://github.com/ebean-orm/ebean/issues/3848
*/
@Test
void wrapSelectExists_existsWithCaseWhen_wrapsAsCaseWhen() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, "");
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end");
}
/**
* Oracle also requires a FROM clause on every select (from dual) - see https://github.com/ebean-orm/ebean/issues/3848
*/
@Test
void wrapSelectExists_existsWithCaseWhenAndFromClause_appendsFromClause() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, " from dual");
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end from dual");
}
@Test
void inlineSqlCommentLabel_rootExplicitLabel_prefixesBeanType() {
String label = CQueryBuilder.inlineSqlCommentLabel("fetchMachineFleets", null, false, "COrganisationMachine");
@@ -112,7 +112,9 @@ public class TestInsertCheckUnique extends BaseTestCase {
assertThat(DB.checkUniqueness(doc2).toString()).contains("title");
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("select exists(select 1 from document t0 where t0.title = ?)");
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from document t0 where t0.title = ?)");
}
}
@@ -135,8 +137,10 @@ public class TestInsertCheckUnique extends BaseTestCase {
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
}
DB.save(basic);
try {
// reload from database
@@ -147,8 +151,10 @@ public class TestInsertCheckUnique extends BaseTestCase {
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?)");
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?)");
}
// and check again - expect to hit query cache
LoggedSql.start();
@@ -187,8 +193,10 @@ public class TestInsertCheckUnique extends BaseTestCase {
assertThat(DB.getDefault().checkUniqueness(basic, null, false, true)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
}
DB.save(basic);
try (Transaction txn = DB.beginTransaction()) {
// reload from database
@@ -33,6 +33,8 @@ public class OraclePlatform extends DatabasePlatform {
this.dbDefaultValue.setTrue("1");
this.dbDefaultValue.setNow("current_timestamp");
this.likeClauseRaw = "like ?";
this.existsWithCaseWhen = true;
this.existsFromClause = " from dual";
this.exceptionTranslator =
new SqlErrorCodes()
@@ -45,4 +45,11 @@ class OraclePlatformTest {
DbPlatformType dbType = platform.dbTypeMap().get(DbPlatformType.UUID);
assertThat(dbType.renderType(0, 0)).isEqualTo("raw(16)");
}
@Test
void existsWithCaseWhen_trueForOracle() {
OraclePlatform platform = new OraclePlatform();
assertThat(platform.existsWithCaseWhen()).isTrue();
assertThat(platform.existsFromClause()).isEqualTo(" from dual");
}
}
@@ -26,6 +26,7 @@ abstract class SqlServerBasePlatform extends DatabasePlatform {
this.idInExpandedForm = true;
this.selectCountWithAlias = true;
this.selectCountWithColumnAlias = true;
this.existsWithCaseWhen = true;
this.sqlLimiter = new SqlServerSqlLimiter();
this.basicSqlLimiter = new SqlServerBasicSqlLimiter();
this.historySupport = new SqlServerHistorySupport();
@@ -40,6 +40,12 @@ class SqlServerPlatformTest {
assertEquals(dbPlatform.unQuote("[firstName]"), "firstName");
}
@Test
public void existsWithCaseWhen_trueForSqlServer() {
SqlServer17Platform dbPlatform = new SqlServer17Platform();
assertEquals(dbPlatform.existsWithCaseWhen(), true);
}
@Test
public void defaultTypesForDecimalAndVarchar() {
DatabasePlatform dbPlatform = new DatabasePlatform();