This is an automated email from the ASF dual-hosted git repository.
korlov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new cd3c790b03 IGNITE-22655 Sql. Improve error message for incorrect SQL
string (#4108)
cd3c790b03 is described below
commit cd3c790b0328b208b3262107c78595a41aef5c12
Author: Max Zhuravkov <[email protected]>
AuthorDate: Wed Jul 24 15:33:43 2024 +0300
IGNITE-22655 Sql. Improve error message for incorrect SQL string (#4108)
---
.../internal/sql/engine/ItDataTypesTest.java | 3 +-
.../sql/identifiers/test_long_identifiers.test | 57 ++++++++++++++------
.../sql/engine/sql/IgniteSqlCreateTableOption.java | 15 ------
.../internal/sql/engine/sql/IgniteSqlParser.java | 63 ++++++++++++++++++++++
.../internal/sql/engine/sql/IgniteSqlUpdate.java | 3 +-
.../sql/engine/sql/IgniteSqlZoneOption.java | 15 ------
6 files changed, 108 insertions(+), 48 deletions(-)
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
index 98b5004017..d64f2d9599 100644
---
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
@@ -240,7 +240,8 @@ public class ItDataTypesTest extends BaseSqlIntegrationTest
{
assertQuery(format("SELECT * FROM tbl WHERE v NOT IN ('{}' - '1',
'0')", strUpper)).returns(checkReturn).check();
assertQuery("SELECT * FROM tbl WHERE v NOT IN (? - '1',
'0')").withParam(checkReturn).returns(checkReturn).check();
- // TODO: https://issues.apache.org/jira/browse/IGNITE-22519 Comparison
on BIGINT cast brings overflow
+ // TODO: https://issues.apache.org/jira/browse/IGNITE-20889
+ // Sql. Change type derivation for literals and expressions for
overflowed BIGINT
// BigDecimal moreThanUpperBoundExact = new
BigDecimal(strUpper).add(new BigDecimal(1));
// assertQuery(format("SELECT * FROM tbl WHERE v < {}::DECIMAL(19,
0)", moreThanUpperBoundExact)).returns(checkReturn).check();
diff --git
a/modules/sql-engine/src/integrationTest/sql/identifiers/test_long_identifiers.test
b/modules/sql-engine/src/integrationTest/sql/identifiers/test_long_identifiers.test
index 84ea70023c..e39c6af3d8 100644
---
a/modules/sql-engine/src/integrationTest/sql/identifiers/test_long_identifiers.test
+++
b/modules/sql-engine/src/integrationTest/sql/identifiers/test_long_identifiers.test
@@ -7,6 +7,12 @@
statement ok
PRAGMA enable_verification
+statement error: Non-query expression encountered in illegal context.
+tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
+
+statement error: Non-query expression encountered in illegal context.
+SELECT 1;
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
+
# Create table with short identifiers for test simplicity purpose
statement ok
CREATE TABLE t (id INTEGER, val INTEGER, PRIMARY KEY (id))
@@ -15,14 +21,25 @@ CREATE TABLE t (id INTEGER, val INTEGER, PRIMARY KEY (id))
statement ok
CREATE TABLE
tableName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
(keyColumnName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
INTEGER,
valueColumnName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
INTEGER, PRIMARY KEY
(keyColumnName_veryLooooooooooooooooooooooooooooooooooooooooo [...]
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
CREATE TABLE
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
(key INTEGER, val INTEGER, PRIMARY KEY (key));
+statement error: Length of identifier
+SELECT * from t as
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
+
+statement error: Length of identifier
+SELECT * FROM t where 1 = (SELECT 1 from t as
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
LIMIT 1);
+
+statement error: Length of identifier
+CREATE TABLE ttt (key
type0Name_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters,
val INTEGER, PRIMARY KEY (key));
+
+statement error: Length of identifier
+CREATE TABLE ttt (key
type0Name_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
ARRAY, val INTEGER, PRIMARY KEY (key));
# Rename table with long identifiers
skipif ignite3
# Ignored: https://issues.apache.org/jira/browse/IGNITE-19484
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
ALTER TABLE t RENAME TO
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
skipif ignite3
@@ -40,7 +57,7 @@ ALTER TABLE
tableName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooo
statement ok
ALTER TABLE t ADD COLUMN
(columnName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
INTEGER);
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
ALTER TABLE t ADD COLUMN
(columnName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
INTEGER);
statement ok
@@ -50,7 +67,7 @@ ALTER TABLE t DROP COLUMN
(columnName_veryLooooooooooooooooooooooooooooooooooooo
# Alter table rename column with long identifier
skipif ignite3
# Ignored: https://issues.apache.org/jira/browse/IGNITE-19485
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
ALTER TABLE t RENAME COLUMN val TO
columnName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
skipif ignite3
@@ -86,10 +103,10 @@ SELECT
columnAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooo
1
2
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
FROM (VALUES (1), (2))
t(columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters);
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT col FROM (VALUES (1), (2))
tableAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters(col);
query I
@@ -102,10 +119,16 @@ SELECT 1
columnAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooo
----
1
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT 1 as
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
+SELECT * FROM (VALUES (1))
t(columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters);
+
+statement error: Length of identifier
+SELECT * FROM (VALUES (1))
tableAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters(c);
+
+statement error: Length of identifier
SELECT 1
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
# Long column aliases with subquery
@@ -119,10 +142,10 @@ SELECT
columnAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooo
----
1
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
FROM (SELECT 1 as
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters);
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT
columnAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
FROM (SELECT 1
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters);
query I
@@ -137,7 +160,7 @@ SELECT * FROM (SELECT 1) as
tableAlias_veryLoooooooooooooooooooooooooooooooooooo
----
1
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT * FROM (SELECT 1) as
tableAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
query I
@@ -159,7 +182,7 @@ SELECT id as
columnAlias_veryLoooooooooooooooooooooooooooooooooooooooooooooooooo
2
1
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT id as
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
FROM t ORDER BY
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
DESC;
@@ -174,9 +197,11 @@ SELECT val as
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooo
----
1 3
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
SELECT id as
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
FROM t GROUP BY
columnAlias_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
+statement error: Length of identifier
+SELECT
functionName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters(1,
2, 3)
# Long identifier in WHERE clause
query I
@@ -192,7 +217,7 @@ CREATE INDEX
indexName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooo
statement ok
DROP INDEX
indexName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters;
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
CREATE INDEX
indexName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
on t (val);
@@ -208,7 +233,7 @@ DROP TABLE
tableName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooo
statement ok
CREATE ZONE
zoneName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
WITH STORAGE_PROFILES='default', PARTITIONS=1, REPLICAS=3;
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
CREATE ZONE
zoneName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters
WITH STORAGE_PROFILES='default', PARTITIONS=1, REPLICAS=3;
statement ok
@@ -217,7 +242,7 @@ ALTER ZONE
zoneName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooo
statement ok
ALTER ZONE
zoneName_veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf128Characters
RENAME TO zoneName;
-statement error: Failed to parse query: Length of identifier
+statement error: Length of identifier
ALTER ZONE zoneName RENAME TO
zoneName_veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongIdentifierOf129Characters;
statement ok
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlCreateTableOption.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlCreateTableOption.java
index a9e92afbbb..27c59da3fd 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlCreateTableOption.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlCreateTableOption.java
@@ -26,9 +26,6 @@ import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.parser.SqlParserPos;
-import org.apache.calcite.sql.util.SqlVisitor;
-import org.apache.calcite.sql.validate.SqlValidator;
-import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Litmus;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -92,18 +89,6 @@ public class IgniteSqlCreateTableOption extends SqlCall {
value.unparse(writer, leftPrec, rightPrec);
}
- /** {@inheritDoc} */
- @Override
- public void validate(SqlValidator validator, SqlValidatorScope scope) {
- throw new UnsupportedOperationException();
- }
-
- /** {@inheritDoc} */
- @Override
- public <R> R accept(SqlVisitor<R> visitor) {
- throw new UnsupportedOperationException();
- }
-
/** {@inheritDoc} */
@Override
public boolean equalsDeep(SqlNode node, Litmus litmus) {
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlParser.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlParser.java
index e19ab9e812..2fe97d27c1 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlParser.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlParser.java
@@ -17,22 +17,31 @@
package org.apache.ignite.internal.sql.engine.sql;
+import static org.apache.calcite.util.Static.RESOURCE;
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_PARSE_ERR;
import java.io.Reader;
import java.util.List;
import org.apache.calcite.config.Lex;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCollectionTypeNameSpec;
+import org.apache.calcite.sql.SqlDataTypeSpec;
import org.apache.calcite.sql.SqlDelete;
+import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlMerge;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlUpdate;
+import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.SqlParserImplFactory;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.util.SqlShuttle;
import org.apache.calcite.util.SourceStringReader;
import
org.apache.ignite.internal.generated.query.calcite.sql.IgniteSqlParserImpl;
import
org.apache.ignite.internal.generated.query.calcite.sql.IgniteSqlParserImplConstants;
@@ -41,6 +50,7 @@ import
org.apache.ignite.internal.generated.query.calcite.sql.Token;
import org.apache.ignite.internal.generated.query.calcite.sql.TokenMgrError;
import org.apache.ignite.internal.util.StringUtils;
import org.apache.ignite.sql.SqlException;
+import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Provides method for parsing SQL statements in SQL dialect of Apache Ignite
3.
@@ -55,6 +65,10 @@ public final class IgniteSqlParser {
public static final SqlParser.Config PARSER_CONFIG = SqlParser.config()
.withParserFactory(InternalIgniteSqlParser.FACTORY)
.withLex(Lex.ORACLE)
+ // Do not validate the length of SQL identifiers by the parser
because a validation error is throw after creating a token,
+ // which is rather confusing. E.g. A string 12...(129 chars) is
going to produce "identifier is too long" error,
+ // instead of reporting that expected a query but got something
else.
+ .withIdentifierMaxLength(Integer.MAX_VALUE)
.withConformance(IgniteSqlConformance.INSTANCE);
private IgniteSqlParser() {
@@ -107,6 +121,9 @@ public final class IgniteSqlParser {
list.set(i, node);
}
+ ValidateSqlIdentifiers visitor = new ValidateSqlIdentifiers();
+ nodeList.accept(visitor);
+
return mode.createResult(list, dynamicParamsCount);
} catch (SqlParseException e) {
throw convertException(e);
@@ -273,4 +290,50 @@ public final class IgniteSqlParser {
return node;
}
}
+
+ private static class ValidateSqlIdentifiers extends SqlShuttle {
+ @Override
+ public @Nullable SqlNode visit(SqlIdentifier id) {
+ for (String segment : id.names) {
+ validateId(id.getParserPosition(), segment);
+ }
+
+ return super.visit(id);
+ }
+
+ @Override
+ public @Nullable SqlNode visit(SqlCall call) {
+ SqlOperator operator = call.getOperator();
+
+ // If something when wrong during the parsing, fail at the
validation stage
+ if (operator != null) {
+ validateId(call.getParserPosition(), operator.getName());
+ }
+
+ return super.visit(call);
+ }
+
+ @Override
+ public @Nullable SqlNode visit(SqlDataTypeSpec type) {
+ this.visit(type.getTypeName());
+
+ // getComponentTypeSpec throws AssertionError if typeNameSpec is
not an instance of CollectionTypeNameSpec.
+ if (type.getTypeNameSpec() instanceof SqlCollectionTypeNameSpec) {
+ SqlDataTypeSpec componentTypeSpec =
type.getComponentTypeSpec();
+ if (componentTypeSpec != null) {
+ this.visit(componentTypeSpec);
+ }
+ }
+
+ return super.visit(type);
+ }
+
+ private static void validateId(SqlParserPos pos, String segment) {
+ int maxLength = SqlParser.DEFAULT_IDENTIFIER_MAX_LENGTH;
+
+ if (segment.length() > maxLength) {
+ throw SqlUtil.newContextException(pos,
RESOURCE.identifierTooLong(segment, maxLength));
+ }
+ }
+ }
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlUpdate.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlUpdate.java
index 92967de3b9..3f2638ef1b 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlUpdate.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlUpdate.java
@@ -26,6 +26,7 @@ import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlSpecialOperator;
import org.apache.calcite.sql.SqlUpdate;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.util.ImmutableNullableList;
@@ -37,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class IgniteSqlUpdate extends SqlUpdate {
/** UPDATE operator. */
- protected static final class Operator extends IgniteSqlSpecialOperator {
+ protected static final class Operator extends SqlSpecialOperator {
/** Constructor. */
protected Operator() {
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlZoneOption.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlZoneOption.java
index 1a523acac9..8ba9f225df 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlZoneOption.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlZoneOption.java
@@ -26,9 +26,6 @@ import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.parser.SqlParserPos;
-import org.apache.calcite.sql.util.SqlVisitor;
-import org.apache.calcite.sql.validate.SqlValidator;
-import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Litmus;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -94,18 +91,6 @@ public class IgniteSqlZoneOption extends SqlCall {
value.unparse(writer, leftPrec, rightPrec);
}
- /** {@inheritDoc} */
- @Override
- public void validate(SqlValidator validator, SqlValidatorScope scope) {
- throw new UnsupportedOperationException();
- }
-
- /** {@inheritDoc} */
- @Override
- public <R> R accept(SqlVisitor<R> visitor) {
- throw new UnsupportedOperationException();
- }
-
/** {@inheritDoc} */
@Override
public boolean equalsDeep(SqlNode node, Litmus litmus) {