blob: 00957a8c0d22de26e9ecb609793308212447983d [file]
/*
* 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.
*
*/
#include "search/ir_pass.h"
#include "fmt/core.h"
#include "gtest/gtest.h"
#include "search/interval.h"
#include "search/ir_sema_checker.h"
#include "search/passes/interval_analysis.h"
#include "search/passes/lower_to_plan.h"
#include "search/passes/manager.h"
#include "search/passes/push_down_not_expr.h"
#include "search/passes/simplify_and_or_expr.h"
#include "search/passes/simplify_boolean.h"
#include "search/sql_transformer.h"
using namespace kqir;
static auto Parse(const std::string& in) { return sql::ParseToIR(peg::string_input(in, "test")); }
TEST(IRPassTest, Simple) {
auto ir = *Parse("select a from b where not c = 1 or d hastag \"x\" and 2 <= e order by e asc limit 0, 10");
auto original = ir->Dump();
struct MyVisitor : Visitor {
std::string_view Name() override { return ""; };
} visitor;
auto ir2 = visitor.Transform(std::move(ir));
ASSERT_EQ(original, ir2->Dump());
}
TEST(IRPassTest, SimplifyBoolean) {
SimplifyBoolean sb;
ASSERT_EQ(sb.Transform(*Parse("select a from b where not false"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where not not false"))->Dump(), "select a from b where false");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true and true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true and false"))->Dump(), "select a from b where false");
ASSERT_EQ(sb.Transform(*Parse("select a from b where false and true"))->Dump(), "select a from b where false");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true and false and true"))->Dump(),
"select a from b where false");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true and true and true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where x > 1 and false"))->Dump(), "select a from b where false");
ASSERT_EQ(sb.Transform(*Parse("select a from b where x > 1 and true"))->Dump(), "select a from b where x > 1");
ASSERT_EQ(sb.Transform(*Parse("select a from b where x > 1 and true and y < 10"))->Dump(),
"select a from b where (and x > 1, y < 10)");
ASSERT_EQ(sb.Transform(*Parse("select a from b where not (false and (not true))"))->Dump(),
"select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true or true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true or false"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where false or true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true or false or true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where true or false or true"))->Dump(), "select a from b where true");
ASSERT_EQ(sb.Transform(*Parse("select a from b where not ((x < 1 or true) and (y > 2 and true))"))->Dump(),
"select a from b where not y > 2");
}
TEST(IRPassTest, SimplifyAndOrExpr) {
SimplifyAndOrExpr saoe;
ASSERT_EQ(Parse("select a from b where true and (false and true)").GetValue()->Dump(),
"select a from b where (and true, (and false, true))");
ASSERT_EQ(saoe.Transform(*Parse("select a from b where true and (false and true)"))->Dump(),
"select a from b where (and true, false, true)");
ASSERT_EQ(saoe.Transform(*Parse("select a from b where true or (false or true)"))->Dump(),
"select a from b where (or true, false, true)");
ASSERT_EQ(saoe.Transform(*Parse("select a from b where true and (false or true)"))->Dump(),
"select a from b where (and true, (or false, true))");
ASSERT_EQ(saoe.Transform(*Parse("select a from b where true or (false and true)"))->Dump(),
"select a from b where (or true, (and false, true))");
ASSERT_EQ(saoe.Transform(*Parse("select a from b where x > 1 or (y < 2 or z = 3)"))->Dump(),
"select a from b where (or x > 1, y < 2, z = 3)");
}
TEST(IRPassTest, PushDownNotExpr) {
PushDownNotExpr pdne;
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not a > 1"))->Dump(), "select * from a where a <= 1");
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not a hastag \"\""))->Dump(),
"select * from a where not a hastag \"\"");
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not not a > 1"))->Dump(), "select * from a where a > 1");
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not (a > 1 and b <= 3)"))->Dump(),
"select * from a where (or a <= 1, b > 3)");
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not (a > 1 or b <= 3)"))->Dump(),
"select * from a where (and a <= 1, b > 3)");
ASSERT_EQ(pdne.Transform(*Parse("select * from a where not (not a > 1 or (b < 3 and c hastag \"\"))"))->Dump(),
"select * from a where (and a > 1, (or b >= 3, not c hastag \"\"))");
}
TEST(IRPassTest, Manager) {
auto expr_passes = PassManager::ExprPasses();
ASSERT_EQ(PassManager::Execute(expr_passes,
*Parse("select * from a where not (x > 1 or (y < 2 or z = 3)) and (true or x = 1)"))
->Dump(),
"select * from a where (and x <= 1, y >= 2, z != 3)");
}
TEST(IRPassTest, SortByWithLimitToKnnExpr) {
SortByWithLimitToKnnExpr tsbtke;
ASSERT_EQ(tsbtke.Transform(*Parse("select a from b order by embedding <-> [3.6] limit 5"))->Dump(),
"select a from b where KNN k=5, embedding <-> [3.600000] limit 0, 5");
ASSERT_EQ(tsbtke.Transform(*Parse("select a from b where false order by embedding <-> [3,1,2] limit 5"))->Dump(),
"select a from b where false sortby embedding <-> [3.000000, 1.000000, 2.000000] limit 0, 5");
ASSERT_EQ(tsbtke.Transform(*Parse("select a from b where true order by embedding <-> [3,1,2] limit 5"))->Dump(),
"select a from b where KNN k=5, embedding <-> [3.000000, 1.000000, 2.000000] limit 0, 5");
ASSERT_EQ(tsbtke.Transform(*Parse("select a from b where true order by embedding <-> [3,1,2] limit 3, 5"))->Dump(),
"select a from b where KNN k=8, embedding <-> [3.000000, 1.000000, 2.000000] limit 3, 5");
}
TEST(IRPassTest, LowerToPlan) {
LowerToPlan ltp;
ASSERT_EQ(ltp.Transform(*Parse("select * from a"))->Dump(), "project *: full-scan a");
ASSERT_EQ(ltp.Transform(*Parse("select * from a limit 1"))->Dump(), "project *: (limit 0, 1: full-scan a)");
ASSERT_EQ(ltp.Transform(*Parse("select * from a where false"))->Dump(), "project *: noop");
ASSERT_EQ(ltp.Transform(*Parse("select * from a where false limit 1"))->Dump(), "project *: noop");
ASSERT_EQ(ltp.Transform(*Parse("select * from a where false order by embedding <-> [3,1,2] limit 5"))->Dump(),
"project *: noop");
ASSERT_EQ(ltp.Transform(*Parse("select * from a where b > 1"))->Dump(), "project *: (filter b > 1: full-scan a)");
ASSERT_EQ(ltp.Transform(*Parse("select a from b where c = 1 order by d"))->Dump(),
"project a: (sort d, asc: (filter c = 1: full-scan b))");
ASSERT_EQ(ltp.Transform(*Parse("select a from b where c = 1 limit 1"))->Dump(),
"project a: (limit 0, 1: (filter c = 1: full-scan b))");
ASSERT_EQ(ltp.Transform(*Parse("select a from b where c = 1 and d = 2 order by e limit 1"))->Dump(),
"project a: (limit 0, 1: (sort e, asc: (filter (and c = 1, d = 2): full-scan b)))");
ASSERT_EQ(ltp.Transform(*Parse("select a from b where c = 1 order by d limit 1"))->Dump(),
"project a: (limit 0, 1: (sort d, asc: (filter c = 1: full-scan b)))");
}
TEST(IRPassTest, IntervalAnalysis) {
auto ia_passes = PassManager::Create(IntervalAnalysis{true}, SimplifyAndOrExpr{}, SimplifyBoolean{});
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a > 1 or a < 3"))->Dump(),
"select * from a where true");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a < 1 and a > 3"))->Dump(),
"select * from a where false");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where (a > 3 or a < 1) and a = 2"))->Dump(),
"select * from a where false");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where b = 1 and (a = 1 or a != 1)"))->Dump(),
"select * from a where b = 1");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a = 1 or b = 1 or a != 1"))->Dump(),
"select * from a where true");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where (a < 3 or a > 1) and b >= 1"))->Dump(),
"select * from a where b >= 1");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a != 1 or a != 2"))->Dump(),
"select * from a where true");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a = 1 and a = 2"))->Dump(),
"select * from a where false");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a < 1 and a < 3"))->Dump(),
"select * from a where a < 1");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a < 1 or a < 3"))->Dump(),
"select * from a where a < 3");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a = 1 and a < 3"))->Dump(),
"select * from a where a = 1");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a = 1 or a < 3"))->Dump(),
"select * from a where a < 3");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a = 1 or a = 3"))->Dump(),
"select * from a where (or a = 1, a = 3)");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a != 1"))->Dump(),
"select * from a where a != 1");
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a != 1 and a != 2"))->Dump(),
"select * from a where (and a != 1, a != 2)");
ASSERT_EQ(
PassManager::Execute(ia_passes, *Parse("select * from a where a >= 0 and a >= 1 and a < 4 and a != 2"))->Dump(),
fmt::format("select * from a where (or (and a >= 1, a < 2), (and a >= {}, a < 4))", IntervalSet::NextNum(2)));
ASSERT_EQ(PassManager::Execute(ia_passes, *Parse("select * from a where a != 1 and b > 1 and b = 2"))->Dump(),
"select * from a where (and a != 1, b = 2)");
}
static IndexMap MakeIndexMap() {
auto f1 = FieldInfo("t1", std::make_unique<redis::TagFieldMetadata>());
auto f2 = FieldInfo("t2", std::make_unique<redis::TagFieldMetadata>());
f2.metadata->noindex = true;
auto f3 = FieldInfo("n1", std::make_unique<redis::NumericFieldMetadata>());
auto f4 = FieldInfo("n2", std::make_unique<redis::NumericFieldMetadata>());
auto f5 = FieldInfo("n3", std::make_unique<redis::NumericFieldMetadata>());
f5.metadata->noindex = true;
auto hnsw_field_meta = std::make_unique<redis::HnswVectorFieldMetadata>();
hnsw_field_meta->vector_type = redis::VectorType::FLOAT64;
hnsw_field_meta->dim = 3;
hnsw_field_meta->distance_metric = redis::DistanceMetric::L2;
auto f6 = FieldInfo("v1", std::move(hnsw_field_meta));
hnsw_field_meta = std::make_unique<redis::HnswVectorFieldMetadata>();
hnsw_field_meta->vector_type = redis::VectorType::FLOAT64;
hnsw_field_meta->dim = 3;
hnsw_field_meta->distance_metric = redis::DistanceMetric::L2;
auto f7 = FieldInfo("v2", std::move(hnsw_field_meta));
f7.metadata->noindex = true;
auto ia = std::make_unique<IndexInfo>("ia", redis::IndexMetadata(), "");
ia->Add(std::move(f1));
ia->Add(std::move(f2));
ia->Add(std::move(f3));
ia->Add(std::move(f4));
ia->Add(std::move(f5));
ia->Add(std::move(f6));
ia->Add(std::move(f7));
IndexMap res;
res.Insert(std::move(ia));
return res;
}
std::unique_ptr<Node> ParseS(SemaChecker& sc, const std::string& in) {
auto res = *Parse(in);
EXPECT_EQ(sc.Check(res.get()).Msg(), Status::ok_msg);
return res;
}
TEST(IRPassTest, IndexSelection) {
auto index_map = MakeIndexMap();
auto sc = SemaChecker(index_map);
auto passes = PassManager::Default();
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia"))->Dump(), "project *: full-scan ia");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by n1"))->Dump(),
"project *: numeric-scan n1, [-inf, inf), asc");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by n1 limit 1"))->Dump(),
"project *: (limit 0, 1: numeric-scan n1, [-inf, inf), asc)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by n3"))->Dump(),
"project *: (sort n3, asc: full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by n3 limit 1"))->Dump(),
"project *: (top-n sort n3, asc, 0, 1: full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n2 = 1 order by n1"))->Dump(),
"project *: (filter n2 = 1: numeric-scan n1, [-inf, inf), asc)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 = 1"))->Dump(),
fmt::format("project *: numeric-scan n1, [1, {}), asc", IntervalSet::NextNum(1)));
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 != 1"))->Dump(),
"project *: (filter n1 != 1: full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1"))->Dump(),
"project *: numeric-scan n1, [1, inf), asc");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 and n1 < 2"))->Dump(),
"project *: numeric-scan n1, [1, 2), asc");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 and n2 >= 2"))->Dump(),
"project *: (filter n2 >= 2: numeric-scan n1, [1, inf), asc)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 and n2 = 2"))->Dump(),
fmt::format("project *: (filter n1 >= 1: numeric-scan n2, [2, {}), asc)", IntervalSet::NextNum(2)));
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 and n3 = 2"))->Dump(),
"project *: (filter n3 = 2: numeric-scan n1, [1, inf), asc)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n3 = 1"))->Dump(),
"project *: (filter n3 = 1: full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 = 1 and n3 = 2"))->Dump(),
fmt::format("project *: (filter n3 = 2: numeric-scan n1, [1, {}), asc)", IntervalSet::NextNum(1)));
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 = 1 and t1 hastag \"a\""))->Dump(),
fmt::format("project *: (filter t1 hastag \"a\": numeric-scan n1, [1, {}), asc)", IntervalSet::NextNum(1)));
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where t1 hastag \"a\""))->Dump(),
"project *: tag-scan t1, a");
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where t1 hastag \"a\" and t2 hastag \"a\""))->Dump(),
"project *: (filter t2 hastag \"a\": tag-scan t1, a)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where t2 hastag \"a\""))->Dump(),
"project *: (filter t2 hastag \"a\": full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where v1 <-> [3,1,2] < 5"))->Dump(),
"project *: hnsw-vector-range-scan v1, [3.000000, 1.000000, 2.000000], 5");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by v1 <-> [3,1,2] limit 5"))->Dump(),
"project *: (limit 0, 5: hnsw-vector-knn-scan v1, [3.000000, 1.000000, 2.000000], 5)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia order by v1 <-> [3,1,2] limit 2, 7"))->Dump(),
"project *: (limit 2, 7: hnsw-vector-knn-scan v1, [3.000000, 1.000000, 2.000000], 9)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where v2 <-> [3,1,2] < 5"))->Dump(),
"project *: (filter v2 <-> [3.000000, 1.000000, 2.000000] < 5: full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 and v1 <-> [3,1,2] < 5"))->Dump(),
"project *: (filter n1 >= 1: hnsw-vector-range-scan v1, [3.000000, 1.000000, 2.000000], 5)");
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where v1 <-> [3,1,2] < 5 and t1 hastag \"a\""))->Dump(),
"project *: (filter t1 hastag \"a\": hnsw-vector-range-scan v1, [3.000000, 1.000000, 2.000000], 5)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 2 or n1 < 1"))->Dump(),
"project *: (merge numeric-scan n1, [-inf, 1), asc, numeric-scan n1, [2, inf), asc)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 or n2 >= 2"))->Dump(),
"project *: (merge numeric-scan n1, [1, inf), asc, (filter n1 < 1: numeric-scan n2, [2, inf), asc))");
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 or n2 = 2"))->Dump(),
fmt::format("project *: (merge numeric-scan n1, [1, inf), asc, (filter n1 < 1: numeric-scan n2, [2, {}), asc))",
IntervalSet::NextNum(2)));
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 >= 1 or n3 = 2"))->Dump(),
"project *: (filter (or n1 >= 1, n3 = 2): full-scan ia)");
ASSERT_EQ(PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 = 1 or n3 = 2"))->Dump(),
"project *: (filter (or n1 = 1, n3 = 2): full-scan ia)");
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where n1 = 1 or t1 hastag \"a\""))->Dump(),
fmt::format("project *: (merge tag-scan t1, a, (filter not t1 hastag \"a\": numeric-scan n1, [1, {}), asc))",
IntervalSet::NextNum(1)));
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where t1 hastag \"a\" or t2 hastag \"a\""))->Dump(),
"project *: (filter (or t1 hastag \"a\", t2 hastag \"a\"): full-scan ia)");
ASSERT_EQ(
PassManager::Execute(passes, ParseS(sc, "select * from ia where t1 hastag \"a\" or t1 hastag \"b\""))->Dump(),
"project *: (merge tag-scan t1, a, (filter not t1 hastag \"a\": tag-scan t1, b))");
ASSERT_EQ(
PassManager::Execute(
passes, ParseS(sc, "select * from ia where (n1 < 2 or n1 >= 3) and (n1 >= 1 and n1 < 4) and not n3 != 1"))
->Dump(),
"project *: (filter n3 = 1: (merge numeric-scan n1, [1, 2), asc, numeric-scan n1, [3, 4), asc))");
}