This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 471def6539e [fix](fe) Fix IS TRUE/ IS FALSE predicate null semantics
(#64696)
471def6539e is described below
commit 471def6539e033257a75c6e7317665e5ac595b70
Author: morrySnow <[email protected]>
AuthorDate: Thu Jun 25 17:20:13 2026 +0800
[fix](fe) Fix IS TRUE/ IS FALSE predicate null semantics (#64696)
### What problem does this PR solve?
Problem Summary: `IS TRUE`, `IS FALSE`, `IS NOT TRUE`, and `IS NOT
FALSE` in Nereids did not preserve SQL three-valued logic correctly
because the parser reused generic boolean expressions instead of
representing these predicates explicitly. This patch adds Nereids
`IsTrue` and `IsFalse` expressions, parses `IS TRUE` and `IS FALSE` into
those nodes, and rewrites them during expression analysis to boolean
casts guarded by `IS NOT NULL`. With the existing `NOT` wrapper from
parsing, `IS NOT TRUE` and `IS NOT FALSE` now correctly include NULL
rows.
### Release note
Fix Nereids null semantics for `IS TRUE`, `IS FALSE`, `IS NOT TRUE`, and
`IS NOT FALSE` predicates.
---
.../doris/nereids/parser/LogicalPlanBuilder.java | 9 +--
.../nereids/rules/analysis/ExpressionAnalyzer.java | 21 +++++
.../doris/nereids/trees/expressions/IsFalse.java | 90 ++++++++++++++++++++++
.../doris/nereids/trees/expressions/IsTrue.java | 90 ++++++++++++++++++++++
.../expressions/visitor/ExpressionVisitor.java | 10 +++
.../doris/nereids/parser/NereidsParserTest.java | 20 +++++
.../rules/analysis/ExpressionAnalyzerTest.java | 37 +++++++++
.../sql/test_compare_expression.out | 2 +-
.../sql_functions/test_is_true_false_predicate.out | 23 ++++++
.../test_is_true_false_predicate.groovy | 48 ++++++++++++
10 files changed, 344 insertions(+), 6 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 34c69e97eee..c470bc789d9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -546,7 +546,9 @@ import
org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.InSubquery;
import org.apache.doris.nereids.trees.expressions.IntegralDivide;
+import org.apache.doris.nereids.trees.expressions.IsFalse;
import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.IsTrue;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.Like;
@@ -1077,7 +1079,6 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalUsingJoin;
import org.apache.doris.nereids.types.AggStateType;
import org.apache.doris.nereids.types.ArrayType;
import org.apache.doris.nereids.types.BigIntType;
-import org.apache.doris.nereids.types.BooleanType;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeType;
import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -5067,12 +5068,10 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
outExpression = new IsNull(valueExpression);
break;
case DorisParser.TRUE:
- outExpression = new Cast(valueExpression,
- BooleanType.INSTANCE, true);
+ outExpression = new IsTrue(valueExpression);
break;
case DorisParser.FALSE:
- outExpression = new Not(new Cast(valueExpression,
- BooleanType.INSTANCE, true));
+ outExpression = new IsFalse(valueExpression);
break;
case DorisParser.MATCH:
case DorisParser.MATCH_ANY:
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index 972129749af..a8f9d7bb27b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -58,6 +58,9 @@ import
org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.InSubquery;
import org.apache.doris.nereids.trees.expressions.IntegralDivide;
+import org.apache.doris.nereids.trees.expressions.IsFalse;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.IsTrue;
import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.Match;
import org.apache.doris.nereids.trees.expressions.Not;
@@ -939,6 +942,24 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
}
}
+ @Override
+ public Expression visitIsTrue(IsTrue isTrue, ExpressionRewriteContext
context) {
+ Expression child = isTrue.child().accept(this, context);
+ if (!child.getDataType().isBooleanType()) {
+ child = new Cast(child, BooleanType.INSTANCE);
+ }
+ return new And(child, new Not(new IsNull(child)));
+ }
+
+ @Override
+ public Expression visitIsFalse(IsFalse isFalse, ExpressionRewriteContext
context) {
+ Expression child = isFalse.child().accept(this, context);
+ if (!child.getDataType().isBooleanType()) {
+ child = new Cast(child, BooleanType.INSTANCE);
+ }
+ return new And(new Not(child), new Not(new IsNull(child)));
+ }
+
@Override
public Expression visitInSubquery(InSubquery inSubquery,
ExpressionRewriteContext context) {
// analyze subquery
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsFalse.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsFalse.java
new file mode 100644
index 00000000000..fdd17c296fc
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsFalse.java
@@ -0,0 +1,90 @@
+// 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.doris.nereids.trees.expressions;
+
+import org.apache.doris.nereids.exceptions.UnboundException;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.DataType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * expr is false predicate.
+ */
+public class IsFalse extends Expression implements UnaryExpression,
AlwaysNotNullable {
+
+ public IsFalse(Expression e) {
+ super(ImmutableList.of(e));
+ }
+
+ private IsFalse(List<Expression> children) {
+ super(children);
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitIsFalse(this, context);
+ }
+
+ @Override
+ public IsFalse withChildren(List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new IsFalse(children);
+ }
+
+ @Override
+ public String computeToSql() throws UnboundException {
+ return child().toSql() + " IS FALSE";
+ }
+
+ @Override
+ public String toString() {
+ return child().toString() + " IS FALSE";
+ }
+
+ @Override
+ public String toDigest() {
+ return child().toDigest() + " IS FALSE";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) {
+ return false;
+ }
+ IsFalse other = (IsFalse) o;
+ return Objects.equals(child(), other.child());
+ }
+
+ @Override
+ public int computeHashCode() {
+ return child().hashCode();
+ }
+
+ @Override
+ public DataType getDataType() throws UnboundException {
+ return BooleanType.INSTANCE;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsTrue.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsTrue.java
new file mode 100644
index 00000000000..366ab0056dc
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/IsTrue.java
@@ -0,0 +1,90 @@
+// 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.doris.nereids.trees.expressions;
+
+import org.apache.doris.nereids.exceptions.UnboundException;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.DataType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * expr is true predicate.
+ */
+public class IsTrue extends Expression implements UnaryExpression,
AlwaysNotNullable {
+
+ public IsTrue(Expression e) {
+ super(ImmutableList.of(e));
+ }
+
+ private IsTrue(List<Expression> children) {
+ super(children);
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitIsTrue(this, context);
+ }
+
+ @Override
+ public IsTrue withChildren(List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new IsTrue(children);
+ }
+
+ @Override
+ public String computeToSql() throws UnboundException {
+ return child().toSql() + " IS TRUE";
+ }
+
+ @Override
+ public String toString() {
+ return child().toString() + " IS TRUE";
+ }
+
+ @Override
+ public String toDigest() {
+ return child().toDigest() + " IS TRUE";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) {
+ return false;
+ }
+ IsTrue other = (IsTrue) o;
+ return Objects.equals(child(), other.child());
+ }
+
+ @Override
+ public int computeHashCode() {
+ return child().hashCode();
+ }
+
+ @Override
+ public DataType getDataType() throws UnboundException {
+ return BooleanType.INSTANCE;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
index aeaea2e8db2..a25b110dfb4 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
@@ -52,7 +52,9 @@ import
org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.InSubquery;
import org.apache.doris.nereids.trees.expressions.IntegralDivide;
+import org.apache.doris.nereids.trees.expressions.IsFalse;
import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.IsTrue;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.MarkJoinSlotReference;
@@ -448,6 +450,14 @@ public abstract class ExpressionVisitor<R, C>
return visit(isNull, context);
}
+ public R visitIsTrue(IsTrue isTrue, C context) {
+ return visit(isTrue, context);
+ }
+
+ public R visitIsFalse(IsFalse isFalse, C context) {
+ return visit(isFalse, context);
+ }
+
public R visitInSubquery(InSubquery in, C context) {
return visitSubqueryExpr(in, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
index c87981ed368..82c7066d4c9 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
@@ -34,7 +34,9 @@ import org.apache.doris.nereids.glue.LogicalPlanAdapter;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsFalse;
import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.IsTrue;
import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.OrderExpression;
import org.apache.doris.nereids.trees.expressions.functions.generator.Unnest;
@@ -1749,4 +1751,22 @@ public class NereidsParserTest extends ParserTestBase {
Assertions.assertInstanceOf(Not.class, expression.child(0));
Assertions.assertInstanceOf(IsNull.class,
expression.child(0).child(0));
}
+
+ @Test
+ public void testIsTrueAndIsFalseExpression() {
+ NereidsParser nereidsParser = new NereidsParser();
+ Expression expression = nereidsParser.parseExpression("X IS TRUE");
+ Assertions.assertInstanceOf(IsTrue.class, expression);
+
+ expression = nereidsParser.parseExpression("X IS FALSE");
+ Assertions.assertInstanceOf(IsFalse.class, expression);
+
+ expression = nereidsParser.parseExpression("X IS NOT TRUE");
+ Assertions.assertInstanceOf(Not.class, expression);
+ Assertions.assertInstanceOf(IsTrue.class, expression.child(0));
+
+ expression = nereidsParser.parseExpression("X IS NOT FALSE");
+ Assertions.assertInstanceOf(Not.class, expression);
+ Assertions.assertInstanceOf(IsFalse.class, expression.child(0));
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
index 052325cac35..6c70a37aa0c 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
@@ -21,14 +21,21 @@ import org.apache.doris.nereids.analyzer.Scope;
import org.apache.doris.nereids.analyzer.UnboundFunction;
import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.And;
import org.apache.doris.nereids.trees.expressions.BoundStar;
+import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsFalse;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.IsTrue;
+import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.BooleanType;
import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Assertions;
@@ -96,4 +103,34 @@ public class ExpressionAnalyzerTest {
);
Assertions.assertEquals(expectedResult, result);
}
+
+ @Test
+ public void testAnalyzeIsTrueAndIsFalse() {
+ ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new
Scope(ImmutableList.of()),
+ null, true, true);
+ SlotReference slot = new SlotReference(new ExprId(1), "c1",
BigIntType.INSTANCE, true, ImmutableList.of());
+
+ Expression isTrue = analyzer.analyze(new IsTrue(slot));
+ Assertions.assertInstanceOf(And.class, isTrue);
+ Assertions.assertInstanceOf(Cast.class, isTrue.child(0));
+ Assertions.assertEquals(BooleanType.INSTANCE,
isTrue.child(0).getDataType());
+ Assertions.assertInstanceOf(Not.class, isTrue.child(1));
+ Assertions.assertInstanceOf(IsNull.class, isTrue.child(1).child(0));
+
+ Expression isFalse = analyzer.analyze(new IsFalse(slot));
+ Assertions.assertInstanceOf(And.class, isFalse);
+ Assertions.assertInstanceOf(Not.class, isFalse.child(0));
+ Assertions.assertInstanceOf(Cast.class, isFalse.child(0).child(0));
+ Assertions.assertEquals(BooleanType.INSTANCE,
isFalse.child(0).child(0).getDataType());
+ Assertions.assertInstanceOf(Not.class, isFalse.child(1));
+ Assertions.assertInstanceOf(IsNull.class, isFalse.child(1).child(0));
+
+ Expression isNotTrue = analyzer.analyze(new Not(new IsTrue(slot)));
+ Assertions.assertInstanceOf(Not.class, isNotTrue);
+ Assertions.assertInstanceOf(And.class, isNotTrue.child(0));
+
+ Expression isNotFalse = analyzer.analyze(new Not(new IsFalse(slot)));
+ Assertions.assertInstanceOf(Not.class, isNotFalse);
+ Assertions.assertInstanceOf(And.class, isNotFalse.child(0));
+ }
}
diff --git
a/regression-test/data/nereids_syntax_p0/sql/test_compare_expression.out
b/regression-test/data/nereids_syntax_p0/sql/test_compare_expression.out
index 7f4f2d231be..8ee4c5c164e 100644
--- a/regression-test/data/nereids_syntax_p0/sql/test_compare_expression.out
+++ b/regression-test/data/nereids_syntax_p0/sql/test_compare_expression.out
@@ -45,5 +45,5 @@ true false true true
true false false true
-- !test_compare_expression_16 --
-\N \N \N \N
+false true false true
diff --git
a/regression-test/data/query_p0/sql_functions/test_is_true_false_predicate.out
b/regression-test/data/query_p0/sql_functions/test_is_true_false_predicate.out
new file mode 100644
index 00000000000..cc50d0409d2
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/test_is_true_false_predicate.out
@@ -0,0 +1,23 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !is_true --
+1
+
+-- !is_false --
+2
+
+-- !is_not_true --
+2
+3
+4
+
+-- !is_not_false --
+1
+3
+4
+
+-- !truth_table --
+1 true true false false true true true false false
true
+2 false false true true false false false true true
false
+3 \N false false true true abc false false true
true
+4 \N false false true true \N false false true
true
+
diff --git
a/regression-test/suites/query_p0/sql_functions/test_is_true_false_predicate.groovy
b/regression-test/suites/query_p0/sql_functions/test_is_true_false_predicate.groovy
new file mode 100644
index 00000000000..4ad05100d42
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/test_is_true_false_predicate.groovy
@@ -0,0 +1,48 @@
+// 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.
+
+suite("test_is_true_false_predicate", "arrow_flight_sql") {
+ sql """drop table if exists test_is_true_false_predicate;"""
+ sql """
+ create table test_is_true_false_predicate (
+ id int null,
+ b boolean null,
+ c string null
+ )
+ duplicate key (id)
+ distributed by hash(id) buckets 1
+ properties("replication_num" = "1");
+ """
+ sql """insert into test_is_true_false_predicate values (1, true, 'true'),
(2, false, 'false'), (3, null, 'abc'), (4, null, null);"""
+
+ order_qt_is_true """
+ select id from test_is_true_false_predicate where b is true order by
id;
+ """
+ order_qt_is_false """
+ select id from test_is_true_false_predicate where b is false order by
id;
+ """
+ order_qt_is_not_true """
+ select id from test_is_true_false_predicate where b is not true order
by id;
+ """
+ order_qt_is_not_false """
+ select id from test_is_true_false_predicate where b is not false order
by id;
+ """
+ order_qt_truth_table """
+ select id, b, b is true, b is false, b is not true, b is not false, c,
c is true, c is false, c is not true, c is not false
+ from test_is_true_false_predicate order by id;
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]