Skip to content
Draft
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
7 changes: 7 additions & 0 deletions be/src/exprs/function/function_string_misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,13 @@ class FunctionNgramSearch : public IFunction {
}
auto pattern = assert_cast<const ColumnString*>(argument_columns[1].get())->get_data_at(0);
auto gram_num = assert_cast<const ColumnInt32*>(argument_columns[2].get())->get_element(0);
// FE only rejects a nonpositive gram_num once it is a literal. A constant expression that
// FE cannot evaluate (e.g. `crc32('abc') % 3 - 3`) reaches BE unchecked when the whole
// call is folded on BE, so validate here before it is used as a substring length.
if (gram_num <= 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate the gram even when execution is skipped

This is the only value check, but execute_impl is not guaranteed to run. crc32('abc') % 0 remains a constant integral tree in FE and evaluates to NULL on BE, where default NULL propagation returns before this line. Also, select ngram_search(cast(number as string), 'abc', crc32('abc') % 3) from numbers("number"="0") has a row-dependent root, so the empty projection skips the function and the known-zero gram is never rejected. Literal NULL/zero grams are rejected during analysis regardless of these shapes, so newly admitted BE-only constants change the contract. Please validate semantic constants on a path that survives CSE and zero-row execution, while retaining a pre-NULL batch check for materialized nonempty slots, and cover both cases in both fold modes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d487595.

The gram is now resolved and validated during function binding, before NULL propagation, CSE, or empty-plan rewrites can discard the call. FE-evaluable constants stay local; other constants use the existing BE evaluator regardless of the optional BE-fold setting. The evaluated INT literal is retained in the plan, and failed evaluation is reported rather than silently deferring validation to execution.

BE also checks materialized grams before propagating NULL from text/pattern. Regression coverage includes crc32('abc') % 0, zero/negative grams with zero rows, CSE-shaped expressions, NULL text, WHERE false, and LIMIT 0, in both fold modes. This also covers the case where NULL text previously removed the entire function on FE, which a BE-only open() fix would miss.

Validation: FE UT 12/12, ASAN BE UT 7/7, ASAN FE/BE build, and the new/existing string regression suites 2/2 passed. The prior 26 SQL probes now produce the expected outcomes. clang-tidy remains blocked by the existing unmatched NOLINTEND in core/types.h, with no emitted changed-line diagnostic.

The PR description explicitly records the additional planning RPC for BE-only grams, its existing five-second timeout, and the planner-lock waiting tradeoff. This thread is left for re-review rather than manually resolved.

return Status::InvalidArgument(
"ngram_search(text,pattern,gram_num): gram_num must be a positive constant.");
}
const auto* text_col = assert_cast<const ColumnString*>(argument_columns[0].get());

if (col_const[0]) {
Expand Down
28 changes: 28 additions & 0 deletions be/test/exprs/function/function_string_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ DataSet make_md5_varbinary_dataset(const std::vector<std::string>& inputs) {

} // namespace

TEST(function_string_test, ngram_search_gram_num) {
const InputTypeSet input_types = {Notnull {TYPE_STRING}, ConstedNotnull {TYPE_STRING},
ConstedNotnull {TYPE_INT}};
// 2 * |intersection| / (|text grams| + |pattern grams|); one row per data set because the
// const arguments are built from a single value.
const DataSet data_set = {{{std::string("abc"), std::string("abc"), int32_t(3)}, 1.0},
{{std::string("ab"), std::string("abc"), int32_t(3)}, 0.0},
{{std::string("ab"), std::string("abc"), int32_t(1)}, 0.8}};
for (const auto& row : data_set) {
ASSERT_TRUE(check_function<DataTypeFloat64>("ngram_search", input_types, {row}).ok());
}
}

TEST(function_string_test, ngram_search_nonpositive_gram_num) {
// FE folds a constant expression like `crc32('abc') % 3 - 3` on BE before it can check the
// value, so BE must reject a nonpositive gram_num instead of using it as a substring length.
const InputTypeSet input_types = {Notnull {TYPE_STRING}, ConstedNotnull {TYPE_STRING},
ConstedNotnull {TYPE_INT}};
for (int32_t gram_num : {0, -1}) {
const DataSet data_set = {{{std::string("abc"), std::string("abc"), gram_num}, 0.0}};
auto st = check_function<DataTypeFloat64>("ngram_search", input_types, data_set, -1, -1,
true);
EXPECT_TRUE(st.is<ErrorCode::INVALID_ARGUMENT>()) << st;
EXPECT_NE(st.to_string().find("gram_num must be a positive constant"), std::string::npos)
<< st;
}
}

TEST(function_string_test, parse_data_size_nullable) {
const InputTypeSet input_types = {PrimitiveType::TYPE_STRING};
const DataSet data_set = {{{Null()}, Null()},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import org.apache.doris.catalog.FunctionSignature;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.DoubleType;
import org.apache.doris.nereids.types.IntegerType;
Expand Down Expand Up @@ -57,16 +59,34 @@ private NgramSearch(ScalarFunctionParams functionParams) {

@Override
public void checkLegalityBeforeTypeCoercion() {
if (!child(1).isConstant()) {
if (!getArgument(1).isConstant()) {
throw new AnalysisException(
"ngram_search(text,pattern,gram_num): pattern support const value only.");
}
Expression gramNum = child(2);
if (!(gramNum instanceof IntegerLikeLiteral)) {
Expression gramNum = getArgument(2);
if (!gramNum.isConstant() || !gramNum.getDataType().isIntegralType()) {
throw new AnalysisException(
"ngram_search(text,pattern,gram_num): gram_num support const value only.");
}
if (((IntegerLikeLiteral) gramNum).getIntValue() <= 0) {
// Constant folding has not run yet, so a constant expression such as `1 + 2` is not a
// literal here. Reject the values FE can already determine now, before NULL propagation or
// plan pruning can drop the whole call and skip checkLegalityAfterRewrite.
checkGramNumValue(FoldConstantRuleOnFE.evaluateWithoutContext(gramNum));
}

@Override
public void checkLegalityAfterRewrite() {
// Constant folding (FE or BE) may have produced the literal by now. A constant that is
// still not a literal is evaluated by BE, which rejects a nonpositive gram_num itself.
checkGramNumValue(getArgument(2));
}

private static void checkGramNumValue(Expression gramNum) {
if (gramNum instanceof NullLiteral) {
throw new AnalysisException(
"ngram_search(text,pattern,gram_num): gram_num support const value only.");
}
if (gramNum instanceof IntegerLikeLiteral && ((IntegerLikeLiteral) gramNum).getLongValue() <= 0) {
throw new AnalysisException(
"ngram_search(text,pattern,gram_num): gram_num must be a positive constant.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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.functions.scalar;

import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.rules.analysis.ExpressionAnalyzer;
import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
import org.apache.doris.nereids.trees.expressions.Add;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.Mod;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.types.IntegerType;
import org.apache.doris.nereids.types.StringType;

import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class NgramSearchTest {

@Test
void testLiteralGramNumber() {
assertFoldableGramNumber(new IntegerLiteral(3));
}

@Test
void testFoldableArithmeticGramNumber() {
assertFoldableGramNumber(new Add(new IntegerLiteral(1), new IntegerLiteral(2)));
}

@Test
void testFoldableCastGramNumber() {
assertFoldableGramNumber(new Cast(new StringLiteral("3"), IntegerType.INSTANCE));
}

@Test
void testFoldableFunctionGramNumber() {
assertFoldableGramNumber(new Abs(new IntegerLiteral(-3)));
}

@Test
void testConstantGramNumberBeyondFeEvaluator() {
// FE cannot evaluate crc32, so the constant stays unfolded and is left for BE to
// evaluate and validate; FE must not reject it as nonconstant at either stage.
Expression gram = new Add(new Mod(new Crc32(new StringLiteral("abc")), new IntegerLiteral(3)),
new IntegerLiteral(1));
NgramSearch analyzed = analyze(gram);
Assertions.assertFalse(analyzed.child(2).isLiteral());
Assertions.assertDoesNotThrow(analyzed::checkLegalityAfterRewrite);
}

@Test
void testAfterRewriteRejectsInvalidLiteral() {
NgramSearch analyzed = analyze(new IntegerLiteral(3));
assertAfterRewriteFails(withGramNumber(analyzed, new IntegerLiteral(0)),
"gram_num must be a positive constant");
assertAfterRewriteFails(withGramNumber(analyzed, new IntegerLiteral(-1)),
"gram_num must be a positive constant");
assertAfterRewriteFails(withGramNumber(analyzed, new NullLiteral(IntegerType.INSTANCE)),
"gram_num support const value only");
}

@Test
void testNonPositiveGramNumber() {
assertAnalyzeFails(new IntegerLiteral(0), "gram_num must be a positive constant");
assertAnalyzeFails(new IntegerLiteral(-1), "gram_num must be a positive constant");
assertAnalyzeFails(new Subtract(new IntegerLiteral(1), new IntegerLiteral(1)),
"gram_num must be a positive constant");
assertAnalyzeFails(new Subtract(new IntegerLiteral(1), new IntegerLiteral(2)),
"gram_num must be a positive constant");
assertAnalyzeFails(new Cast(new StringLiteral("0"), IntegerType.INSTANCE),
"gram_num must be a positive constant");
}

@Test
void testNonConstantGramNumber() {
SlotReference gram = SlotReference.of("gram", IntegerType.INSTANCE);
assertAnalyzeFails(gram, "gram_num support const value only");
assertAnalyzeFails(new Add(gram, new IntegerLiteral(1)), "gram_num support const value only");
assertAnalyzeFails(new Cast(new Random(), IntegerType.INSTANCE), "gram_num support const value only");
}

@Test
void testNonIntegerGramNumber() {
assertAnalyzeFails(new StringLiteral("3"), "gram_num support const value only");
assertAnalyzeFails(new DoubleLiteral(3.0), "gram_num support const value only");
assertAnalyzeFails(new NullLiteral(), "gram_num support const value only");
assertAnalyzeFails(new Cast(new NullLiteral(), IntegerType.INSTANCE), "gram_num support const value only");
}

@Test
void testNonConstantPattern() {
NgramSearch function = new NgramSearch(new StringLiteral("abc"),
SlotReference.of("pattern", StringType.INSTANCE), new IntegerLiteral(3));
AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
() -> ExpressionAnalyzer.analyzeFunction(null, null, function));
Assertions.assertTrue(exception.getMessage().contains("pattern support const value only"));
}

private NgramSearch analyze(Expression gram) {
Expression analyzed = ExpressionAnalyzer.analyzeFunction(null, null,
new NgramSearch(new StringLiteral("abc"), new StringLiteral("abc"), gram));
return (NgramSearch) analyzed;
}

private NgramSearch withGramNumber(NgramSearch function, Expression gram) {
return function.withChildren(ImmutableList.of(function.child(0), function.child(1), gram));
}

private void assertFoldableGramNumber(Expression gram) {
NgramSearch analyzed = analyze(gram);
Expression folded = FoldConstantRuleOnFE.evaluateWithoutContext(analyzed);
Assertions.assertEquals(new IntegerLiteral(3), folded.child(2));
Assertions.assertDoesNotThrow(folded::checkLegalityAfterRewrite);
}

private void assertAnalyzeFails(Expression gram, String message) {
AnalysisException exception = Assertions.assertThrows(AnalysisException.class, () -> analyze(gram));
Assertions.assertTrue(exception.getMessage().contains(message), exception.getMessage());
}

private void assertAfterRewriteFails(NgramSearch function, String message) {
AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
function::checkLegalityAfterRewrite);
Assertions.assertTrue(exception.getMessage().contains(message), exception.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !literal --
1

-- !arithmetic --
1

-- !cast --
1

-- !function --
1

-- !nested --
1

-- !null_text --
\N

-- !rows --
1 1 1
2 0 0.8
3 0.3333333333333333 0.75
4 \N \N

-- !be_function --
1

-- !be_null_gram --
\N

-- !be_rows --
1 1 \N
2 0.8 \N
3 0.75 \N
4 \N \N

-- !literal --
1

-- !arithmetic --
1

-- !cast --
1

-- !function --
1

-- !nested --
1

-- !null_text --
\N

-- !rows --
1 1 1
2 0 0.8
3 0.3333333333333333 0.75
4 \N \N

-- !be_function --
1

-- !be_null_gram --
\N

-- !be_rows --
1 1 \N
2 0.8 \N
3 0.75 \N
4 \N \N

Loading
Loading