diff --git a/cql3/expr/expression.cc b/cql3/expr/expression.cc index dca08e480f..75ab313a74 100644 --- a/cql3/expr/expression.cc +++ b/cql3/expr/expression.cc @@ -903,6 +903,27 @@ expression replace_column_def(const expression& expr, const column_definition* n }, expr); } +expression replace_token(const expression& expr, const column_definition* new_cdef) { + return std::visit(overloaded_functor{ + [] (bool b) { return expression(b); }, + [&] (const conjunction& conj) { + const auto applied = conj.children | transformed( + std::bind(replace_token, std::placeholders::_1, new_cdef)); + return expression(conjunction{std::vector(applied.begin(), applied.end())}); + }, + [&] (const binary_operator& oper) { + return expression(binary_operator(replace_token(*oper.lhs, new_cdef), oper.op, oper.rhs)); + }, + [&] (const column_value&) { + return expr; + }, + [&] (const column_value_tuple&) -> expression { + throw std::logic_error(format("replace_token invalid with column tuple: {}", to_string(expr))); + }, + [&] (const token&) -> expression { return column_value{new_cdef}; } + }, expr); +} + std::ostream& operator<<(std::ostream& s, oper_t op) { switch (op) { case oper_t::EQ: diff --git a/cql3/expr/expression.hh b/cql3/expr/expression.hh index e658dde5d2..69ae6f83d4 100644 --- a/cql3/expr/expression.hh +++ b/cql3/expr/expression.hh @@ -281,6 +281,10 @@ extern bool is_on_collection(const binary_operator&); /// column_value. extern expression replace_column_def(const expression&, const column_definition*); +// Replaces all occurences of token(p1, p2) on the left hand side with the given colum. +// For example this changes token(p1, p2) < token(1, 2) to my_column_name < token(1, 2). +extern expression replace_token(const expression&, const column_definition*); + inline oper_t pick_operator(statements::bound b, bool inclusive) { return is_start(b) ? (inclusive ? oper_t::GTE : oper_t::GT) : diff --git a/cql3/restrictions/statement_restrictions.cc b/cql3/restrictions/statement_restrictions.cc index c511a19970..37639b1067 100644 --- a/cql3/restrictions/statement_restrictions.cc +++ b/cql3/restrictions/statement_restrictions.cc @@ -1068,6 +1068,112 @@ std::vector get_single_column_clustering_bounds( return ck_ranges; } +// In old v1 indexes the token column was of type blob. +// This causes problems because blobs are sorted differently than the bigint token values that they represent. +// Tokens are encoded as 8 byte big endian two's complement signed integers, +// which with blob sorting makes them ordered like this: +// 0, 1, 2, 3, 4, 5, ..., bigint_max, bigint_min, ...., -5, -4, -3, -2, -1 +// Because of this clustering restrictions like token(p) > -4 and token(p) < 4 need to be translated +// to two clustering ranges on the old index. +// All binary_operators in token_restriction must have column_value{token_column} as their LHS. +static std::vector get_index_v1_token_range_clustering_bounds( + const query_options& options, + const column_definition& token_column, + const expression& token_restriction) { + + // A workaround in order to make possible_lhs_values work properly. + // possible_lhs_values looks at the column type and uses this type's comparator. + // This is a problem because when using blob's comparator, -4 is greater than 4. + // This makes possible_lhs_values think that an expression like token(p) > -4 and token(p) < 4 + // is impossible to fulfill. + // Create a fake token column with the type set to bigint, translate the restriction to use this column + // and use this restriction to calculate possible lhs values. + column_definition token_column_bigint = token_column; + token_column_bigint.type = long_type; + expression new_token_restrictions = replace_column_def(token_restriction, &token_column_bigint); + + std::variant> values = + possible_lhs_values(&token_column_bigint, new_token_restrictions, options); + + return std::visit(overloaded_functor { + [](const value_list& list) { + std::vector ck_ranges; + ck_ranges.reserve(list.size()); + + for (auto&& value : list) { + ck_ranges.emplace_back(query::clustering_range::make_singular(std::vector{value})); + } + + return ck_ranges; + }, + [](const nonwrapping_interval& range) { + auto int64_from_be_bytes = [](const managed_bytes& int_bytes) -> int64_t { + if (int_bytes.size() != 8) { + throw std::runtime_error("token restriction value should be 8 bytes"); + } + + return read_be((const char*)&int_bytes[0]); + }; + + auto int64_to_be_bytes = [](int64_t int_val) -> managed_bytes { + managed_bytes int_bytes(managed_bytes::initialized_later{}, 8); + write_be((char*)&int_bytes[0], int_val); + return int_bytes; + }; + + int64_t token_low = std::numeric_limits::min(); + int64_t token_high = std::numeric_limits::max(); + bool low_inclusive = true, high_inclusive = true; + + const std::optional>& start = range.start(); + const std::optional>& end = range.end(); + + if (start.has_value()) { + token_low = int64_from_be_bytes(start->value()); + low_inclusive = start->is_inclusive(); + } + + if (end.has_value()) { + token_high = int64_from_be_bytes(end->value()); + high_inclusive = end->is_inclusive(); + } + + query::clustering_range::bound lower_bound(std::vector{int64_to_be_bytes(token_low)}, low_inclusive); + query::clustering_range::bound upper_bound(std::vector{int64_to_be_bytes(token_high)}, high_inclusive); + + std::vector ck_ranges; + + if (token_high < token_low) { + // Impossible range, return empty clustering ranges + return ck_ranges; + } + + // Blob encoded tokens are sorted like this: + // 0, 1, 2, 3, 4, 5, ..., bigint_max, bigint_min, ...., -5, -4, -3, -2, -1 + // This means that in cases where low >= 0 or high < 0 we can simply use the whole range. + // In other cases we have to take two ranges: (low, -1] and [0, high). + if (token_low >= 0 || token_high < 0) { + ck_ranges.emplace_back(std::move(lower_bound), std::move(upper_bound)); + } else { + query::clustering_range::bound zero_bound(std::vector{int64_to_be_bytes(0)}); + query::clustering_range::bound min1_bound(std::vector{int64_to_be_bytes(-1)}); + + ck_ranges.reserve(2); + + if (!(token_high == 0 && !high_inclusive)) { + ck_ranges.emplace_back(std::move(zero_bound), std::move(upper_bound)); + } + + if (!(token_low == -1 && !low_inclusive)) { + ck_ranges.emplace_back(std::move(lower_bound), std::move(min1_bound)); + } + } + + return ck_ranges; + } + }, values); +} + using opt_bound = std::optional; /// Makes a partial bound out of whole_bound's prefix. If the partial bound is strictly shorter than the whole, it is @@ -1253,6 +1359,18 @@ bool token_known(const statement_restrictions& r) { bool statement_restrictions::need_filtering() const { using namespace expr; + if (_uses_secondary_indexing && has_token(_partition_key_restrictions->expression)) { + // If there is a token(p1, p2) restriction, no p1, p2 restrictions are allowed in the query. + // All other restrictions must be on clustering or regular columns. + int64_t non_pk_restrictions_count = _clustering_columns_restrictions->size(); + non_pk_restrictions_count += _nonprimary_key_restrictions->size(); + + // We are querying using an index, one restriction goes to the index restriction. + // If there are some restrictions other than token() and index column then we need to do filtering. + // p1, p2 can have many different values, so clustering prefix breaks. + return non_pk_restrictions_count > 1; + } + const auto npart = _partition_key_restrictions->size(); if (npart > 0 && npart < _schema->partition_key_size()) { // Can't calculate the token value, so a naive base-table query must be filtered. Same for any index tables, @@ -1355,6 +1473,19 @@ void statement_restrictions::prepare_indexed(const schema& idx_tbl_schema, bool if (is_local || !_partition_range_is_simple) { return; } + + const column_definition* token_column = &idx_tbl_schema.clustering_column_at(0); + + if (has_token(_partition_key_restrictions->expression)) { + // When there is a token(p1, p2) >/expression, token_column); + _idx_tbl_ck_prefix = std::vector{std::move(token_restriction)}; + + return; + } + // If we're here, it means the index cannot be on a partition column: process_partition_key_restrictions() // avoids indexing when _partition_range_is_simple. See _idx_tbl_ck_prefix blurb for its composition. _idx_tbl_ck_prefix = std::vector(1 + _schema->partition_key_size()); @@ -1376,7 +1507,6 @@ void statement_restrictions::prepare_indexed(const schema& idx_tbl_schema, bool const auto col = std::get(*any_binop->lhs).col; _idx_tbl_ck_prefix->push_back(replace_column_def(e, idx_tbl_schema.get_column_definition(col->name()))); } - auto token_column = &idx_tbl_schema.clustering_column_at(0); (*_idx_tbl_ck_prefix)[0] = binary_operator( column_value(token_column), oper_t::EQ, @@ -1419,5 +1549,25 @@ std::vector statement_restrictions::get_global_index_cl return get_single_column_clustering_bounds(options, idx_tbl_schema, *_idx_tbl_ck_prefix); } +std::vector statement_restrictions::get_global_index_token_clustering_ranges( + const query_options& options, + const schema& idx_tbl_schema +) const { + if (!_idx_tbl_ck_prefix.has_value()) { + on_internal_error( + rlogger, "statement_restrictions::get_global_index_token_clustering_ranges called with unprepared index"); + } + + const column_definition& token_column = idx_tbl_schema.clustering_column_at(0); + + // In old indexes the token column was of type blob. + // This causes problems with sorting and must be handled separately. + if (token_column.type != long_type) { + return get_index_v1_token_range_clustering_bounds(options, token_column, _idx_tbl_ck_prefix->at(0)); + } + + return get_single_column_clustering_bounds(options, idx_tbl_schema, *_idx_tbl_ck_prefix); +} + } // namespace restrictions } // namespace cql3 diff --git a/cql3/restrictions/statement_restrictions.hh b/cql3/restrictions/statement_restrictions.hh index b17fcfe8dd..97aabb6bee 100644 --- a/cql3/restrictions/statement_restrictions.hh +++ b/cql3/restrictions/statement_restrictions.hh @@ -208,6 +208,10 @@ public: return _clustering_columns_restrictions; } + bool has_token_restrictions() const { + return has_token(_partition_key_restrictions->expression); + } + /** * Builds a possibly empty collection of column definitions that will be used for filtering * @param db - the database context @@ -445,7 +449,16 @@ public: } bool ck_restrictions_need_filtering() const { - return _partition_key_restrictions->has_unrestricted_components(*_schema) || _clustering_columns_restrictions->needs_filtering(*_schema); + if (_clustering_columns_restrictions->empty()) { + return false; + } + + return _partition_key_restrictions->has_unrestricted_components(*_schema) + || _clustering_columns_restrictions->needs_filtering(*_schema) + // If token restrictions are present in an indexed query, then all other restrictions need to be filtered. + // A single token restriction can have multiple matching partition key values. + // Because of this we can't create a clustering prefix with more than token restriction. + || (_uses_secondary_indexing && has_token(_partition_key_restrictions->expression)); } /** @@ -484,6 +497,10 @@ public: /// Calculates clustering ranges for querying a global-index table. std::vector get_global_index_clustering_ranges( const query_options& options, const schema& idx_tbl_schema) const; + + /// Calculates clustering ranges for querying a global-index table for queries with token restrictions present. + std::vector get_global_index_token_clustering_ranges( + const query_options& options, const schema& idx_tbl_schema) const; }; } diff --git a/cql3/statements/select_statement.cc b/cql3/statements/select_statement.cc index c25d1c7418..e51d9ab97d 100644 --- a/cql3/statements/select_statement.cc +++ b/cql3/statements/select_statement.cc @@ -1158,6 +1158,10 @@ query::partition_slice indexed_table_select_statement::get_partition_slice_for_g if (single_pk_restrictions && single_pk_restrictions->is_all_eq()) { partition_slice_builder.with_ranges( _restrictions->get_global_index_clustering_ranges(options, *_view_schema)); + } else if (_restrictions->has_token_restrictions()) { + // Restrictions like token(p1, p2) < 0 have all partition key components restricted, but require special handling. + partition_slice_builder.with_ranges( + _restrictions->get_global_index_token_clustering_ranges(options, *_view_schema)); } } diff --git a/test/boost/secondary_index_test.cc b/test/boost/secondary_index_test.cc index 337cbdc9c8..a9f1bed25e 100644 --- a/test/boost/secondary_index_test.cc +++ b/test/boost/secondary_index_test.cc @@ -1533,6 +1533,27 @@ SEASTAR_TEST_CASE(test_computed_columns) { }); } +// Sorted by token value. See testset_tokens for token values. +static const std::vector> testset_pks = { + { int32_type->decompose(5) }, + { int32_type->decompose(1) }, + { int32_type->decompose(0) }, + { int32_type->decompose(2) }, + { int32_type->decompose(4) }, + { int32_type->decompose(6) }, + { int32_type->decompose(3) }, +}; + +static const std::vector testset_tokens = { + { -7509452495886106294 }, + { -4069959284402364209 }, + { -3485513579396041028 }, + { -3248873570005575792 }, + { -2729420104000364805 }, + { +2705480034054113608 }, + { +9010454139840013625 }, +}; + // Ref: #3423 - rows should be returned in token order, // using signed comparison (ref: #7443) SEASTAR_TEST_CASE(test_token_order) { @@ -1544,26 +1565,238 @@ SEASTAR_TEST_CASE(test_token_order) { cquery_nofail(e, format("INSERT INTO t (pk, v) VALUES ({}, 1)", i).c_str()); } - std::vector> expected_rows = { - { int32_type->decompose(5) }, // token(pk) = -7509452495886106294 - { int32_type->decompose(1) }, // token(pk) = -4069959284402364209 - { int32_type->decompose(0) }, // token(pk) = -3485513579396041028 - { int32_type->decompose(2) }, // token(pk) = -3248873570005575792 - { int32_type->decompose(4) }, // token(pk) = -2729420104000364805 - { int32_type->decompose(6) }, // token(pk) = +2705480034054113608 - { int32_type->decompose(3) }, // token(pk) = +9010454139840013625 - }; - eventually([&] { auto nonindex_order = cquery_nofail(e, "SELECT pk FROM t"); auto index_order = cquery_nofail(e, "SELECT pk FROM t WHERE v = 1"); - assert_that(nonindex_order).is_rows().with_rows(expected_rows); - assert_that(index_order).is_rows().with_rows(expected_rows); + assert_that(nonindex_order).is_rows().with_rows(testset_pks); + assert_that(index_order).is_rows().with_rows(testset_pks); }); }); } +SEASTAR_TEST_CASE(test_select_with_token_range_cases) { + return do_with_cql_env_thread([] (auto& e) { + cquery_nofail(e, "CREATE TABLE t (pk int, v int, PRIMARY KEY(pk))"); + + for (int i = 0; i < 7; i++) { + // v=1 in each row, so WHERE v = 1 will select them all + cquery_nofail(e, format("INSERT INTO t (pk, v) VALUES ({}, 1)", i).c_str()); + } + + auto get_result_rows = [&](int start, int end) { + std::vector> result; + for (int i = start; i <= end; i++) { + result.push_back({ testset_pks[i][0], int32_type->decompose(1) }); + } + return result; + }; + + auto get_query = [&](sstring token_restriction) { + return format("SELECT * FROM t WHERE v = 1 AND {} ALLOW FILTERING", token_restriction); + }; + + auto q = [&](sstring token_restriction) { + return cquery_nofail(e, get_query(token_restriction).c_str()); + }; + + auto inclusive_inclusive_range = [](int64_t start, int64_t end) { return format("token(pk) >= {} AND token(pk) <= {}", start, end); }; + auto exclusive_inclusive_range = [](int64_t start, int64_t end) { return format("token(pk) > {} AND token(pk) <= {}", start, end); }; + auto inclusive_exclusive_range = [](int64_t start, int64_t end) { return format("token(pk) >= {} AND token(pk) < {}", start, end); }; + auto exclusive_exclusive_range = [](int64_t start, int64_t end) { return format("token(pk) > {} AND token(pk) < {}", start, end); }; + + auto inclusive_infinity_range = [](int64_t start) { return format("token(pk) >= {}", start); }; + auto exclusive_infinity_range = [](int64_t start) { return format("token(pk) > {}", start); }; + + auto infinity_inclusive_range = [](int64_t end) { return format("token(pk) <= {}", end); }; + auto infinity_exclusive_range = [](int64_t end) { return format("token(pk) < {}", end); }; + + auto equal_range = [](int64_t value) { return format("token(pk) = {}", value); }; + + auto do_tests = [&] { + assert_that(q(inclusive_inclusive_range(testset_tokens[1], testset_tokens[5]))).is_rows().with_rows(get_result_rows(1, 5)); + assert_that(q(inclusive_inclusive_range(testset_tokens[1], testset_tokens[5] - 1))).is_rows().with_rows(get_result_rows(1, 4)); + assert_that(q(inclusive_inclusive_range(testset_tokens[1], testset_tokens[5] + 1))).is_rows().with_rows(get_result_rows(1, 5)); + assert_that(q(inclusive_inclusive_range(testset_tokens[1] + 1, testset_tokens[5]))).is_rows().with_rows(get_result_rows(2, 5)); + + assert_that(q(exclusive_inclusive_range(testset_tokens[1], testset_tokens[4]))).is_rows().with_rows(get_result_rows(2, 4)); + + assert_that(q(inclusive_exclusive_range(testset_tokens[1], testset_tokens[4]))).is_rows().with_rows(get_result_rows(1, 3)); + + assert_that(q(exclusive_exclusive_range(testset_tokens[1], testset_tokens[4]))).is_rows().with_rows(get_result_rows(2, 3)); + + assert_that(q(inclusive_infinity_range(testset_tokens[3]))).is_rows().with_rows(get_result_rows(3, testset_pks.size() - 1)); + + assert_that(q(exclusive_infinity_range(testset_tokens[3]))).is_rows().with_rows(get_result_rows(4, testset_pks.size() - 1)); + + assert_that(q(infinity_inclusive_range(testset_tokens[3]))).is_rows().with_rows(get_result_rows(0, 3)); + + assert_that(q(infinity_exclusive_range(testset_tokens[3]))).is_rows().with_rows(get_result_rows(0, 2)); + + assert_that(q("token(pk) < 0")).is_rows().with_rows(get_result_rows(0, 4)); + + assert_that(q("token(pk) > 0")).is_rows().with_rows(get_result_rows(5, testset_pks.size() - 1)); + + assert_that(q(equal_range(testset_tokens[3]))).is_rows().with_rows(get_result_rows(3, 3)); + + // empty range + assert_that(q("token(pk) < 5 AND token(pk) > 100")).is_rows().with_size(0); + + // prepared statement + auto prepared_id = e.prepare(get_query("token(pk) >= ? AND token(pk) <= ?")).get0(); + auto msg = e.execute_prepared(prepared_id, { + cql3::raw_value::make_value(long_type->decompose(testset_tokens[1])), cql3::raw_value::make_value(long_type->decompose(testset_tokens[5])) + }).get0(); + assert_that(msg).is_rows().with_rows(get_result_rows(1, 5)); + + msg = e.execute_prepared(prepared_id, { + cql3::raw_value::make_value(long_type->decompose(testset_tokens[2])), cql3::raw_value::make_value(long_type->decompose(testset_tokens[6])) + }).get0(); + assert_that(msg).is_rows().with_rows(get_result_rows(2, 6)); + }; + + do_tests(); + + cquery_nofail(e, "CREATE INDEX t_global ON t(v)"); + + eventually(do_tests); + + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk), v)"); + + eventually(do_tests); + }); +} + +struct testset_row { + int pk1, pk2; + int64_t token; + int ck1, ck2; + int v, v2; +}; + +SEASTAR_TEST_CASE(test_select_with_token_range_filtering) { + return do_with_cql_env_thread([] (auto& e) { + // Do tests for each column (excluding partition key columns - cannot mix partition column + // restrictions and TOKEN restrictions). First run the query without index, then with global index, + // and finally with local index. + + cquery_nofail(e, "CREATE TABLE t (pk1 int, pk2 int, ck1 int, ck2 int, v int, v2 int, PRIMARY KEY((pk1, pk2), ck1, ck2))"); + + std::map, int64_t> pk_to_token = { + {{3, 3}, -5763496297744201138}, + {{2, 2}, -3974863545882308264}, + {{1, 3}, 793791837967961899}, + {{2, 1}, 1222388547083740924}, + {{2, 3}, 3312996362008120679}, + {{1, 2}, 4881097376275569167}, + {{1, 1}, 5765203080415074583}, + {{3, 2}, 6375086356864122089}, + {{3, 1}, 8740098777817515610} + }; + + std::vector rows; + auto insert_row = [&](testset_row row) { + cquery_nofail(e, format("INSERT INTO t(pk1, pk2, ck1, ck2, v, v2) VALUES ({}, {}, {}, {}, {}, {})", row.pk1, row.pk2, row.ck1, row.ck2, row.v, row.v2).c_str()); + rows.push_back(row); + }; + + for (int pk1 = 1; pk1 <= 3; pk1++) { + for (int pk2 = 1; pk2 <= 3; pk2++) { + insert_row(testset_row{pk1, pk2, pk_to_token[{pk1, pk2}], 1, 1, 1, 1}); + insert_row(testset_row{pk1, pk2, pk_to_token[{pk1, pk2}], 1, 3, 2, 1}); + insert_row(testset_row{pk1, pk2, pk_to_token[{pk1, pk2}], 2, 1, 3, 1}); + } + } + + auto do_test = [&](sstring token_restriction, sstring column_restrictions, std::function matches_row) { + auto expected_rows = boost::copy_range>>(rows | + boost::adaptors::filtered(std::move(matches_row)) | boost::adaptors::transformed([] (const testset_row& row) { + return std::vector { + int32_type->decompose(row.pk1), int32_type->decompose(row.pk2), + int32_type->decompose(row.ck1), int32_type->decompose(row.ck2), + int32_type->decompose(row.v), int32_type->decompose(row.v2) + }; + })); + auto msg = cquery_nofail(e, format("SELECT pk1, pk2, ck1, ck2, v, v2 FROM t WHERE {} AND {} ALLOW FILTERING", token_restriction, column_restrictions).c_str()); + assert_that(msg).is_rows().with_rows_ignore_order(expected_rows); + }; + + auto do_tests_ck1 = [&] { + do_test("token(pk1, pk2) >= token(1, 2)", "ck1 = 1", [&](const testset_row& row) { return row.ck1 == 1 && row.token >= pk_to_token[{1, 2}]; }); + do_test("token(pk1, pk2) >= token(1, 2)", "ck1 = 1 AND v2 = 1", [&](const testset_row& row) { return row.ck1 == 1 && row.v2 == 1 && row.token >= pk_to_token[{1, 2}]; }); + do_test("token(pk1, pk2) >= token(1, 2)", "ck1 = 1 AND ck2 = 3", [&](const testset_row& row) { return row.ck1 == 1 && row.ck2 == 3 && row.token >= pk_to_token[{1, 2}]; }); + do_test("token(pk1, pk2) >= token(1, 2)", "ck1 = 1 AND ck2 = 3 AND v = 2", [&](const testset_row& row) { return row.ck1 == 1 && row.ck2 == 3 && row.v == 2 && row.token >= pk_to_token[{1, 2}]; }); + do_test("token(pk1, pk2) >= token(1, 2)", "ck1 = 1 AND v = 3", [&](const testset_row& row) { return row.ck1 == 1 && row.v == 3 && row.token >= pk_to_token[{1, 2}]; }); + }; + + auto do_tests_ck2 = [&] { + do_test("token(pk1, pk2) <= token(1, 3)", "ck2 = 1", [&](const testset_row& row) { return row.ck2 == 1 && row.token <= pk_to_token[{1, 3}]; }); + do_test("token(pk1, pk2) <= token(1, 3)", "ck2 = 1 AND ck1 = 1", [&](const testset_row& row) { return row.ck2 == 1 && row.ck1 == 1 && row.token <= pk_to_token[{1, 3}]; }); + do_test("token(pk1, pk2) <= token(1, 3)", "ck2 = 1 AND v2 = 1", [&](const testset_row& row) { return row.ck2 == 1 && row.v2 == 1 && row.token <= pk_to_token[{1, 3}]; }); + }; + + auto do_tests_v = [&] { + do_test("token(pk1, pk2) <= token(3, 2)", "v = 2", [&](const testset_row& row) { return row.v == 2 && row.token <= pk_to_token[{3, 2}]; }); + do_test("token(pk1, pk2) <= token(3, 2)", "v = 2 AND ck1 = 1", [&](const testset_row& row) { return row.v == 2 && row.ck1 == 1 && row.token <= pk_to_token[{3, 2}]; }); + do_test("token(pk1, pk2) <= token(3, 2)", "v = 2 AND ck1 = 1 AND ck2 = 3", [&](const testset_row& row) { return row.v == 2 && row.ck1 == 1 && row.ck2 == 3 && row.token <= pk_to_token[{3, 2}]; }); + }; + + auto do_tests_v2 = [&] { + do_test("token(pk1, pk2) <= token(3, 2)", "v2 = 1", [&](const testset_row& row) { return row.v2 == 1 && row.token <= pk_to_token[{3, 2}]; }); + do_test("token(pk1, pk2) <= token(3, 2)", "v2 = 1 AND ck1 = 1", [&](const testset_row& row) { return row.v2 == 1 && row.ck1 == 1 && row.token <= pk_to_token[{3, 2}]; }); + do_test("token(pk1, pk2) <= token(3, 2)", "v2 = 1 AND ck1 = 1 AND ck2 = 1", [&](const testset_row& row) { return row.v2 == 1 && row.ck1 == 1 && row.ck2 == 1 && row.token <= pk_to_token[{3, 2}]; }); + do_test("token(pk1, pk2) <= token(3, 2)", "v2 = 1 AND ck2 = 1", [&](const testset_row& row) { return row.v2 == 1 && row.ck2 == 1 && row.token <= pk_to_token[{3, 2}]; }); + }; + + do_tests_ck1(); + cquery_nofail(e, "CREATE INDEX t_global ON t(ck1)"); + eventually(do_tests_ck1); + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk1, pk2), ck1)"); + eventually(do_tests_ck1); + cquery_nofail(e, "DROP INDEX t_local"); + + do_tests_ck2(); + cquery_nofail(e, "CREATE INDEX t_global ON t(ck2)"); + eventually(do_tests_ck2); + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk1, pk2), ck2)"); + eventually(do_tests_ck2); + cquery_nofail(e, "DROP INDEX t_local"); + + do_tests_v(); + cquery_nofail(e, "CREATE INDEX t_global ON t(v)"); + eventually(do_tests_v); + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk1, pk2), v)"); + eventually(do_tests_v); + cquery_nofail(e, "DROP INDEX t_local"); + + do_tests_v2(); + cquery_nofail(e, "CREATE INDEX t_global ON t(v2)"); + eventually(do_tests_v2); + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk1, pk2), v2)"); + eventually(do_tests_v2); + cquery_nofail(e, "DROP INDEX t_local"); + + // Two indexes + cquery_nofail(e, "CREATE INDEX t_global ON t(v2)"); + cquery_nofail(e, "CREATE INDEX t_global2 ON t(ck2)"); + eventually(do_tests_v2); + eventually(do_tests_ck2); + cquery_nofail(e, "DROP INDEX t_global"); + cquery_nofail(e, "DROP INDEX t_global2"); + cquery_nofail(e, "CREATE INDEX t_local ON t((pk1, pk2), v2)"); + cquery_nofail(e, "CREATE INDEX t_local2 ON t((pk1, pk2), ck2)"); + eventually(do_tests_v2); + eventually(do_tests_ck2); + cquery_nofail(e, "DROP INDEX t_local"); + cquery_nofail(e, "DROP INDEX t_local2"); + }); +} + // Ref: #5708 - filtering should be applied on an indexed column // if the restriction is not eligible for indexing (it's not EQ) SEASTAR_TEST_CASE(test_filtering_indexed_column) {