Skip to content
Merged
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 @@ -321,6 +321,46 @@ static int lastTopLevelOrderBy(String sql) {
return lastFound;
}

/**
* Find the index of the top-level (non-nested) "select" keyword in sql. Used to detect if sql
* starts with a WITH clause (CTE) header - SQL Server does not support a WITH clause nested
* inside a subquery/derived table, so it must be hoisted in front of a wrapping SELECT
* (count/exists) rather than wrapped along with the rest of the query.
* <p>
* Returns 0 if there is no leading WITH clause (sql starts directly with SELECT), or -1 if no
* top-level SELECT is found at all.
*/
static int topLevelSelectStart(String sql) {
int depth = 0;
int len = sql.length();
for (int i = 0; i < len; i++) {
char c = sql.charAt(i);
if (c == '(') {
depth++;
} else if (c == ')') {
depth--;
} else if (depth == 0 && sql.regionMatches(true, i, "select", 0, 6)
&& (i == 0 || !Character.isLetterOrDigit(sql.charAt(i - 1)))
&& (i + 6 == len || !Character.isLetterOrDigit(sql.charAt(i + 6)))) {
return i;
}
}
return -1;
}

/**
* Split off a leading WITH clause (CTE header) from sql, returning {@code {header, body}} so the
* header can be hoisted in front of a wrapping SELECT. Returns an empty header (unchanged sql as
* the body) when there is no leading WITH clause.
*/
static String[] splitCteHeader(String sql) {
int pos = topLevelSelectStart(sql);
if (pos <= 0) {
return new String[]{"", sql};
}
return new String[]{sql.substring(0, pos), sql.substring(pos)};
}

static String inlineSqlCommentLabel(String label, ProfileLocation profileLocation, boolean secondary, String simpleName) {
if (label != null) {
return secondary ? label : CQueryPlan.planLabelWithType(label, simpleName);
Expand All @@ -329,18 +369,22 @@ static String inlineSqlCommentLabel(String label, ProfileLocation profileLocatio
}

private String wrapSelectCount(String sql) {
sql = "select count(*) from ( " + sql + ")";
String[] parts = splitCteHeader(sql);
sql = parts[0] + "select count(*) from ( " + parts[1] + ")";
if (selectCountWithAlias) {
sql += " as c";
}
return sql;
}

static String wrapSelectExists(String sql, boolean existsWithCaseWhen, String existsFromClause) {
String[] parts = splitCteHeader(sql);
String header = parts[0];
String body = parts[1];
if (existsWithCaseWhen) {
return "select case when exists(" + sql + ") then 1 else 0 end" + existsFromClause;
return header + "select case when exists(" + body + ") then 1 else 0 end" + existsFromClause;
}
return "select exists(" + sql + ")";
return header + "select exists(" + body + ")";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,63 @@ void countWrap_simpleOrderByIsStripped() {
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
}

@Test
void topLevelSelectStart_noLeadingCte_returnsZero() {
String sql = "select 1 from o_order t0 where t0.id > ?";
assertThat(CQueryBuilder.topLevelSelectStart(sql)).isEqualTo(0);
}

@Test
void topLevelSelectStart_leadingCte_findsOuterSelect() {
// The only depth-0 "select" is the outer query - the CTE body's "select" is nested in parens.
String sql = "with order_totals as (" +
" select o.id as order_id," +
" sum(d.order_qty * d.unit_price) as total_amount" +
" from o_order o" +
" join o_order_detail d on d.order_id = o.id" +
" group by o.id" +
")" +
" select order_id, total_amount" +
" from order_totals" +
" where total_amount > ?";

int pos = CQueryBuilder.topLevelSelectStart(sql);
assertThat(sql.substring(pos)).startsWith("select order_id, total_amount");
}

/**
* SQL Server does not support a WITH clause (CTE) nested inside a subquery/derived table - see
* https://github.com/ebean-orm/ebean/issues/3848 (findCount() wraps raw sql in "select count(*)
* from ( ... )" which breaks when the raw sql is a CTE). The CTE header must be hoisted in front
* of the wrapping SELECT.
*/
@Test
void splitCteHeader_hoistsLeadingWithClause() {
String sql = "with order_totals as (" +
" select o.id as order_id," +
" sum(d.order_qty * d.unit_price) as total_amount" +
" from o_order o" +
" join o_order_detail d on d.order_id = o.id" +
" group by o.id" +
")" +
" select order_id, total_amount" +
" from order_totals" +
" where total_amount > ?";

String[] parts = CQueryBuilder.splitCteHeader(sql);
assertThat(parts[0] + parts[1]).isEqualTo(sql);
assertThat(parts[0]).startsWith("with order_totals as (").endsWith(") ");
assertThat(parts[1]).isEqualTo("select order_id, total_amount from order_totals where total_amount > ?");
}

@Test
void splitCteHeader_noCte_returnsEmptyHeader() {
String sql = "select 1 from o_order t0 where t0.id > ?";
String[] parts = CQueryBuilder.splitCteHeader(sql);
assertThat(parts[0]).isEmpty();
assertThat(parts[1]).isEqualTo(sql);
}

@Test
void wrapSelectExists_default_usesScalarExists() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", false, "");
Expand Down
Loading