Skip to content
1 change: 1 addition & 0 deletions cpp/arcticdb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ set(arcticdb_srcs
processing/operation_dispatch_binary_operator_minus.cpp
processing/operation_dispatch_binary_operator_times.cpp
processing/operation_dispatch_binary_operator_divide.cpp
processing/operation_dispatch_binary_operator_mod.cpp
processing/operation_dispatch_ternary.cpp
processing/query_planner.cpp
processing/sorted_aggregation.cpp
Expand Down
9 changes: 9 additions & 0 deletions cpp/arcticdb/processing/expression_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ std::variant<BitSetTag, DataType> ExpressionNode::compute(
case OperationType::SUB:
case OperationType::MUL:
case OperationType::DIV:
case OperationType::MOD:
user_input::check<ErrorCode::E_INVALID_USER_ARGUMENT>(
std::holds_alternative<DataType>(left_type),
"Unexpected bitset input as left operand to {}",
Expand Down Expand Up @@ -226,6 +227,14 @@ std::variant<BitSetTag, DataType> ExpressionNode::compute(
res = data_type_from_raw_type<TargetType>();
break;
}
case OperationType::MOD: {
using TargetType = typename binary_operation_promoted_type<
Comment thread
academy-codex marked this conversation as resolved.
typename left_type_info::RawType,
typename right_type_info::RawType,
std::remove_reference_t<ModOperator>>::type;
res = data_type_from_raw_type<TargetType>();
break;
}
default:
internal::raise<ErrorCode::E_ASSERTION_FAILURE>("Unexpected binary operator");
}
Expand Down
2 changes: 2 additions & 0 deletions cpp/arcticdb/processing/operation_dispatch_binary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ VariantData dispatch_binary(const VariantData& left, const VariantData& right, O
return visit_binary_operator(left, right, TimesOperator{});
case OperationType::DIV:
return visit_binary_operator(left, right, DivideOperator{});
case OperationType::MOD:
return visit_binary_operator(left, right, ModOperator{});
case OperationType::EQ:
return visit_binary_comparator(left, right, EqualsOperator{});
case OperationType::NE:
Expand Down
2 changes: 2 additions & 0 deletions cpp/arcticdb/processing/operation_dispatch_binary.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,8 @@ extern template VariantData visit_binary_operator<
arcticdb::TimesOperator>(const VariantData&, const VariantData&, TimesOperator&&);
extern template VariantData visit_binary_operator<
arcticdb::DivideOperator>(const VariantData&, const VariantData&, DivideOperator&&);
extern template VariantData visit_binary_operator<
arcticdb::ModOperator>(const VariantData&, const VariantData&, ModOperator&&);

// instantiated in operation_dispatch_binary_comparator.cpp to reduce compilation memory use
extern template VariantData visit_binary_comparator<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright 2026 Man Group Operations Limited
*
* Use of this software is governed by the Business Source License 1.1 included in the file licenses/BSL.txt.
*
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software
* will be governed by the Apache License, version 2.0.
*/
#include <arcticdb/processing/operation_dispatch_binary.hpp>

namespace arcticdb {
template VariantData visit_binary_operator<ModOperator>(const VariantData&, const VariantData&, ModOperator&&);
}
28 changes: 28 additions & 0 deletions cpp/arcticdb/processing/operation_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <unordered_set>
#include <optional>
#include <cmath>

#include <arcticdb/processing/signed_unsigned_comparison.hpp>
#include <arcticdb/util/constants.hpp>
Expand Down Expand Up @@ -51,6 +52,8 @@ enum class OperationType : uint8_t {
AND,
OR,
XOR,
// Operator (kept here to preserve existing enum values)
Comment thread
academy-codex marked this conversation as resolved.
Outdated
MOD,
// Ternary
TERNARY
};
Expand All @@ -70,6 +73,7 @@ inline std::string_view operation_type_to_str(const OperationType ot) {
TO_STR(SUB)
TO_STR(MUL)
TO_STR(DIV)
TO_STR(MOD)
TO_STR(EQ)
TO_STR(NE)
TO_STR(LT)
Expand Down Expand Up @@ -356,6 +360,17 @@ struct DivideOperator {
}
};

struct ModOperator {
template<typename T, typename U, typename V = typename binary_operation_promoted_type<T, U, ModOperator>::type>
V apply(T t, U u) {
if constexpr (std::is_floating_point_v<V>) {
return std::fmod(static_cast<V>(t), static_cast<V>(u));
Comment thread
academy-codex marked this conversation as resolved.
Outdated
} else {
return static_cast<V>(t) % static_cast<V>(u);
Comment thread
academy-codex marked this conversation as resolved.
Outdated
}
}
};

struct EqualsOperator {
template<typename T, typename U>
bool operator()(T t, U u) const {
Expand Down Expand Up @@ -715,6 +730,19 @@ struct formatter<arcticdb::DivideOperator> {
}
};

template<>
struct formatter<arcticdb::ModOperator> {
template<typename ParseContext>
constexpr auto parse(ParseContext& ctx) {
return ctx.begin();
}

template<typename FormatContext>
constexpr auto format(arcticdb::ModOperator, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "%");
}
};

template<>
struct formatter<arcticdb::EqualsOperator> {
template<typename ParseContext>
Expand Down
16 changes: 15 additions & 1 deletion cpp/arcticdb/processing/test/test_operation_dispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ TEST(OperationDispatch, binary_operator) {
EXPECT_THROW(visit_binary_operator(value, empty_column, PlusOperator{}), SchemaException);
}

TEST(OperationDispatch, binary_operator_modulo) {
Comment thread
academy-codex marked this conversation as resolved.
Outdated
using namespace arcticdb;
size_t num_rows = 100;
auto int_column = ColumnWithStrings(std::make_unique<Column>(generate_int_column(num_rows)), "int_col");
auto value = std::make_shared<Value>(static_cast<int64_t>(7), DataType::INT64);

auto variant_data = visit_binary_operator(int_column, value, ModOperator{});
ASSERT_TRUE(std::holds_alternative<ColumnWithStrings>(variant_data));
auto results_column = std::get<ColumnWithStrings>(variant_data).column_;
for (size_t idx = 0; idx < num_rows; idx++) {
ASSERT_EQ(static_cast<int64_t>(idx) % 7, results_column->scalar_at<int64_t>(idx));
}
}

TEST(OperationDispatch, binary_comparator) {
using namespace arcticdb;
size_t num_rows = 100;
Expand Down Expand Up @@ -145,4 +159,4 @@ TEST(OperationDispatch, binary_membership) {
// empty col isnotin set
ASSERT_TRUE(std::holds_alternative<FullResult>(visit_binary_membership(empty_column, value_set, IsNotInOperator{}))
);
}
}
1 change: 1 addition & 0 deletions cpp/arcticdb/version/python_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ void register_bindings(py::module& version, py::exception<arcticdb::ArcticExcept
.value("SUB", OperationType::SUB)
.value("MUL", OperationType::MUL)
.value("DIV", OperationType::DIV)
.value("MOD", OperationType::MOD)
.value("EQ", OperationType::EQ)
.value("NE", OperationType::NE)
.value("LT", OperationType::LT)
Expand Down
8 changes: 7 additions & 1 deletion python/arcticdb/version_store/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ def __mul__(self, right):
def __truediv__(self, right):
return self._apply(right, _OperationType.DIV)

def __mod__(self, right):
return self._apply(right, _OperationType.MOD)

def __eq__(self, right):
if is_supported_sequence(right):
return self.isin(right)
Expand Down Expand Up @@ -186,6 +189,9 @@ def __rmul__(self, left):
def __rtruediv__(self, left):
return self._rapply(left, _OperationType.DIV)

def __rmod__(self, left):
return self._rapply(left, _OperationType.MOD)

def __rand__(self, left):
if left is True:
return self
Expand Down Expand Up @@ -435,7 +441,7 @@ class QueryBuilder:

Supported arithmetic operations when projection or filtering:

* Binary arithmetic: +, -, *, /
* Binary arithmetic: +, -, *, /, %
* Unary arithmetic: -, abs

Supported filtering operations:
Expand Down
35 changes: 35 additions & 0 deletions python/tests/unit/arcticdb/version_store/test_query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,41 @@ def can_read_back(write_with_time, filter_with_time):
assert not can_read_back(us_time, notz_summer_time)


def test_querybuilder_projection_modulo(lmdb_version_store_tiny_segment, any_output_format):
Comment thread
academy-codex marked this conversation as resolved.
Outdated
Comment thread
academy-codex marked this conversation as resolved.
Outdated
lib = lmdb_version_store_tiny_segment
lib._set_output_format_for_pipeline_tests(any_output_format)
symbol = "test_querybuilder_projection_modulo"
df = pd.DataFrame({"col": np.arange(10, dtype=np.int64)}, index=np.arange(10))
lib.write(symbol, df)

q = QueryBuilder()
q = q.apply("mod_col", q["col"] % 3)

expected = df.copy()
expected["mod_col"] = expected["col"] % 3
received = lib.read(symbol, query_builder=q).data

assert_frame_equal(expected, received)
Comment thread
academy-codex marked this conversation as resolved.
Outdated


def test_querybuilder_filter_datetime_index_by_minute_with_modulo(lmdb_version_store_tiny_segment, any_output_format):
Comment thread
academy-codex marked this conversation as resolved.
Outdated
lib = lmdb_version_store_tiny_segment
lib._set_output_format_for_pipeline_tests(any_output_format)
symbol = "test_querybuilder_filter_datetime_index_by_minute_with_modulo"
index = pd.date_range("2024-01-01", periods=180, freq="min")
df = pd.DataFrame({"col": np.arange(index.shape[0], dtype=np.int64)}, index=index)
lib.write(symbol, df)

q = QueryBuilder()
minute_in_hour = q["index"] % pd.Timedelta(hours=1)
q = q[(minute_in_hour >= pd.Timedelta(minutes=10)) & (minute_in_hour < pd.Timedelta(minutes=11))]

expected = df[df.index.minute == 10]
received = lib.read(symbol, query_builder=q).data

assert_frame_equal(expected, received)


@pytest.mark.parametrize("batch", [True, False])
@pytest.mark.parametrize("use_date_range_clause", [True, False])
def test_querybuilder_date_range_then_date_range(
Expand Down