Yeah, unfortunately, there’s not a single function for that. In theory, it’s logically equivalent to something like:
Query.or(
Query.and(Query.field("a").equalsValue(1), Query.field("b").equalsValue(2), Query.field("c").greaterThan(3)),
Query.and(Query.field("a").equalsValue(1), Query.field("b").greaterThan(2)),
Query.field("a").greaterThan(1)
)
But, (1) that’s a bit unwieldy and (2) the planner isn’t quite smart to notice that if you have an index on concatenateFields("a", "b", "c"), the given ranges can be used to turn that into a single scan.
If you did introduce a tupleize function and made it a QueryableKeyExpression, then you certainly could index the result of that function. Assuming the index exists, the planner should chose it for you with something like the expression you suggested.
It’s a bit unsatsifying, but I think the most straightforward way to get this to work is actually to forego the planner and just issue an index scan directly. Something like:
recordStore.scanIndexRecords(
indexName,
IndexScanType.BY_VALUE,
new TupleRange(Tuple.from(1, 2, 3), null, EndpointType.RANGE_EXCLUSIVE, EndpointType.TREE_END),
continuation // null if starting from the beginning; previous run's continuation if resuming a scan
scanProperties // specifies forward/reverse as well as limits
);
If you know you have the index you want, this can be simpler than trying to get the planner to produce the thing you want.