Merge 'secondary_index: Fix TOKEN() restrictions in indexed SELECTs' from Jan Ciołek

This is a rewrite of an old PR: #7582

`TOKEN()` restrictions don't work properly when a query uses an index.
For example this returns both rows:
```cql
CREATE TABLE t(pk int, ck int, v int, PRIMARY KEY(pk, ck));
CREATE INDEX ON t(v);
INSERT INTO t (pk, ck, v) VALUES (0, 0, 0);
INSERT INTO t (pk, ck, v) VALUES (1, 0, 0);
SELECT token(pk), pk, ck, v FROM t WHERE v = 0 AND token(pk) = token(0) ALLOW FILTERING;
```

This functionality is supported on both old and new indexes.  In old
indexes the type of the token column was `blob`.  This causes problems,
because `blob` representation of tokens is ordered differently. Tokens
represented as blobs are ordered like this:
```
0, 1, 2, 3, 4, 5, ..., bigint_max, bigint_min, ...., -5, -4, -3, -2, -1
```
Because of that clustering range for `token()` restrictions needs to be
translated to two clustering ranges on the `blob` column.

To create old indexes disable the feature called:
`CORRECT_IDX_TOKEN_IN_SECONDARY_INDEX` or run scylla version from branch
[`cvybhu/si-token2-old-index`](https://github.com/cvybhu/scylla/commits/si-token2-old-index)

I'm not sure if it's possible to create automatic tests with old
indexes. I ran `dev-test` manually on the `si-token2-old-index` branch,
and the only tests that failed were the ones testing row ordering. Rows
should be ordered by `token`, but because in old indexes the token is
represented as a `blob` this ordering breaks. This is a known issue
(#7443), that has been fixed by introducing new indexes.

To sum up:
* `token()` restrictions are fixed on both new and old indexes.
* When using old indexes, the rows are not properly ordered by token.
* With new indexes the rows are properly ordered by token.

Fixes #7043

Closes #9067

* github.com:scylladb/scylla:
  tests: add secondary index tests with TOKEN clause
  secondary_index_test: extract test data
  secondary_index: Fix TOKEN() restrictions in indexed SELECTs
  expression: Add replace_token function
This commit is contained in:
Piotr Sarna
2021-07-22 10:22:45 +02:00
6 changed files with 443 additions and 14 deletions

View File

@@ -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:

View File

@@ -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) :

View File

@@ -1068,6 +1068,112 @@ std::vector<query::clustering_range> 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<query::clustering_range> 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<value_list, nonwrapping_range<managed_bytes>> values =
possible_lhs_values(&token_column_bigint, new_token_restrictions, options);
return std::visit(overloaded_functor {
[](const value_list& list) {
std::vector<query::clustering_range> ck_ranges;
ck_ranges.reserve(list.size());
for (auto&& value : list) {
ck_ranges.emplace_back(query::clustering_range::make_singular(std::vector<managed_bytes>{value}));
}
return ck_ranges;
},
[](const nonwrapping_interval<managed_bytes>& 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<int64_t>((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<int64_t>::min();
int64_t token_high = std::numeric_limits<int64_t>::max();
bool low_inclusive = true, high_inclusive = true;
const std::optional<interval_bound<managed_bytes>>& start = range.start();
const std::optional<interval_bound<managed_bytes>>& 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<query::clustering_range> 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<query::clustering_range::bound>;
/// 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) >/</= ? restriction, it is not allowed to have restrictions on p1 or p2.
// This means that p1 and p2 can have many different values (token is a hash, can have collisions).
// Clustering prefix ends after token_restriction, all further restrictions have to be filtered.
expr::expression token_restriction = replace_token(_partition_key_restrictions->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<expr::expression>(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<column_value>(*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<query::clustering_range> statement_restrictions::get_global_index_cl
return get_single_column_clustering_bounds(options, idx_tbl_schema, *_idx_tbl_ck_prefix);
}
std::vector<query::clustering_range> 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

View File

@@ -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<query::clustering_range> 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<query::clustering_range> get_global_index_token_clustering_ranges(
const query_options& options, const schema& idx_tbl_schema) const;
};
}

View File

@@ -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));
}
}

View File

@@ -1533,6 +1533,27 @@ SEASTAR_TEST_CASE(test_computed_columns) {
});
}
// Sorted by token value. See testset_tokens for token values.
static const std::vector<std::vector<bytes_opt>> 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<int64_t> 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<std::vector<bytes_opt>> 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<std::vector<bytes_opt>> 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<std::pair<int, int>, 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<testset_row> 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<bool(const testset_row&)> matches_row) {
auto expected_rows = boost::copy_range<std::vector<std::vector<bytes_opt>>>(rows |
boost::adaptors::filtered(std::move(matches_row)) | boost::adaptors::transformed([] (const testset_row& row) {
return std::vector<bytes_opt> {
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) {