diff --git a/benchmark/imdb/init/load.sql b/benchmark/imdb/init/load.sql index d52746b5c7af..9386e37b2204 100644 --- a/benchmark/imdb/init/load.sql +++ b/benchmark/imdb/init/load.sql @@ -18,4 +18,5 @@ CREATE TABLE movie_link AS SELECT * FROM read_parquet('https://github.com/duckdb CREATE TABLE name AS SELECT * FROM read_parquet('https://github.com/duckdb/duckdb-data/releases/download/v1.0/job_name.parquet'); CREATE TABLE person_info AS SELECT * FROM read_parquet('https://github.com/duckdb/duckdb-data/releases/download/v1.0/job_person_info.parquet'); CREATE TABLE role_type AS SELECT * FROM read_parquet('https://github.com/duckdb/duckdb-data/releases/download/v1.0/job_role_type.parquet'); -CREATE TABLE title AS SELECT * FROM read_parquet('https://github.com/duckdb/duckdb-data/releases/download/v1.0/job_title.parquet'); \ No newline at end of file +CREATE TABLE title AS SELECT * FROM read_parquet('https://github.com/duckdb/duckdb-data/releases/download/v1.0/job_title.parquet'); + diff --git a/benchmark/imdb_plan_cost/queries/18a.sql b/benchmark/imdb_plan_cost/queries/18a.sql index edd21f3f8a34..8428dbcfa8f1 100644 --- a/benchmark/imdb_plan_cost/queries/18a.sql +++ b/benchmark/imdb_plan_cost/queries/18a.sql @@ -1,3 +1,4 @@ +pragma threads=1; SELECT MIN(mi.info) AS movie_budget, MIN(mi_idx.info) AS movie_votes, MIN(t.title) AS movie_title diff --git a/src/include/duckdb/planner/table_filter.hpp b/src/include/duckdb/planner/table_filter.hpp index 368b13eda6f1..4893e17329e7 100644 --- a/src/include/duckdb/planner/table_filter.hpp +++ b/src/include/duckdb/planner/table_filter.hpp @@ -12,6 +12,7 @@ #include "duckdb/common/types.hpp" #include "duckdb/common/unordered_map.hpp" #include "duckdb/common/enums/filter_propagate_result.hpp" +#include "duckdb/common/enums/expression_type.hpp" namespace duckdb { class BaseStatistics; @@ -63,12 +64,14 @@ class TableFilter { } }; +//! class TableFilterSet { public: unordered_map> filters; public: - void PushFilter(idx_t table_index, unique_ptr filter); + void PushFilter(idx_t column_index, unique_ptr filter, + TableFilterType conjunction_type = TableFilterType::CONJUNCTION_AND); bool Equals(TableFilterSet &other) { if (filters.size() != other.filters.size()) { @@ -97,6 +100,7 @@ class TableFilterSet { void Serialize(Serializer &serializer) const; static TableFilterSet Deserialize(Deserializer &deserializer); + static bool ExpressionSupportsPushdown(ExpressionType comparison); }; } // namespace duckdb diff --git a/src/optimizer/filter_combiner.cpp b/src/optimizer/filter_combiner.cpp index f3d6141a3fb9..672b464ab859 100644 --- a/src/optimizer/filter_combiner.cpp +++ b/src/optimizer/filter_combiner.cpp @@ -14,6 +14,7 @@ #include "duckdb/planner/filter/constant_filter.hpp" #include "duckdb/planner/filter/null_filter.hpp" #include "duckdb/optimizer/optimizer.hpp" +#include "duckdb/common/types/value.hpp" namespace duckdb { @@ -391,20 +392,29 @@ bool FilterCombiner::HasFilters() { // return zonemap_checks; // } +bool TableFilterSet::ExpressionSupportsPushdown(ExpressionType comparison) { + return (comparison == ExpressionType::COMPARE_EQUAL || comparison == ExpressionType::COMPARE_GREATERTHAN || + comparison == ExpressionType::COMPARE_GREATERTHANOREQUALTO || + comparison == ExpressionType::COMPARE_LESSTHAN || comparison == ExpressionType::COMPARE_LESSTHANOREQUALTO); +} + +static bool ValueTypeSupportsPushown(const Value &value) { + return TypeIsNumeric(value.type().InternalType()) || value.type().InternalType() == PhysicalType::VARCHAR || + value.type().InternalType() == PhysicalType::BOOL; +} + +static bool LeftConstValRightBoundColref(Expression &left, Expression &right) { + return (left.type == ExpressionType::VALUE_CONSTANT && right.type == ExpressionType::BOUND_COLUMN_REF); +} + TableFilterSet FilterCombiner::GenerateTableScanFilters(vector &column_ids) { TableFilterSet table_filters; //! First, we figure the filters that have constant expressions that we can push down to the table scan for (auto &constant_value : constant_values) { if (!constant_value.second.empty()) { auto filter_exp = equivalence_map.end(); - if ((constant_value.second[0].comparison_type == ExpressionType::COMPARE_EQUAL || - constant_value.second[0].comparison_type == ExpressionType::COMPARE_GREATERTHAN || - constant_value.second[0].comparison_type == ExpressionType::COMPARE_GREATERTHANOREQUALTO || - constant_value.second[0].comparison_type == ExpressionType::COMPARE_LESSTHAN || - constant_value.second[0].comparison_type == ExpressionType::COMPARE_LESSTHANOREQUALTO) && - (TypeIsNumeric(constant_value.second[0].constant.type().InternalType()) || - constant_value.second[0].constant.type().InternalType() == PhysicalType::VARCHAR || - constant_value.second[0].constant.type().InternalType() == PhysicalType::BOOL)) { + if (TableFilterSet::ExpressionSupportsPushdown(constant_value.second[0].comparison_type) && + ValueTypeSupportsPushown(constant_value.second[0].constant)) { //! Here we check if these filters are column references filter_exp = equivalence_map.find(constant_value.first); if (filter_exp->second.size() == 1 && @@ -433,6 +443,7 @@ TableFilterSet FilterCombiner::GenerateTableScanFilters(vector &column_id } } //! Here we look for LIKE or IN filters + vector remaining_filters_to_remove; for (idx_t rem_fil_idx = 0; rem_fil_idx < remaining_filters.size(); rem_fil_idx++) { auto &remaining_filter = remaining_filters[rem_fil_idx]; if (remaining_filter->expression_class == ExpressionClass::BOUND_FUNCTION) { @@ -522,6 +533,14 @@ TableFilterSet FilterCombiner::GenerateTableScanFilters(vector &column_id // e.g. if we have x IN (1, 2, 3, 4, 5) we transform this into x >= 1 AND x <= 5 if (!type.IsIntegral()) { continue; +// for (idx_t i = 1; i < func.children.size(); i++) { +// auto &child = func.children[i]; +// auto &const_value_expr = child->Cast(); +// auto filter = make_uniq(ExpressionType::COMPARE_EQUAL, const_value_expr.value); +// table_filters.PushFilter(column_index, std::move(filter), TableFilterType::CONJUNCTION_OR); +// } +// remaining_filters_to_remove.push_back(rem_fil_idx); +// continue; } bool can_simplify_in_clause = true; @@ -546,6 +565,13 @@ TableFilterSet FilterCombiner::GenerateTableScanFilters(vector &column_id } } if (!can_simplify_in_clause) { + // make one big conjunction OR + for (auto &in_val : in_values) { + auto filter = + make_uniq(ExpressionType::COMPARE_EQUAL, Value::Numeric(type, in_val)); + table_filters.PushFilter(column_index, std::move(filter), TableFilterType::CONJUNCTION_OR); + } + remaining_filters_to_remove.push_back(rem_fil_idx); continue; } auto lower_bound = make_uniq(ExpressionType::COMPARE_GREATERTHANOREQUALTO, @@ -556,10 +582,74 @@ TableFilterSet FilterCombiner::GenerateTableScanFilters(vector &column_id table_filters.PushFilter(column_index, std::move(upper_bound)); table_filters.PushFilter(column_index, make_uniq()); - remaining_filters.erase(remaining_filters.begin() + rem_fil_idx); + remaining_filters_to_remove.push_back(rem_fil_idx); + } else if (remaining_filter->type == ExpressionType::CONJUNCTION_OR) { +// continue; + unordered_set columns_that_need_and_null; + auto &conj = remaining_filter->Cast(); + auto or_filter = make_uniq(); + bool all_can_pushdown = true; + idx_t column_id = DConstants::INVALID_INDEX; + for (auto &expr : conj.children) { + // go through every child of the conjunction and check that it can be pushed down + // if not a direct comparison skip + if (expr->expression_class != ExpressionClass::BOUND_COMPARISON) { + all_can_pushdown = false; + continue; + } + auto &comp = expr->Cast(); + BoundColumnRefExpression *colref = nullptr; + BoundConstantExpression *value = nullptr; + bool can_pushdown = false; + // if not a simple comparison between bound column index and constant value, skip + if (TableFilterSet::ExpressionSupportsPushdown(comp.type)) { + if (LeftConstValRightBoundColref(*comp.left, *comp.right)) { + value = &comp.left->Cast(); + colref = &comp.right->Cast(); + can_pushdown = true; + } else if (LeftConstValRightBoundColref(*comp.right, *comp.left)) { + value = &comp.right->Cast(); + colref = &comp.left->Cast(); + can_pushdown = true; + } + } + if (!can_pushdown) { + all_can_pushdown = false; + break; + } + auto column_index = column_ids[colref->binding.column_index]; + if (column_index == COLUMN_IDENTIFIER_ROW_ID) { + all_can_pushdown = false; + break; + } + // if conjunction has two separate column_ids, skip + // (i.e a = 5 or b = 7) + if (column_id == DConstants::INVALID_INDEX) { + column_id = column_index; + } + if (column_id != column_index) { + all_can_pushdown = false; + break; + } + auto column_filter = make_uniq(comp.type, value->value); + or_filter->child_filters.push_back(std::move(column_filter)); + columns_that_need_and_null.insert(column_index); + } + // if all of the expressions in the conjunction or can be pushed down, then add add them to the + // table filters. Otherwise leave it as a filter on the read. + if (all_can_pushdown) { + table_filters.PushFilter(column_id, std::move(or_filter), TableFilterType::CONJUNCTION_OR); +// table_filters.PushFilter(column_id, make_uniq(), TableFilterType::CONJUNCTION_AND); + remaining_filters_to_remove.push_back(rem_fil_idx); + } } } + for (int i = remaining_filters_to_remove.size() - 1; i >= 0; i--) { + auto remaining_filter_index = remaining_filters_to_remove.at(i); + remaining_filters.erase(remaining_filters.begin() + remaining_filter_index); + } + // GenerateORFilters(table_filters, column_ids); return table_filters; diff --git a/src/optimizer/optimizer.cpp b/src/optimizer/optimizer.cpp index 0a66f0007761..8b5f37339683 100644 --- a/src/optimizer/optimizer.cpp +++ b/src/optimizer/optimizer.cpp @@ -176,7 +176,7 @@ unique_ptr Optimizer::Optimize(unique_ptr plan column_lifetime.VisitOperator(*plan); }); - // compress data based on statistics for materializing operators +// // compress data based on statistics for materializing operators RunOptimizer(OptimizerType::COMPRESSED_MATERIALIZATION, [&]() { CompressedMaterialization compressed_materialization(context, binder, std::move(statistics_map)); compressed_materialization.Compress(plan); diff --git a/src/planner/table_filter.cpp b/src/planner/table_filter.cpp index 0f6f2b0bfc86..e267b3e4dd00 100644 --- a/src/planner/table_filter.cpp +++ b/src/planner/table_filter.cpp @@ -5,7 +5,7 @@ namespace duckdb { -void TableFilterSet::PushFilter(idx_t column_index, unique_ptr filter) { +void TableFilterSet::PushFilter(idx_t column_index, unique_ptr filter, TableFilterType conjunction_type) { auto entry = filters.find(column_index); if (entry == filters.end()) { // no filter yet: push the filter directly @@ -13,9 +13,31 @@ void TableFilterSet::PushFilter(idx_t column_index, unique_ptr filt } else { // there is already a filter: AND it together if (entry->second->filter_type == TableFilterType::CONJUNCTION_AND) { + if (conjunction_type == TableFilterType::CONJUNCTION_OR) { + throw InternalException("Conjunction AND should not be connecting conjunction ors."); + } auto &and_filter = entry->second->Cast(); and_filter.child_filters.push_back(std::move(filter)); + } else if (entry->second->filter_type == TableFilterType::CONJUNCTION_OR) { + // Or filter connecting ands + if (conjunction_type == TableFilterType::CONJUNCTION_AND) { + auto and_filter = make_uniq(); + and_filter->child_filters.push_back(std::move(entry->second)); + and_filter->child_filters.push_back(std::move(filter)); + filters[column_index] = std::move(and_filter); + return; + } + auto &or_filter = entry->second->Cast(); + or_filter.child_filters.push_back(std::move(filter)); } else { + if (conjunction_type == TableFilterType::CONJUNCTION_OR) { + auto or_filter = make_uniq(); + or_filter->child_filters.push_back(std::move(entry->second)); + or_filter->child_filters.push_back(std::move(filter)); + filters[column_index] = std::move(or_filter); + return; + } + D_ASSERT(conjunction_type == TableFilterType::CONJUNCTION_AND); auto and_filter = make_uniq(); and_filter->child_filters.push_back(std::move(entry->second)); and_filter->child_filters.push_back(std::move(filter)); diff --git a/src/storage/table/column_segment.cpp b/src/storage/table/column_segment.cpp index b97c12291f66..3840603814bf 100644 --- a/src/storage/table/column_segment.cpp +++ b/src/storage/table/column_segment.cpp @@ -340,6 +340,18 @@ idx_t ColumnSegment::FilterSelection(SelectionVector &sel, Vector &result, const // similar to the CONJUNCTION_AND, but we need to take care of the SelectionVectors (OR all of them) idx_t count_total = 0; SelectionVector result_sel(approved_tuple_count); + +// SelectionVector current_sel = result_sel; +// idx_t current_count = approved_tuple_count; +// idx_t result_count = 0; +// +// unique_ptr temp_true, temp_false; +// unique_ptr false_sel; +// +// temp_true = make_uniq(STANDARD_VECTOR_SIZE); +// +// temp_false = make_uniq(STANDARD_VECTOR_SIZE); + auto &conjunction_or = filter.Cast(); for (auto &child_filter : conjunction_or.child_filters) { SelectionVector temp_sel; @@ -347,6 +359,13 @@ idx_t ColumnSegment::FilterSelection(SelectionVector &sel, Vector &result, const idx_t temp_tuple_count = approved_tuple_count; idx_t temp_count = FilterSelection(temp_sel, result, *child_filter, temp_tuple_count, mask); // tuples passed, move them into the actual result vector +// if (temp_count > 0) { +// for (idx_t i = 0; i < temp_count; i++) { +// result_sel.set_index(result_count++, temp_true->get_index(i)); +// } +// current_count -= temp_count; +// +// } for (idx_t i = 0; i < temp_count; i++) { auto new_idx = temp_sel.get_index(i); bool is_new_idx = true; diff --git a/src/storage/table/row_group.cpp b/src/storage/table/row_group.cpp index 73fe4e6a373b..a9cacaadfefe 100644 --- a/src/storage/table/row_group.cpp +++ b/src/storage/table/row_group.cpp @@ -20,6 +20,7 @@ #include "duckdb/common/serializer/serializer.hpp" #include "duckdb/common/serializer/deserializer.hpp" #include "duckdb/common/serializer/binary_serializer.hpp" +#include "iostream" namespace duckdb { @@ -267,7 +268,7 @@ unique_ptr RowGroup::AlterType(RowGroupCollection &new_collection, con } unique_ptr RowGroup::AddColumn(RowGroupCollection &new_collection, ColumnDefinition &new_column, - ExpressionExecutor &executor, Expression &default_value, Vector &result) { + ExpressionExecutor &executor, Expression &default_value, Vector &result) { Verify(); // construct a new column data for the new column @@ -394,11 +395,12 @@ void RowGroup::TemplatedScan(TransactionData transaction, CollectionScanState &s const bool ALLOW_UPDATES = TYPE != TableScanType::TABLE_SCAN_COMMITTED_ROWS_DISALLOW_UPDATES && TYPE != TableScanType::TABLE_SCAN_COMMITTED_ROWS_OMIT_PERMANENTLY_DELETED; auto table_filters = state.GetFilters(); + const auto &column_ids = state.GetColumnIds(); auto adaptive_filter = state.GetAdaptiveFilter(); while (true) { if (state.vector_index * STANDARD_VECTOR_SIZE >= state.max_row_group_row) { - // exceeded the amount of rows to scan + // exceeded the amount of rows to scan return; } idx_t current_row = state.vector_index * STANDARD_VECTOR_SIZE; @@ -462,12 +464,46 @@ void RowGroup::TemplatedScan(TransactionData transaction, CollectionScanState &s if (table_filters) { D_ASSERT(adaptive_filter); D_ASSERT(ALLOW_UPDATES); + + // are we apply the whole OR conjunction filter at once. and then slice it. + // What we should do instead is, if the filter is a conjunction OR filter, apply every individual child + // update the selection vector with tuples that pass, then apply the next conjunction to the indexes that did not pass. for (idx_t i = 0; i < table_filters->filters.size(); i++) { + auto tf_idx = adaptive_filter->permutation[i]; auto col_idx = column_ids[tf_idx]; auto &col_data = GetColumn(col_idx); - col_data.Select(transaction, state.vector_index, state.column_scans[tf_idx], result.data[tf_idx], - sel, approved_tuple_count, *table_filters->filters[tf_idx]); + auto &filter = table_filters->filters[tf_idx]; + + switch (filter->filter_type) { + case TableFilterType::CONJUNCTION_OR: { + + auto &or_filter = filter->Cast(); + SelectionVector current_sel = sel; + SelectionVector *true_sel = nullptr; + SelectionVector *false_sel = nullptr; + + // scan and flatten the vector. + // apply children one by one unti + for (idx_t i = 0; i < or_filter.child_filters.size(); i++) { + idx_t tcount = 0; + auto &child_filter_expression = or_filter.child_filters.at(i); + if (child_filter_expression->filter_type == TableFilterType::CONSTANT_COMPARISON) { + auto context = transaction.transaction.get()->context; + auto expression_executor = ExpressionExecutor(&context); + ColumnSegment::FilterSelection(sel, result.data.at(tf_idx), *child_filter_expression, + tcount, FlatVector::Validity(result.data.at(tf_idx))); + } + } + + + } + } + case TableFilterType::CONJUNCTION_AND: + default: { + col_data.Select(transaction, state.vector_index, state.column_scans[tf_idx], result.data[tf_idx], + sel, approved_tuple_count, *table_filters->filters[tf_idx]); + } } for (auto &table_filter : table_filters->filters) { result.data[table_filter.first].Slice(sel, approved_tuple_count); diff --git a/test/optimizer/pushdown/parquet_or_pushdown.test b/test/optimizer/pushdown/parquet_or_pushdown.test index bc4b5418c87b..5e6c9d2c8468 100644 --- a/test/optimizer/pushdown/parquet_or_pushdown.test +++ b/test/optimizer/pushdown/parquet_or_pushdown.test @@ -7,25 +7,31 @@ require parquet statement ok PRAGMA enable_verification -# FIXME: re-enable when or pushdown is fixed mode skip +#statement ok +#copy (select range as a, (range % 3 == 0)::BOOL as b from range(100)) to '__TEST_DIR__/parquet_or_filter.parquet' (FORMAT PARQUET); + # Multiple column in the root OR node, don't push down query II -EXPLAIN SELECT tbl.a, tbl.b FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a, b) WHERE a=1 OR b=false +SELECT tbl.a, tbl.b FROM 'delete_me.parquet' tbl(a, b) WHERE a = 2 or a = 5 or a = 9 order by a; ---- -physical_plan :.*PARQUET_SCAN.*Filters:.* +2 false +5 false +9 true # Single column in the root OR node query II -EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a) WHERE a=1 OR a=2 +EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a) WHERE a=1 OR a=2; ---- physical_plan :.*PARQUET_SCAN.*Filters: a=1 OR a=2.* +mode skip + # Single column + root OR node with AND query II -EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a) WHERE a=1 OR (a>3 AND a<5) +EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a) WHERE a=1 OR (a>3 AND a<5); ---- physical_plan :.*PARQUET_SCAN.*Filters: a=1 OR a>3 AND a<5|.* @@ -37,6 +43,13 @@ EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tb physical_plan :.*PARQUET_SCAN.*Filters: a=1 OR a>3 OR a<5|.* +# No Filter on top of the parquet scan, filters can be applied in the read +query II +EXPLAIN SELECT tbl.a FROM "data/parquet-testing/arrow/alltypes_plain.parquet" tbl(a) WHERE a=1 OR a>3 OR a<5 +---- +physical_plan :.*FILTER.*PARQUET_SCAN.* + + # Testing not equal query II diff --git a/test/optimizer/pushdown/pushdown_or.test b/test/optimizer/pushdown/pushdown_or.test new file mode 100644 index 000000000000..17a214370570 --- /dev/null +++ b/test/optimizer/pushdown/pushdown_or.test @@ -0,0 +1,62 @@ +# name: test/optimizer/pushdown/pushdown_or.test +# description: Test Parquet With Pushing Down of OR Filters +# group: [pushdown] + +require parquet + +statement ok +PRAGMA enable_verification + +statement ok +create table t0 as select range as a, range/2 as b, range/3 as c, range/4 as d from range(100); + +# cannot push down or filters that involve separate columns +query II +explain select * from t0 where a = 5 or b = 8 or c = 20 or d = 10; +---- +physical_plan :.*FILTER.*SEQ_SCAN.* + +# can push down or filters if it is a conjunction or on the same column +query II +explain select * from t0 where a = 5 or a = 10 or a = 15 or a = 12; +---- +physical_plan :.*SEQ_SCAN.*Filters.*OR.* + +# can push down two or filters combined by an and, given the or filters are both on the same column +query II +explain select * from t0 where (a = 5 or a = 10) and (b = 15 or b = 12); +---- +physical_plan :.*SEQ_SCAN.*Filters.*OR.* + +query II +explain select * from t0 where (a = 5 or a = 10) and (b = 15 or b = 12); +---- +physical_plan :.*FILTER.*SEQ_SCAN.* + +# two or conjunction filters on separate column ids are not pushed down +query II +explain select * from t0 where (a = 5 or b = 10) and (c = 15 or d = 12); +---- +physical_plan :.*FILTER.*SEQ_SCAN.* + +# in filter can be pushed down +query II +explain select * from t0 where a in (1, 2, 3, 4, 10, 19, 25); +---- +physical_plan :.*SEQ_SCAN.*Filters.*OR.* + +# test strings and bool + +statement ok +create table t1 as select unnest(['happy', 'sad', 'angry', 'ok', 'medium']) as mood, unnest([true, false, true, false, true]) as col1; + +query II +explain select * from t1 where mood in ('happy', 'ok'); +---- +physical_plan :.*SEQ_SCAN.*Filters.*OR.* + +query II +explain select * from t1 where col1 = true or col1 = false; +---- +physical_plan :.*SEQ_SCAN.*Filters.*OR.* + diff --git a/test/sql/conjunction/or_comparison.test b/test/sql/conjunction/or_comparison.test index aaa4b994c0e6..3e9a42930133 100644 --- a/test/sql/conjunction/or_comparison.test +++ b/test/sql/conjunction/or_comparison.test @@ -6,7 +6,7 @@ statement ok PRAGMA enable_verification; statement ok -CREATE TABLE tab0(pk INTEGER PRIMARY KEY, col0 INTEGER, col1 FLOAT, col2 VARCHAR, col3 INTEGER, col4 FLOAT, col5 VARCHAR);; +CREATE TABLE tab0(pk INTEGER PRIMARY KEY, col0 INTEGER, col1 FLOAT, col2 VARCHAR, col3 INTEGER, col4 FLOAT, col5 VARCHAR); statement ok INSERT INTO tab0 VALUES(0,86,94.959999084472652697,'vuopk',91,47.779998779296875,'sikuk'); diff --git a/test/sql/optimizer/expression/test_conjunction_optimization.test b/test/sql/optimizer/expression/test_conjunction_optimization.test index a7be8ade834e..dc0f528c7ae8 100644 --- a/test/sql/optimizer/expression/test_conjunction_optimization.test +++ b/test/sql/optimizer/expression/test_conjunction_optimization.test @@ -12,11 +12,11 @@ statement ok CREATE TABLE integers(i INTEGER); statement ok -INSERT INTO integers VALUES (1), (2), (3), (NULL) +INSERT INTO integers VALUES (1), (2), (3), (NULL); # test conjunctions in FILTER clause query I -SELECT i FROM integers WHERE (i=1 AND i>0) OR (i=1 AND i<3) ORDER BY i +SELECT i FROM integers WHERE (i=1 AND i>0) OR (i=1 AND i<3) ORDER BY i; ---- 1