This is an automated email from the ASF dual-hosted git repository.
mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new a5c096a09a [CALCITE-5409] Implement BatchNestedLoopJoin for JDBC
a5c096a09a is described below
commit a5c096a09a7ebce1b2e9c674c4562f829b33a4c7
Author: Ulrich Kramer <[email protected]>
AuthorDate: Fri Nov 17 17:14:43 2023 +0100
[CALCITE-5409] Implement BatchNestedLoopJoin for JDBC
---
.../adapter/jdbc/JdbcCorrelationDataContext.java | 63 ++++++++++++++++++++++
...java => JdbcCorrelationDataContextBuilder.java} | 25 ++++-----
.../JdbcCorrelationDataContextBuilderImpl.java | 60 +++++++++++++++++++++
.../calcite/adapter/jdbc/JdbcImplementor.java | 52 ++++++++++++++++--
.../adapter/jdbc/JdbcToEnumerableConverter.java | 11 ++--
.../apache/calcite/rel/rel2sql/SqlImplementor.java | 22 ++++++--
.../calcite/runtime/ResultSetEnumerable.java | 4 +-
.../org/apache/calcite/test/JdbcAdapterTest.java | 57 ++++++++++++++++++++
8 files changed, 265 insertions(+), 29 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContext.java
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContext.java
new file mode 100644
index 0000000000..802e490d83
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContext.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.adapter.jdbc;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.linq4j.QueryProvider;
+import org.apache.calcite.schema.SchemaPlus;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import static java.lang.Integer.parseInt;
+
+/**
+ * A special DataContext which handles correlation variable for batch nested
loop joins.
+ */
+public class JdbcCorrelationDataContext implements DataContext {
+ public static final int OFFSET = Integer.MAX_VALUE - 10000;
+
+ private final DataContext delegate;
+ private final Object[] parameters;
+
+ public JdbcCorrelationDataContext(DataContext delegate, Object[] parameters)
{
+ this.delegate = delegate;
+ this.parameters = parameters;
+ }
+
+ @Override public @Nullable SchemaPlus getRootSchema() {
+ return delegate.getRootSchema();
+ }
+
+ @Override public JavaTypeFactory getTypeFactory() {
+ return delegate.getTypeFactory();
+ }
+
+ @Override public QueryProvider getQueryProvider() {
+ return delegate.getQueryProvider();
+ }
+
+ @Override public @Nullable Object get(String name) {
+ if (name.startsWith("?")) {
+ int index = parseInt(name.substring(1));
+ if (index >= OFFSET && index < OFFSET + parameters.length) {
+ return parameters[index - OFFSET];
+ }
+ }
+ return delegate.get(name);
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilder.java
similarity index 61%
copy from
core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
copy to
core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilder.java
index 0695a77ba8..1877fdbd1e 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
+++
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilder.java
@@ -16,22 +16,17 @@
*/
package org.apache.calcite.adapter.jdbc;
-import org.apache.calcite.adapter.java.JavaTypeFactory;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.rel2sql.RelToSqlConverter;
-import org.apache.calcite.sql.SqlDialect;
-import org.apache.calcite.util.Util;
+import org.apache.calcite.rel.core.CorrelationId;
+
+import java.lang.reflect.Type;
/**
- * State for generating a SQL statement.
+ * An interface to collect all correlation variables
+ * required to create a JdbcCorrelationDataContext.
*/
-public class JdbcImplementor extends RelToSqlConverter {
- public JdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory) {
- super(dialect);
- Util.discard(typeFactory);
- }
-
- public Result implement(RelNode node) {
- return dispatch(node);
- }
+interface JdbcCorrelationDataContextBuilder {
+ /**
+ * Collect a correlation variable.
+ */
+ int add(CorrelationId id, int ordinal, Type type);
}
diff --git
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilderImpl.java
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilderImpl.java
new file mode 100644
index 0000000000..cc907596fa
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcCorrelationDataContextBuilderImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.adapter.jdbc;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.adapter.enumerable.EnumerableRelImplementor;
+import org.apache.calcite.linq4j.tree.BlockBuilder;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.Types;
+import org.apache.calcite.rel.core.CorrelationId;
+
+import com.google.common.collect.ImmutableList;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Type;
+
+/**
+ * An implementation class of JdbcCorrelationDataContext.
+ */
+public class JdbcCorrelationDataContextBuilderImpl implements
JdbcCorrelationDataContextBuilder {
+ private static final Constructor NEW =
+ Types.lookupConstructor(JdbcCorrelationDataContext.class,
DataContext.class, Object[].class);
+ private final ImmutableList.Builder<Expression> parameters = new
ImmutableList.Builder<>();
+ private int offset = JdbcCorrelationDataContext.OFFSET;
+ private final EnumerableRelImplementor implementor;
+ private final BlockBuilder builder;
+ private final Expression dataContext;
+
+ public JdbcCorrelationDataContextBuilderImpl(EnumerableRelImplementor
implementor,
+ BlockBuilder builder, Expression dataContext) {
+ this.implementor = implementor;
+ this.builder = builder;
+ this.dataContext = dataContext;
+ }
+
+ @Override public int add(CorrelationId id, int ordinal, Type type) {
+
parameters.add(implementor.getCorrelVariableGetter(id.getName()).field(builder,
ordinal, type));
+ return offset++;
+ }
+
+ public Expression build() {
+ return Expressions.new_(NEW, dataContext,
+ Expressions.newArrayInit(Object.class, 1, parameters.build()));
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
index 0695a77ba8..1485c4788e 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcImplementor.java
@@ -18,20 +18,66 @@
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.CorrelationId;
import org.apache.calcite.rel.rel2sql.RelToSqlConverter;
+import org.apache.calcite.rel.rel2sql.SqlImplementor;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.sql.SqlDialect;
-import org.apache.calcite.util.Util;
+import org.apache.calcite.sql.SqlDynamicParam;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import java.lang.reflect.Type;
+import java.util.List;
/**
* State for generating a SQL statement.
*/
public class JdbcImplementor extends RelToSqlConverter {
- public JdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory) {
+
+ private final JdbcCorrelationDataContextBuilder dataContextBuilder;
+ private final JavaTypeFactory typeFactory;
+
+ JdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory,
+ JdbcCorrelationDataContextBuilder dataContextBuilder) {
super(dialect);
- Util.discard(typeFactory);
+ this.typeFactory = typeFactory;
+ this.dataContextBuilder = dataContextBuilder;
+ }
+
+ public JdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory) {
+ this(dialect, typeFactory, new JdbcCorrelationDataContextBuilder() {
+ private int counter = 1;
+ @Override public int add(CorrelationId id, int ordinal, Type type) {
+ return counter++;
+ }
+ });
}
public Result implement(RelNode node) {
return dispatch(node);
}
+
+ @Override protected Context getAliasContext(RexCorrelVariable variable) {
+ Context context = correlTableMap.get(variable.id);
+ if (context != null) {
+ return context;
+ }
+ List<RelDataTypeField> fieldList = variable.getType().getFieldList();
+ // We need to provide a context which also includes the correlation
variables
+ // as dynamic parameters.
+ return new Context(dialect, fieldList.size()) {
+ @Override public SqlNode field(int ordinal) {
+ RelDataTypeField field = fieldList.get(ordinal);
+ return new SqlDynamicParam(
+ dataContextBuilder.add(variable.id, ordinal,
+ typeFactory.getJavaClass(field.getType())), SqlParserPos.ZERO);
+ }
+
+ @Override public SqlImplementor implementor() {
+ return JdbcImplementor.this;
+ }
+ };
+ }
}
diff --git
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
index 11b676bd8a..2a68d147aa 100644
---
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
+++
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
@@ -105,7 +105,9 @@ protected JdbcToEnumerableConverter(
final JdbcConvention jdbcConvention =
(JdbcConvention) requireNonNull(child.getConvention(),
() -> "child.getConvention() is null for " + child);
- SqlString sqlString = generateSql(jdbcConvention.dialect);
+ JdbcCorrelationDataContextBuilderImpl dataContextBuilder =
+ new JdbcCorrelationDataContextBuilderImpl(implementor, builder0,
DataContext.ROOT);
+ SqlString sqlString = generateSql(jdbcConvention.dialect,
dataContextBuilder);
String sql = sqlString.getSql();
if (CalciteSystemProperty.DEBUG.value()) {
System.out.println("[" + sql + "]");
@@ -179,7 +181,7 @@ protected JdbcToEnumerableConverter(
Expressions.call(BuiltInMethod.CREATE_ENRICHER.method,
Expressions.newArrayInit(Integer.class, 1,
toIndexesTableExpression(sqlString)),
- DataContext.ROOT));
+ dataContextBuilder.build()));
enumerable =
builder0.append("enumerable",
@@ -356,10 +358,11 @@ private static String jdbcGetMethod(@Nullable Primitive
primitive) {
: "get" + SqlFunctions.initcap(castNonNull(primitive.primitiveName));
}
- private SqlString generateSql(SqlDialect dialect) {
+ private SqlString generateSql(SqlDialect dialect,
+ JdbcCorrelationDataContextBuilder dataContextBuilder) {
final JdbcImplementor jdbcImplementor =
new JdbcImplementor(dialect,
- (JavaTypeFactory) getCluster().getTypeFactory());
+ (JavaTypeFactory) getCluster().getTypeFactory(),
dataContextBuilder);
final JdbcImplementor.Result result =
jdbcImplementor.visitRoot(this.getInput());
return result.asStatement().toSqlString(dialect);
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index c4fa3897ef..470c9ada2e 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -16,6 +16,7 @@
*/
package org.apache.calcite.rel.rel2sql;
+import org.apache.calcite.adapter.jdbc.JdbcCorrelationDataContext;
import org.apache.calcite.config.CalciteSystemProperty;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.linq4j.tree.Expressions;
@@ -675,8 +676,12 @@ public SqlNode toSql(@Nullable RexProgram program, RexNode
rex) {
final RexCorrelVariable variable = (RexCorrelVariable)
referencedExpr;
final Context correlAliasContext = getAliasContext(variable);
final RexFieldAccess lastAccess =
requireNonNull(accesses.pollLast());
- sqlIdentifier = (SqlIdentifier) correlAliasContext
+ SqlNode node = correlAliasContext
.field(lastAccess.getField().getIndex());
+ if (node instanceof SqlDynamicParam) {
+ return node;
+ }
+ sqlIdentifier = (SqlIdentifier) node;
break;
case ROW:
case ITEM:
@@ -763,6 +768,11 @@ public SqlNode toSql(@Nullable RexProgram program, RexNode
rex) {
case DYNAMIC_PARAM:
final RexDynamicParam caseParam = (RexDynamicParam) rex;
+ if (caseParam.getIndex() >= JdbcCorrelationDataContext.OFFSET) {
+ throw new AssertionError("More than "
+ + JdbcCorrelationDataContext.OFFSET
+ + " dynamic parameters used in query");
+ }
return new SqlDynamicParam(caseParam.getIndex(), POS);
case IN:
@@ -1543,6 +1553,12 @@ public static SqlNode toSql(RexLiteral literal) {
}
}
+ protected Context getAliasContext(RexCorrelVariable variable) {
+ return requireNonNull(
+ correlTableMap.get(variable.id),
+ () -> "variable " + variable.id + " is not found");
+ }
+
/** Simple implementation of {@link Context} that cannot handle sub-queries
* or correlations. Because it is so simple, you do not need to create a
* {@link SqlImplementor} or {@link org.apache.calcite.tools.RelBuilder}
@@ -1572,9 +1588,7 @@ protected abstract class BaseContext extends Context {
}
@Override protected Context getAliasContext(RexCorrelVariable variable) {
- return requireNonNull(
- correlTableMap.get(variable.id),
- () -> "variable " + variable.id + " is not found");
+ return SqlImplementor.this.getAliasContext(variable);
}
@Override public SqlImplementor implementor() {
diff --git
a/core/src/main/java/org/apache/calcite/runtime/ResultSetEnumerable.java
b/core/src/main/java/org/apache/calcite/runtime/ResultSetEnumerable.java
index 2f99177352..5d60b6552f 100644
--- a/core/src/main/java/org/apache/calcite/runtime/ResultSetEnumerable.java
+++ b/core/src/main/java/org/apache/calcite/runtime/ResultSetEnumerable.java
@@ -17,7 +17,6 @@
package org.apache.calcite.runtime;
import org.apache.calcite.DataContext;
-import org.apache.calcite.avatica.SqlType;
import org.apache.calcite.linq4j.AbstractEnumerable;
import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Enumerator;
@@ -203,8 +202,7 @@ public static PreparedStatementEnricher
createEnricher(Integer[] indexes,
private static void setDynamicParam(PreparedStatement preparedStatement,
int i, @Nullable Object value) throws SQLException {
if (value == null) {
- // TODO: use proper type instead of ANY
- preparedStatement.setObject(i, null, SqlType.ANY.id);
+ preparedStatement.setNull(i, Types.NULL);
} else if (value instanceof Timestamp) {
preparedStatement.setTimestamp(i, (Timestamp) value);
} else if (value instanceof Time) {
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 5ecb81ba11..ffda7378f7 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -16,11 +16,16 @@
*/
package org.apache.calcite.test;
+import org.apache.calcite.adapter.enumerable.EnumerableRules;
+import org.apache.calcite.adapter.java.ReflectiveSchema;
import org.apache.calcite.config.CalciteConnectionProperty;
import org.apache.calcite.config.Lex;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.runtime.Hook;
import org.apache.calcite.test.CalciteAssert.AssertThat;
import org.apache.calcite.test.CalciteAssert.DatabaseInstance;
import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
+import org.apache.calcite.test.schemata.hr.HrSchema;
import org.apache.calcite.util.Smalls;
import org.apache.calcite.util.TestUtil;
@@ -35,6 +40,7 @@
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -1450,6 +1456,57 @@ private LockWrapper exclusiveCleanDb(Connection c)
throws SQLException {
+ "GROUP BY \"t2\".\"DNAME\", \"t2\".\"DNAME0\"")
.runs();
}
+
+ /**
+ * Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-4188">[CALCITE-4188]
+ * Support EnumerableBatchNestedLoopJoin for JDBC</a>. */
+ @Test void testBatchNestedLoopJoinPlan() {
+ final String sql = "SELECT *\n"
+ + "FROM \"s\".\"emps\" A\n"
+ + "LEFT OUTER JOIN \"foodmart\".\"store\" B ON A.\"empid\" =
B.\"store_id\"";
+ final String explain = "JdbcFilter(condition=[OR(=($cor0.empid0, $0),
=($cor1.empid0, $0)";
+ final String jdbcSql = "SELECT *\n"
+ + "FROM \"foodmart\".\"store\"\n"
+ + "WHERE ? = \"store_id\" OR (? = \"store_id\" OR ? = \"store_id\") OR
(? = \"store_id\" OR"
+ + " (? = \"store_id\" OR ? = \"store_id\")) OR (? = \"store_id\" OR (?
= \"store_id\" OR ? "
+ + "= \"store_id\") OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\"))) OR (? ="
+ + " \"store_id\" OR (? = \"store_id\" OR ? = \"store_id\") OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\")) OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\") OR (? = \"store_id\" OR ? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\")))) OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\") OR (? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\")) OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\") OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\"))) OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\") OR (? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\")) OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\") OR (? = \"store_id\" OR ? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\"))))) OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\") OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\")) OR (? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\") OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\"))) OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\") OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\")) OR (? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\") OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\")))) OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\") OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\")) OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\") OR (? = "
+ + "\"store_id\" OR (? = \"store_id\" OR ? = \"store_id\"))) OR (? =
\"store_id\" OR (? = "
+ + "\"store_id\" OR ? = \"store_id\") OR (? = \"store_id\" OR (? =
\"store_id\" OR ? = "
+ + "\"store_id\")) OR (? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\") OR (? = "
+ + "\"store_id\" OR ? = \"store_id\" OR (? = \"store_id\" OR ? =
\"store_id\"))))))";
+ CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
+ .withSchema("s", new ReflectiveSchema(new HrSchema()))
+ .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+
planner.addRule(EnumerableRules.ENUMERABLE_BATCH_NESTED_LOOP_JOIN_RULE);
+ })
+ .query(sql)
+ .explainContains(explain)
+ .runs()
+ .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.HSQLDB
+ || CalciteAssert.DB == DatabaseInstance.POSTGRESQL)
+ .planHasSql(jdbcSql)
+ .returnsCount(4);
+ }
+
/** Acquires a lock, and releases it when closed. */
static class LockWrapper implements AutoCloseable {
private final Lock lock;