feat: Add query.setHint() to support sql hint as inline comment in select queries (#3365)

* feat: Add query.setHint() to support sql hint as inline comment in select queries

This is for ORM queries only.

An ORM query like:

    new QCustomer()
      .setHint("FirstRows")
      .select(QCustomer.Alias.id, QCustomer.Alias.name)
      .findList();

Produces SQL that includes the hint as an inline comment like:

  select /*+ FirstRows */ t0.id, t0.name
  from customer t0

* Fix test QOrderTest
This commit is contained in:
Rob Bygrave
2024-03-20 21:48:43 +13:00
committed by GitHub
parent 8b858a073a
commit 540ceab96f
11 changed files with 94 additions and 7 deletions
@@ -1601,6 +1601,14 @@ public interface Query<T> extends CancelableQuery {
*/
Query<T> setLabel(String label);
/**
* Set a SQL query hint.
* <p>
* This results in an inline comment that immediately follows
* after the select keyword in the form: {@code /*+ hint *\/ }
*/
Query<T> setHint(String hint);
/**
* Set to true if this query should execute against the doc store.
* <p>
@@ -776,4 +776,8 @@ public class DatabasePlatform {
}
return "/* " + label + " */ ";
}
public String inlineSqlHint(String hint) {
return "/*+ " + hint + " */ ";
}
}
@@ -226,6 +226,11 @@ public interface SpiQuery<T> extends Query<T>, SpiQueryFetch, TxnProfileEventCod
*/
ProfileLocation profileLocation();
/**
* Return the SQL hint to include in the query.
*/
String hint();
/**
* Return the label set on the query.
*/
@@ -226,7 +226,7 @@ public final class BatchControl {
void executeNow(ArrayList<PersistRequest> list) throws BatchedSqlException {
boolean old = transaction.isFlushOnQuery();
transaction.setFlushOnQuery(false);
// disable flush on query due transsaction callbacks
// disable flush on query due transaction callbacks
try {
for (int i = 0; i < list.size(); i++) {
if (i % batchSize == 0) {
@@ -595,7 +595,7 @@ final class CQueryBuilder {
}
private void appendSelectDistinct() {
sb.append("select ").append(inlineSqlComment());
sb.append("select ").append(hint()).append(inlineSqlComment());
if (distinct && !countSingleAttribute) {
if (request.isInlineCountDistinct()) {
sb.append("count(");
@@ -608,6 +608,11 @@ final class CQueryBuilder {
}
}
private String hint() {
String hint = query.hint();
return hint == null ? "" : dbPlatform.inlineSqlHint(hint);
}
private String inlineSqlComment() {
if (!includeLabelInSql) {
return "";
@@ -133,6 +133,11 @@ final class DefaultFetchGroupQuery<T> implements SpiFetchGroupQuery<T>, SpiQuery
return this;
}
@Override
public Query<T> setHint(String hint) {
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
}
// Everything else deemed invalid
@Override
@@ -57,6 +57,7 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
private ProfilingListener profilingListener;
private Type type;
private String label;
private String hint;
private Mode mode = Mode.NORMAL;
private boolean usingFuture;
private Object tenantId;
@@ -235,6 +236,11 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
return label;
}
@Override
public final String hint() {
return hint;
}
@Override
public final String planLabel() {
if (label != null) {
@@ -258,6 +264,12 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
return this;
}
@Override
public final Query<T> setHint(String hint) {
this.hint = hint;
return this;
}
@Override
public final boolean isAutoTunable() {
return nativeSql == null && beanDescriptor.isAutoTunable();
@@ -732,6 +744,7 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
copy.timeout = timeout;
copy.mapKey = mapKey;
copy.id = id;
copy.hint = hint;
copy.label = label;
copy.nativeSql = nativeSql;
copy.useBeanCache = useBeanCache;
@@ -1069,6 +1082,9 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
if (manualId) {
sb.append("/md");
}
if (hint != null) {
sb.append("/h:").append(hint);
}
if (distinct) {
sb.append("/dt");
}
@@ -29,6 +29,27 @@ public class OrmQueryPlanKeyTest extends BaseTest {
assertSame(query().createQueryPlanKey(), query().createQueryPlanKey());
}
@Test
public void equals_when_hintIsDifferent_expect_different() {
DefaultOrmQuery<Customer> q1 = query();
q1.setHint("a");
assertDifferent(q1, query());
DefaultOrmQuery<Customer> q2 = query();
q2.setHint("b");
assertDifferent(q1, q2);
}
@Test
public void equals_when_hintIsSame() {
DefaultOrmQuery<Customer> q1 = query();
q1.setHint("b");
DefaultOrmQuery<Customer> q2 = query();
q2.setHint("b");
assertSame(q1, q2);
}
@Test
public void equals_when_diffTableJoinNull() {
@@ -912,6 +912,17 @@ public abstract class TQRootBean<T, R> {
return root;
}
/**
* Set a SQL query hint.
* <p>
* This results in an inline comment that immediately follows
* after the select keyword in the form: {@code /*+ hint *\/ }
*/
public R setHint(String hint) {
query.setHint(hint);
return root;
}
/**
* Set the profile location.
* <p>
@@ -79,6 +79,19 @@ class QOrderTest {
DB.delete(customer);
}
@Test
void hint() {
LoggedSql.start();
new QCustomer()
.setHint("FirstRows")
.select(QCustomer.Alias.id, QCustomer.Alias.name)
.findList();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("select /*+ FirstRows */ /* QOrderTest.hint */ t0.id, t0.name from be_customer t0");
}
@Test
void fetchQueryWithBatch() {
LoggedSql.start();
@@ -159,8 +172,7 @@ class QOrderTest {
@Test
void viaFetchGraph_withNested_fetchQuery() {
DB.getDefault();
DB.cacheManager().clearAll();
LoggedSql.start();
final Order found = new QOrder()
@@ -181,8 +193,7 @@ class QOrderTest {
@Test
void viaFetchGraph_withNested_fetchCache() {
DB.getDefault();
DB.cacheManager().clearAll();
// ensure the customer is loaded in the L2 cache
new QCustomer().id.eq(customer.getId()).findOne();
@@ -164,6 +164,7 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
DtoQuery<ContactDto> query = DB.find(Contact.class)
// we must explicitly add the id property for DTO query (if we want it)
.setHint("SomeHint")
.select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName")
.where()
.isNotNull("email")
@@ -183,7 +184,7 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
}
List<String> sql = LoggedSql.stop();
assertSql(sql.get(0)).contains("select /* explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
assertSql(sql.get(0)).contains("select /*+ SomeHint */ /* explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
+ " fullName from contact t0 where t0.email is not null and t0.last_name is not null order by t0.last_name");
}