From bca05b947ccd49f6136a226db257f600723c4371 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:41:54 -0700 Subject: [PATCH 01/18] Compile with -Werror for linux and macos. --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c5946a0024..62a5faeb25 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ if not WINDOWS: # Windows does not have this flag extra_compile_args.append("-Wno-strict-prototypes") + extra_compile_args.append("-Werror") if machine == 'x86_64': extra_compile_args.append('-march=nocona') @@ -162,9 +163,6 @@ # --------------------------------------------------------------------------- extra_compile_args = extra_compile_args + [ '-rdynamic', '-finline-functions', - # TODO: On macOS, this flag causes compiler errors - # CLIENT-4869 - '-Werror' ] libraries = libraries + ['rt'] AEROSPIKE_C_TARGET = AEROSPIKE_C_HOME + '/target/Linux-' + machine From c80b4122df299795ca10560b7c01548dbeeed165 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:53:20 -0700 Subject: [PATCH 02/18] Update documentation to reflect latest changes in PRD. TODO - need additional / changes to tests. Also string ops tests should be refactored to make easier to change across the board. --- aerospike_helpers/string_helpers.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/aerospike_helpers/string_helpers.py b/aerospike_helpers/string_helpers.py index d0fe73b512..9ffc27b144 100644 --- a/aerospike_helpers/string_helpers.py +++ b/aerospike_helpers/string_helpers.py @@ -48,13 +48,29 @@ class WriteFlags(IntEnum): String operation policy write bit flags. Use bitwise OR to combine flags. """ - #: Default. Allow create or update. DEFAULT = 0 + """ + TODO - what about string expressions + + Default. These additive string operations will create a new bin: + + - :py:meth:`~aerospike_helpers.operations.string_operations.insert` + - :py:meth:`~aerospike_helpers.operations.string_operations.overwrite` + - :py:meth:`~aerospike_helpers.operations.string_operations.concat` + - :py:meth:`~aerospike_helpers.operations.string_operations.append` + - :py:meth:`~aerospike_helpers.operations.string_operations.prepend` + - :py:meth:`~aerospike_helpers.operations.string_operations.pad_start` + - :py:meth:`~aerospike_helpers.operations.string_operations.pad_end` + - :py:meth:`~aerospike_helpers.operations.string_operations.repeat` + + All other string operations will be no-ops. + """ NO_FAIL = 4 """ - Do not raise an error if a modify operation cannot be applied because - the target bin does not exist. The record is left unchanged. + Suppress an operation failure with the bin unchanged. + + Does not suppress wrong-type errors. """ From 4b0e04a5098f176aac507e7ce203ac64a5687544 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:10:14 -0700 Subject: [PATCH 03/18] Refactor string op tests by combining all test cases that operate on the same string bin. This reduces a good amount of tech debt. --- test/new_tests/test_string_operations.py | 405 +++-------------------- 1 file changed, 46 insertions(+), 359 deletions(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 71b3af190c..116fb98303 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -43,77 +43,37 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa @root_level_and_nested_str @expect_server_version_earlier_than_8_1_3_to_fail - def test_strlen(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.strlen(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - assert bins[bin_name] == len(EXAMPLE_STR) - - @pytest.mark.parametrize( - "op", - [ - str_ops.substr, - str_ops.substr_range - ] - ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_substr(self, bin_name: str, kwargs_with_ctx: dict, op: dict): - kwargs = kwargs_with_ctx.copy() - if op == str_ops.substr_range: - kwargs = kwargs_with_ctx | {"end": START_IDX + 2} - - ops = [ - op(bin_name=bin_name, start=START_IDX, **kwargs) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - if "end" not in kwargs: - assert bins[bin_name] == EXAMPLE_STR[START_IDX:] - else: - end = kwargs["end"] - assert bins[bin_name] == EXAMPLE_STR[START_IDX:end] - @pytest.mark.parametrize( - "index", + "op_method, kwargs, expected_result", [ - START_IDX, - -1 + (str_ops.strlen, {}, len(EXAMPLE_STR)), + (str_ops.substr, {"start": START_IDX}, EXAMPLE_STR[START_IDX:]), + (str_ops.substr_range, {"start": START_IDX, "end": START_IDX + 2}, EXAMPLE_STR[START_IDX:START_IDX + 2]), + (str_ops.char_at, {"index": START_IDX}, EXAMPLE_STR[START_IDX]), + (str_ops.char_at, {"index": -1}, EXAMPLE_STR[-1]), + (str_ops.find, {"needle": NEEDLE}, 0), + (str_ops.find, {"needle": NEEDLE, "occurrence": 1}, 0), + (str_ops.find, {"needle": NEEDLE, "occurrence": 2}, 4), + (str_ops.contains, {"needle": NEEDLE}, True), + (str_ops.starts_with, {"prefix": NEEDLE}, True), + (str_ops.starts_with, {"prefix": NOT_IN_EXAMPLE_STR}, False), + (str_ops.ends_with, {"suffix": NEEDLE}, True), + (str_ops.ends_with, {"suffix": NOT_IN_EXAMPLE_STR}, False), + (str_ops.byte_length, {}, len(EXAMPLE_STR)), + (str_ops.is_numeric, {}, False), + (str_ops.is_upper, {}, False), + (str_ops.is_lower, {}, True), + (str_ops.to_blob, {}, bytes(EXAMPLE_STR, encoding="utf-8")), + (str_ops.split, {}, list(EXAMPLE_STR)), ] ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_char_at(self, index: int, bin_name: str, kwargs_with_ctx: dict): + def test_string_read_op_on_str_value(self, op_method, bin_name: str, kwargs_with_ctx: dict, kwargs: dict, expected_result): ops = [ - str_ops.char_at(bin_name=bin_name, index=index, **kwargs_with_ctx) + op_method(bin_name=bin_name, **kwargs_with_ctx, **kwargs) ] with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR[index] - - @pytest.mark.parametrize( - "occurrence_kwargs, expected_idx", - [ - ({}, 0), - ({"occurrence": 1}, 0), - ({"occurrence": 2}, 4) - ] - ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_find(self, occurrence_kwargs: dict, expected_idx: int, bin_name: str, kwargs_with_ctx: dict): - kwargs_with_ctx = kwargs_with_ctx | occurrence_kwargs - ops = [ - str_ops.find(bin_name=bin_name, needle=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_idx + assert bins[bin_name] == expected_result @expect_server_version_earlier_than_8_1_3_to_fail def test_find_not_found(self): @@ -125,17 +85,6 @@ def test_find_not_found(self): assert bins[STR_BIN_NAME] == -1 - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_contains(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.contains(bin_name=bin_name, needle=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - @expect_server_version_earlier_than_8_1_3_to_fail def test_contains_not_found(self): ops = [ @@ -146,17 +95,6 @@ def test_contains_not_found(self): assert bins[STR_BIN_NAME] is False - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_starts_with(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.starts_with(bin_name=bin_name, prefix=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - @expect_server_version_earlier_than_8_1_3_to_fail def test_starts_with_returns_false(self): ops = [ @@ -167,27 +105,6 @@ def test_starts_with_returns_false(self): assert bins[STR_BIN_NAME] is False - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_ends_with(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.ends_with(bin_name=bin_name, suffix=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - - @expect_server_version_earlier_than_8_1_3_to_fail - def test_ends_with_returns_false(self): - ops = [ - str_ops.ends_with(bin_name=STR_BIN_NAME, suffix=NOT_IN_EXAMPLE_STR) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[STR_BIN_NAME] is False - @expect_server_version_earlier_than_8_1_3_to_fail def test_to_integer(self): ops = [ @@ -224,17 +141,6 @@ def test_to_double(self): assert bins[STR_WITH_DOUBLE_BIN_NAME] == float(STRING_WITH_DOUBLE) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_byte_length(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.byte_length(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == len(EXAMPLE_STR) - @expect_server_version_earlier_than_8_1_3_to_fail def test_byte_length_for_multibyte_codepoint(self): ops = [ @@ -248,7 +154,6 @@ def test_byte_length_for_multibyte_codepoint(self): @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, False), (STR_WITH_INT_BIN_NAME, True), (STR_WITH_DOUBLE_BIN_NAME, True), ] @@ -287,7 +192,6 @@ def test_numeric_type(self, numeric_type: NumericType, bin_name: str, expected_r @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, False), (UPPERCASE_STR_BIN_NAME, True) ] ) @@ -304,7 +208,6 @@ def test_is_upper(self, bin_name: str, expected_result: bool): @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, True), (UPPERCASE_STR_BIN_NAME, False) ] ) @@ -318,28 +221,6 @@ def test_is_lower(self, bin_name: str, expected_result: bool): assert bins[bin_name] is expected_result - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_to_blob(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.to_blob(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == bytes(EXAMPLE_STR, encoding="utf-8") - - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_split(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.split(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == list(EXAMPLE_STR) - @pytest.mark.parametrize( "separator", [ @@ -400,38 +281,36 @@ def add_read_op(self, ops, bin_name): ops.append(op) @pytest.mark.parametrize( - "index, expected_value", - [ - (1, EXAMPLE_STR[:1] + NEEDLE + EXAMPLE_STR[1:]), - (-1, EXAMPLE_STR[:-1] + NEEDLE + EXAMPLE_STR[-1:]) - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_insert(self, index: int, expected_value: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.insert(bin_name=bin_name, index=index, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_value - - @pytest.mark.parametrize( - "index, expected_value", + "op, kwargs, expected_value", [ - (1, EXAMPLE_STR[:1] + SINGLE_CHAR + EXAMPLE_STR[2:]), + (str_ops.insert, {"index": 1, "value": NEEDLE}, EXAMPLE_STR[:1] + NEEDLE + EXAMPLE_STR[1:]), + (str_ops.insert, {"index": -1, "value": NEEDLE}, EXAMPLE_STR[:-1] + NEEDLE + EXAMPLE_STR[-1:]), + (str_ops.overwrite, {"index": 1, "value": SINGLE_CHAR}, EXAMPLE_STR[:1] + SINGLE_CHAR + EXAMPLE_STR[2:]), + (str_ops.overwrite, {"index": 0, "value": EXAMPLE_STR + "a"}, EXAMPLE_STR + "a"), + (str_ops.append, {"value": NEEDLE}, EXAMPLE_STR + NEEDLE), + (str_ops.prepend, {"value": NEEDLE}, NEEDLE + EXAMPLE_STR), + (str_ops.concat, {"value_list": [NEEDLE]}, EXAMPLE_STR + NEEDLE), + (str_ops.concat, {"value_list": [NEEDLE, NEEDLE]}, EXAMPLE_STR + NEEDLE * 2), + (str_ops.snip, {"start": START_IDX, "end": len(EXAMPLE_STR) - 1}, EXAMPLE_STR[:START_IDX] + EXAMPLE_STR[-1]), + (str_ops.replace, {"needle": NEEDLE, "replacement": SINGLE_CHAR}, EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR, 1)), + (str_ops.replace_all, {"needle": NEEDLE, "replacement": SINGLE_CHAR}, EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR)), + (str_ops.upper, {}, EXAMPLE_STR.upper()), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) + 2}, 2 * PAD_STRING + EXAMPLE_STR), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR)}, EXAMPLE_STR), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) - 1}, EXAMPLE_STR), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) + 2}, EXAMPLE_STR + 2 * PAD_STRING), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR)}, EXAMPLE_STR), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) - 1}, EXAMPLE_STR), + (str_ops.repeat, {"count": 1}, EXAMPLE_STR), + (str_ops.repeat, {"count": 2}, EXAMPLE_STR * 2) ] ) @root_level_and_nested_str @kwargs_policy @expect_server_version_earlier_than_8_1_3_to_fail - def test_overwrite_single_char(self, index: int, expected_value: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): + def test_string_write_op_on_str_value(self, op, expected_value: str, kwargs: dict, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): ops = [ - str_ops.overwrite(bin_name=bin_name, index=index, value=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) + op(bin_name=bin_name, **kwargs, **kwargs_policy, **kwargs_with_ctx) ] self.add_read_op(ops, bin_name) @@ -440,69 +319,6 @@ def test_overwrite_single_char(self, index: int, expected_value: str, kwargs_pol assert bins[bin_name] == expected_value - @expect_server_version_earlier_than_8_1_3_to_fail - def test_overwrite_past_string_length(self): - NEW_STR = EXAMPLE_STR + "a" - ops = [ - str_ops.overwrite(bin_name=STR_BIN_NAME, index=0, value=NEW_STR) - ] - self.add_read_op(ops, STR_BIN_NAME) - - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[STR_BIN_NAME] == NEW_STR - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_append(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.append(bin_name=bin_name, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR + NEEDLE - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_prepend(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.prepend(bin_name=bin_name, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == NEEDLE + EXAMPLE_STR - - @pytest.mark.parametrize( - "value_list", - [ - [NEEDLE], - [NEEDLE, NEEDLE] - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_concat(self, value_list: list[str], kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.concat(bin_name=bin_name, value_list=value_list, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR + "".join(value_list) - def test_concat_with_non_str_in_list(self): ops = [ str_ops.concat(bin_name=STR_BIN_NAME, value_list=[1]) @@ -511,70 +327,6 @@ def test_concat_with_non_str_in_list(self): with pytest.raises(e.ServerError): self.as_connection.operate(KEY, ops) - @pytest.mark.parametrize( - "end_kwargs", - [ - {"end": len(EXAMPLE_STR) - 1} - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_snip(self, end_kwargs, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - - START_IDX = 1 - ops = [ - str_ops.snip(bin_name=bin_name, start=START_IDX, **end_kwargs, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR[:START_IDX] + EXAMPLE_STR[-1] - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_replace(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.replace(bin_name=bin_name, needle=NEEDLE, replacement=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR, 1) - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_replace_all(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.replace_all(bin_name=bin_name, needle=NEEDLE, replacement=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR) - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_upper(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.upper(bin_name=bin_name, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.upper() - @pytest.mark.parametrize( "bin_name, expected_result", [ @@ -661,71 +413,6 @@ def test_trim(self, kwargs_policy): assert bins[SURROUNDING_WHITESPACE_BIN_NAME] == EXAMPLE_STR_WITH_SURROUNDING_WHITESPACE[1:-1] - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "target_length, expected_results", - [ - (len(EXAMPLE_STR) + 2, 2 * PAD_STRING + EXAMPLE_STR), - (len(EXAMPLE_STR), EXAMPLE_STR), - (len(EXAMPLE_STR) - 1, EXAMPLE_STR) - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_pad_start(self, target_length: int, expected_results: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.pad_start(bin_name=bin_name, pad_string=PAD_STRING, target_length=target_length, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_results - - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "target_length, expected_results", - [ - (len(EXAMPLE_STR) + 2, EXAMPLE_STR + 2 * PAD_STRING), - (len(EXAMPLE_STR), EXAMPLE_STR), - (len(EXAMPLE_STR) - 1, EXAMPLE_STR) - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_pad_end(self, target_length: int, expected_results: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.pad_end(bin_name=bin_name, pad_string=PAD_STRING, target_length=target_length, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_results - - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "count", - [ - 1, - 2, - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_repeat(self, count: int, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.repeat(bin_name=bin_name, count=count, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR * count - @kwargs_policy @root_level_and_nested_str @expect_server_version_earlier_than_8_1_3_to_fail From 352cef8b1d86f48e3659182ce40bdd70590b7a98 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:29:52 -0700 Subject: [PATCH 04/18] Add test_string_ops_on_nonexistent_bin_creates_it for additive string ops. TODO - Some of these are failing locally, so need to triage --- test/new_tests/string_helpers.py | 1 + test/new_tests/test_string_operations.py | 28 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index 3ef5147370..5a12f89cf5 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -17,6 +17,7 @@ SURROUNDING_WHITESPACE_BIN_NAME = "whitespace" MULTILINE_STR_BIN_NAME = "multiline" MULTILINE_STR_WITH_CR_BIN_NAME = "multiline_cr" +NON_EXISTENT_BIN_NAME = "nonexistent" PAD_STRING = " " SINGLE_CHAR = "z" diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 116fb98303..54571d62f5 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -462,3 +462,31 @@ def test_string_policy_no_fail(self): _, _, bins = self.as_connection.operate(KEY, ops) assert bins[NON_STR_BIN_NAME] == BINS[NON_STR_BIN_NAME] + + @pytest.mark.parametrize( + "op, kwargs", + [ + (str_ops.append, {"value": NEEDLE}), + (str_ops.prepend, {"value": NEEDLE}), + (str_ops.concat, {"value_list": [NEEDLE]}), + (str_ops.overwrite, {"index": 0, "value": NEEDLE}), + (str_ops.insert, {"index": 0, "value": NEEDLE}), + (str_ops.pad_start, {"target_length": 1, "pad_string": NEEDLE}), + (str_ops.pad_end, {"target_length": 1, "pad_string": NEEDLE}), + (str_ops.repeat, {"count": 2}), + ] + ) + @expect_server_version_earlier_than_8_1_3_to_fail + def test_string_ops_on_nonexistent_bin_creates_it(self, op, kwargs: dict): + ops = [ + op(bin_name=NON_EXISTENT_BIN_NAME, **kwargs), + operations.read(NON_EXISTENT_BIN_NAME) + ] + + with self.expected_context_for_pos_tests: + _, _, bins = self.as_connection.operate(KEY, ops) + + if op == str_ops.repeat: + assert bins[NON_EXISTENT_BIN_NAME] == "" + else: + assert bins[NON_EXISTENT_BIN_NAME] == NEEDLE From 1f91d0d6c53063b4a024870a084fb7a750dab42c Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:10:14 -0700 Subject: [PATCH 05/18] Refactor string op tests by combining all test cases that operate on the same string bin. This reduces a good amount of tech debt. --- test/new_tests/test_string_operations.py | 405 +++-------------------- 1 file changed, 46 insertions(+), 359 deletions(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 71b3af190c..116fb98303 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -43,77 +43,37 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa @root_level_and_nested_str @expect_server_version_earlier_than_8_1_3_to_fail - def test_strlen(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.strlen(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - assert bins[bin_name] == len(EXAMPLE_STR) - - @pytest.mark.parametrize( - "op", - [ - str_ops.substr, - str_ops.substr_range - ] - ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_substr(self, bin_name: str, kwargs_with_ctx: dict, op: dict): - kwargs = kwargs_with_ctx.copy() - if op == str_ops.substr_range: - kwargs = kwargs_with_ctx | {"end": START_IDX + 2} - - ops = [ - op(bin_name=bin_name, start=START_IDX, **kwargs) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - if "end" not in kwargs: - assert bins[bin_name] == EXAMPLE_STR[START_IDX:] - else: - end = kwargs["end"] - assert bins[bin_name] == EXAMPLE_STR[START_IDX:end] - @pytest.mark.parametrize( - "index", + "op_method, kwargs, expected_result", [ - START_IDX, - -1 + (str_ops.strlen, {}, len(EXAMPLE_STR)), + (str_ops.substr, {"start": START_IDX}, EXAMPLE_STR[START_IDX:]), + (str_ops.substr_range, {"start": START_IDX, "end": START_IDX + 2}, EXAMPLE_STR[START_IDX:START_IDX + 2]), + (str_ops.char_at, {"index": START_IDX}, EXAMPLE_STR[START_IDX]), + (str_ops.char_at, {"index": -1}, EXAMPLE_STR[-1]), + (str_ops.find, {"needle": NEEDLE}, 0), + (str_ops.find, {"needle": NEEDLE, "occurrence": 1}, 0), + (str_ops.find, {"needle": NEEDLE, "occurrence": 2}, 4), + (str_ops.contains, {"needle": NEEDLE}, True), + (str_ops.starts_with, {"prefix": NEEDLE}, True), + (str_ops.starts_with, {"prefix": NOT_IN_EXAMPLE_STR}, False), + (str_ops.ends_with, {"suffix": NEEDLE}, True), + (str_ops.ends_with, {"suffix": NOT_IN_EXAMPLE_STR}, False), + (str_ops.byte_length, {}, len(EXAMPLE_STR)), + (str_ops.is_numeric, {}, False), + (str_ops.is_upper, {}, False), + (str_ops.is_lower, {}, True), + (str_ops.to_blob, {}, bytes(EXAMPLE_STR, encoding="utf-8")), + (str_ops.split, {}, list(EXAMPLE_STR)), ] ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_char_at(self, index: int, bin_name: str, kwargs_with_ctx: dict): + def test_string_read_op_on_str_value(self, op_method, bin_name: str, kwargs_with_ctx: dict, kwargs: dict, expected_result): ops = [ - str_ops.char_at(bin_name=bin_name, index=index, **kwargs_with_ctx) + op_method(bin_name=bin_name, **kwargs_with_ctx, **kwargs) ] with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR[index] - - @pytest.mark.parametrize( - "occurrence_kwargs, expected_idx", - [ - ({}, 0), - ({"occurrence": 1}, 0), - ({"occurrence": 2}, 4) - ] - ) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_find(self, occurrence_kwargs: dict, expected_idx: int, bin_name: str, kwargs_with_ctx: dict): - kwargs_with_ctx = kwargs_with_ctx | occurrence_kwargs - ops = [ - str_ops.find(bin_name=bin_name, needle=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_idx + assert bins[bin_name] == expected_result @expect_server_version_earlier_than_8_1_3_to_fail def test_find_not_found(self): @@ -125,17 +85,6 @@ def test_find_not_found(self): assert bins[STR_BIN_NAME] == -1 - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_contains(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.contains(bin_name=bin_name, needle=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - @expect_server_version_earlier_than_8_1_3_to_fail def test_contains_not_found(self): ops = [ @@ -146,17 +95,6 @@ def test_contains_not_found(self): assert bins[STR_BIN_NAME] is False - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_starts_with(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.starts_with(bin_name=bin_name, prefix=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - @expect_server_version_earlier_than_8_1_3_to_fail def test_starts_with_returns_false(self): ops = [ @@ -167,27 +105,6 @@ def test_starts_with_returns_false(self): assert bins[STR_BIN_NAME] is False - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_ends_with(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.ends_with(bin_name=bin_name, suffix=NEEDLE, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] is True - - @expect_server_version_earlier_than_8_1_3_to_fail - def test_ends_with_returns_false(self): - ops = [ - str_ops.ends_with(bin_name=STR_BIN_NAME, suffix=NOT_IN_EXAMPLE_STR) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[STR_BIN_NAME] is False - @expect_server_version_earlier_than_8_1_3_to_fail def test_to_integer(self): ops = [ @@ -224,17 +141,6 @@ def test_to_double(self): assert bins[STR_WITH_DOUBLE_BIN_NAME] == float(STRING_WITH_DOUBLE) - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_byte_length(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.byte_length(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == len(EXAMPLE_STR) - @expect_server_version_earlier_than_8_1_3_to_fail def test_byte_length_for_multibyte_codepoint(self): ops = [ @@ -248,7 +154,6 @@ def test_byte_length_for_multibyte_codepoint(self): @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, False), (STR_WITH_INT_BIN_NAME, True), (STR_WITH_DOUBLE_BIN_NAME, True), ] @@ -287,7 +192,6 @@ def test_numeric_type(self, numeric_type: NumericType, bin_name: str, expected_r @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, False), (UPPERCASE_STR_BIN_NAME, True) ] ) @@ -304,7 +208,6 @@ def test_is_upper(self, bin_name: str, expected_result: bool): @pytest.mark.parametrize( "bin_name, expected_result", [ - (STR_BIN_NAME, True), (UPPERCASE_STR_BIN_NAME, False) ] ) @@ -318,28 +221,6 @@ def test_is_lower(self, bin_name: str, expected_result: bool): assert bins[bin_name] is expected_result - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_to_blob(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.to_blob(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == bytes(EXAMPLE_STR, encoding="utf-8") - - @root_level_and_nested_str - @expect_server_version_earlier_than_8_1_3_to_fail - def test_split(self, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.split(bin_name=bin_name, **kwargs_with_ctx) - ] - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == list(EXAMPLE_STR) - @pytest.mark.parametrize( "separator", [ @@ -400,38 +281,36 @@ def add_read_op(self, ops, bin_name): ops.append(op) @pytest.mark.parametrize( - "index, expected_value", - [ - (1, EXAMPLE_STR[:1] + NEEDLE + EXAMPLE_STR[1:]), - (-1, EXAMPLE_STR[:-1] + NEEDLE + EXAMPLE_STR[-1:]) - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_insert(self, index: int, expected_value: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.insert(bin_name=bin_name, index=index, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_value - - @pytest.mark.parametrize( - "index, expected_value", + "op, kwargs, expected_value", [ - (1, EXAMPLE_STR[:1] + SINGLE_CHAR + EXAMPLE_STR[2:]), + (str_ops.insert, {"index": 1, "value": NEEDLE}, EXAMPLE_STR[:1] + NEEDLE + EXAMPLE_STR[1:]), + (str_ops.insert, {"index": -1, "value": NEEDLE}, EXAMPLE_STR[:-1] + NEEDLE + EXAMPLE_STR[-1:]), + (str_ops.overwrite, {"index": 1, "value": SINGLE_CHAR}, EXAMPLE_STR[:1] + SINGLE_CHAR + EXAMPLE_STR[2:]), + (str_ops.overwrite, {"index": 0, "value": EXAMPLE_STR + "a"}, EXAMPLE_STR + "a"), + (str_ops.append, {"value": NEEDLE}, EXAMPLE_STR + NEEDLE), + (str_ops.prepend, {"value": NEEDLE}, NEEDLE + EXAMPLE_STR), + (str_ops.concat, {"value_list": [NEEDLE]}, EXAMPLE_STR + NEEDLE), + (str_ops.concat, {"value_list": [NEEDLE, NEEDLE]}, EXAMPLE_STR + NEEDLE * 2), + (str_ops.snip, {"start": START_IDX, "end": len(EXAMPLE_STR) - 1}, EXAMPLE_STR[:START_IDX] + EXAMPLE_STR[-1]), + (str_ops.replace, {"needle": NEEDLE, "replacement": SINGLE_CHAR}, EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR, 1)), + (str_ops.replace_all, {"needle": NEEDLE, "replacement": SINGLE_CHAR}, EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR)), + (str_ops.upper, {}, EXAMPLE_STR.upper()), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) + 2}, 2 * PAD_STRING + EXAMPLE_STR), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR)}, EXAMPLE_STR), + (str_ops.pad_start, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) - 1}, EXAMPLE_STR), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) + 2}, EXAMPLE_STR + 2 * PAD_STRING), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR)}, EXAMPLE_STR), + (str_ops.pad_end, {"pad_string": PAD_STRING, "target_length": len(EXAMPLE_STR) - 1}, EXAMPLE_STR), + (str_ops.repeat, {"count": 1}, EXAMPLE_STR), + (str_ops.repeat, {"count": 2}, EXAMPLE_STR * 2) ] ) @root_level_and_nested_str @kwargs_policy @expect_server_version_earlier_than_8_1_3_to_fail - def test_overwrite_single_char(self, index: int, expected_value: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): + def test_string_write_op_on_str_value(self, op, expected_value: str, kwargs: dict, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): ops = [ - str_ops.overwrite(bin_name=bin_name, index=index, value=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) + op(bin_name=bin_name, **kwargs, **kwargs_policy, **kwargs_with_ctx) ] self.add_read_op(ops, bin_name) @@ -440,69 +319,6 @@ def test_overwrite_single_char(self, index: int, expected_value: str, kwargs_pol assert bins[bin_name] == expected_value - @expect_server_version_earlier_than_8_1_3_to_fail - def test_overwrite_past_string_length(self): - NEW_STR = EXAMPLE_STR + "a" - ops = [ - str_ops.overwrite(bin_name=STR_BIN_NAME, index=0, value=NEW_STR) - ] - self.add_read_op(ops, STR_BIN_NAME) - - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[STR_BIN_NAME] == NEW_STR - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_append(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.append(bin_name=bin_name, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR + NEEDLE - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_prepend(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.prepend(bin_name=bin_name, value=NEEDLE, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == NEEDLE + EXAMPLE_STR - - @pytest.mark.parametrize( - "value_list", - [ - [NEEDLE], - [NEEDLE, NEEDLE] - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_concat(self, value_list: list[str], kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.concat(bin_name=bin_name, value_list=value_list, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR + "".join(value_list) - def test_concat_with_non_str_in_list(self): ops = [ str_ops.concat(bin_name=STR_BIN_NAME, value_list=[1]) @@ -511,70 +327,6 @@ def test_concat_with_non_str_in_list(self): with pytest.raises(e.ServerError): self.as_connection.operate(KEY, ops) - @pytest.mark.parametrize( - "end_kwargs", - [ - {"end": len(EXAMPLE_STR) - 1} - ] - ) - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_snip(self, end_kwargs, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - - START_IDX = 1 - ops = [ - str_ops.snip(bin_name=bin_name, start=START_IDX, **end_kwargs, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR[:START_IDX] + EXAMPLE_STR[-1] - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_replace(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.replace(bin_name=bin_name, needle=NEEDLE, replacement=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR, 1) - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_replace_all(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.replace_all(bin_name=bin_name, needle=NEEDLE, replacement=SINGLE_CHAR, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.replace(NEEDLE, SINGLE_CHAR) - - @root_level_and_nested_str - @kwargs_policy - @expect_server_version_earlier_than_8_1_3_to_fail - def test_upper(self, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.upper(bin_name=bin_name, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR.upper() - @pytest.mark.parametrize( "bin_name, expected_result", [ @@ -661,71 +413,6 @@ def test_trim(self, kwargs_policy): assert bins[SURROUNDING_WHITESPACE_BIN_NAME] == EXAMPLE_STR_WITH_SURROUNDING_WHITESPACE[1:-1] - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "target_length, expected_results", - [ - (len(EXAMPLE_STR) + 2, 2 * PAD_STRING + EXAMPLE_STR), - (len(EXAMPLE_STR), EXAMPLE_STR), - (len(EXAMPLE_STR) - 1, EXAMPLE_STR) - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_pad_start(self, target_length: int, expected_results: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.pad_start(bin_name=bin_name, pad_string=PAD_STRING, target_length=target_length, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_results - - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "target_length, expected_results", - [ - (len(EXAMPLE_STR) + 2, EXAMPLE_STR + 2 * PAD_STRING), - (len(EXAMPLE_STR), EXAMPLE_STR), - (len(EXAMPLE_STR) - 1, EXAMPLE_STR) - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_pad_end(self, target_length: int, expected_results: str, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.pad_end(bin_name=bin_name, pad_string=PAD_STRING, target_length=target_length, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == expected_results - - @kwargs_policy - @root_level_and_nested_str - @pytest.mark.parametrize( - "count", - [ - 1, - 2, - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_repeat(self, count: int, kwargs_policy: dict, bin_name: str, kwargs_with_ctx: dict): - ops = [ - str_ops.repeat(bin_name=bin_name, count=count, **kwargs_policy, **kwargs_with_ctx) - ] - self.add_read_op(ops, bin_name) - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - assert bins[bin_name] == EXAMPLE_STR * count - @kwargs_policy @root_level_and_nested_str @expect_server_version_earlier_than_8_1_3_to_fail From 221e0a4ab8e9844173dd3a5cc040e1009ea1a028 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:31:54 -0700 Subject: [PATCH 06/18] Address incorrect tests for pad_start and pad_end on nonexistent bin. --- test/new_tests/test_string_operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 54571d62f5..da131e4573 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -471,8 +471,8 @@ def test_string_policy_no_fail(self): (str_ops.concat, {"value_list": [NEEDLE]}), (str_ops.overwrite, {"index": 0, "value": NEEDLE}), (str_ops.insert, {"index": 0, "value": NEEDLE}), - (str_ops.pad_start, {"target_length": 1, "pad_string": NEEDLE}), - (str_ops.pad_end, {"target_length": 1, "pad_string": NEEDLE}), + (str_ops.pad_start, {"target_length": 1, "pad_string": NEEDLE[0]}), + (str_ops.pad_end, {"target_length": 1, "pad_string": NEEDLE[0]}), (str_ops.repeat, {"count": 2}), ] ) From 061a174566c2d767b1bc0074da9a0209a51df760 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:39:34 -0700 Subject: [PATCH 07/18] Since all tests in this function expect NEEDLE as the output, don't change the pad string for pad_start and pad_end, but allow the target_length to be as big as NEEDLE --- test/new_tests/test_string_operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index da131e4573..156c4104f5 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -471,8 +471,8 @@ def test_string_policy_no_fail(self): (str_ops.concat, {"value_list": [NEEDLE]}), (str_ops.overwrite, {"index": 0, "value": NEEDLE}), (str_ops.insert, {"index": 0, "value": NEEDLE}), - (str_ops.pad_start, {"target_length": 1, "pad_string": NEEDLE[0]}), - (str_ops.pad_end, {"target_length": 1, "pad_string": NEEDLE[0]}), + (str_ops.pad_start, {"target_length": 4, "pad_string": NEEDLE}), + (str_ops.pad_end, {"target_length": 4, "pad_string": NEEDLE}), (str_ops.repeat, {"count": 2}), ] ) From 4416fb5add1d21ae93e0786d6f545b72914a722e Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:12:12 -0700 Subject: [PATCH 08/18] Add negative tests that check the other string write ops don't create a new bin. --- test/new_tests/test_string_operations.py | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 156c4104f5..e9b02ae000 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -464,20 +464,24 @@ def test_string_policy_no_fail(self): assert bins[NON_STR_BIN_NAME] == BINS[NON_STR_BIN_NAME] @pytest.mark.parametrize( - "op, kwargs", + "op, kwargs, creates_bin", [ - (str_ops.append, {"value": NEEDLE}), - (str_ops.prepend, {"value": NEEDLE}), - (str_ops.concat, {"value_list": [NEEDLE]}), - (str_ops.overwrite, {"index": 0, "value": NEEDLE}), - (str_ops.insert, {"index": 0, "value": NEEDLE}), - (str_ops.pad_start, {"target_length": 4, "pad_string": NEEDLE}), - (str_ops.pad_end, {"target_length": 4, "pad_string": NEEDLE}), - (str_ops.repeat, {"count": 2}), + (str_ops.append, {"value": NEEDLE}, True), + (str_ops.prepend, {"value": NEEDLE}, True), + (str_ops.concat, {"value_list": [NEEDLE]}, True), + (str_ops.overwrite, {"index": 0, "value": NEEDLE}, True), + (str_ops.insert, {"index": 0, "value": NEEDLE}, True), + (str_ops.pad_start, {"target_length": 4, "pad_string": NEEDLE}, True), + (str_ops.pad_end, {"target_length": 4, "pad_string": NEEDLE}, True), + (str_ops.repeat, {"count": 2}, True), + (str_ops.repeat, {"count": 2}, True), + (str_ops.snip, {"start": 0, "end": 1}, False), + (str_ops.replace, {"needle": "a", "replacement": "b"}, False), + (str_ops.replace_all, {"needle": "a", "replacement": "b"}, False), ] ) @expect_server_version_earlier_than_8_1_3_to_fail - def test_string_ops_on_nonexistent_bin_creates_it(self, op, kwargs: dict): + def test_string_ops_on_nonexistent_bin(self, op, kwargs: dict, creates_bin: bool): ops = [ op(bin_name=NON_EXISTENT_BIN_NAME, **kwargs), operations.read(NON_EXISTENT_BIN_NAME) @@ -486,6 +490,12 @@ def test_string_ops_on_nonexistent_bin_creates_it(self, op, kwargs: dict): with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) + if creates_bin is False: + assert NON_EXISTENT_BIN_NAME not in bins + return + + assert NON_EXISTENT_BIN_NAME in bins + if op == str_ops.repeat: assert bins[NON_EXISTENT_BIN_NAME] == "" else: From 96c059526f14a24771b0e838df04974d7fdd49b0 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:29:37 -0700 Subject: [PATCH 09/18] Address failing negative tests where operations.read() will return a non-existent bin name with value set to None instead of the bin name being absent in the bin dictionary. Add more e2e test cases that cover the rest of the string write operations that should be a no-op on a non-existent bin. --- test/new_tests/test_string_operations.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index e9b02ae000..7be5405c39 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -466,6 +466,7 @@ def test_string_policy_no_fail(self): @pytest.mark.parametrize( "op, kwargs, creates_bin", [ + # Positive (str_ops.append, {"value": NEEDLE}, True), (str_ops.prepend, {"value": NEEDLE}, True), (str_ops.concat, {"value_list": [NEEDLE]}, True), @@ -475,9 +476,18 @@ def test_string_policy_no_fail(self): (str_ops.pad_end, {"target_length": 4, "pad_string": NEEDLE}, True), (str_ops.repeat, {"count": 2}, True), (str_ops.repeat, {"count": 2}, True), + # Negative (str_ops.snip, {"start": 0, "end": 1}, False), (str_ops.replace, {"needle": "a", "replacement": "b"}, False), (str_ops.replace_all, {"needle": "a", "replacement": "b"}, False), + (str_ops.upper, {}, False), + (str_ops.lower, {}, False), + (str_ops.casefold, {}, False), + (str_ops.normalize_nfc, {}, False), + (str_ops.trim_start, {}, False), + (str_ops.trim_end, {}, False), + (str_ops.trim, {}, False), + (str_ops.regex_replace, {"pattern": "a", "replacement": "b"}, False), ] ) @expect_server_version_earlier_than_8_1_3_to_fail @@ -491,7 +501,7 @@ def test_string_ops_on_nonexistent_bin(self, op, kwargs: dict, creates_bin: bool _, _, bins = self.as_connection.operate(KEY, ops) if creates_bin is False: - assert NON_EXISTENT_BIN_NAME not in bins + assert bins[NON_EXISTENT_BIN_NAME] is None return assert NON_EXISTENT_BIN_NAME in bins From 0dfc023e22d63305f79e86ee513d5e6ef12f699e Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:00:08 -0700 Subject: [PATCH 10/18] Document how each string op behaves with regards to a nonexistent bin in each operation's docstring instead of in the string write flags section. Also align with API docs standards by having string write ops' docstrings start with one sentence and have details in a following paragraph. --- .../operations/string_operations.py | 48 +++++++++++++++++-- aerospike_helpers/string_helpers.py | 15 +----- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/aerospike_helpers/operations/string_operations.py b/aerospike_helpers/operations/string_operations.py index ce3fec80d2..4dbdb3fe7f 100644 --- a/aerospike_helpers/operations/string_operations.py +++ b/aerospike_helpers/operations/string_operations.py @@ -400,7 +400,10 @@ def regex_compare(bin_name: str, pattern: str, regex_flags: RegexFlags = RegexFl def insert(bin_name: str, index: int, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``insert`` operation that splices value into the bin at codepoint - index. Negative indexes count from the end of the string. + index. + + Negative indexes count from the end of the string. + If the bin doesn't exist, this operation will create a new bin. Args: @@ -423,8 +426,10 @@ def insert(bin_name: str, index: int, value: str, policy: StringPolicy | None = def overwrite(bin_name: str, index: int, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``overwrite`` operation that overwrites codepoints starting at index - with value. The result may grow beyond the original length when value extends - past the end. + with value. + + The result may grow beyond the original length when value extends past the end. + If the bin doesn't exist, this operation will create a new bin. Args: @@ -447,8 +452,10 @@ def overwrite(bin_name: str, index: int, value: str, policy: StringPolicy | None def append(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``append`` operation that appends value to the end of the bin. + Unlike :func:`~aerospike_helpers.operations.operations.append`, this string-package operation uses Unicode codepoint semantics and supports string policy and ctx. + If the bin doesn't exist, this operation will create a new bin. Args: @@ -469,8 +476,10 @@ def append(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: T def prepend(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``prepend`` operation that prepend value to the start of the bin. + Unlike :func:`~aerospike_helpers.operations.operations.prepend`, this string-package operation uses Unicode codepoint semantics and supports string policy and ctx. + If the bin doesn't exist, this operation will create a new bin. Args: @@ -493,6 +502,8 @@ def concat(bin_name: str, value_list: list[str], policy: StringPolicy | None = N Create string ``concat`` operation that appends each string element in values to the bin in order. + If the bin doesn't exist, this operation will create a new bin. + Args: bin_name: name of string bin. @@ -513,6 +524,8 @@ def snip(bin_name: str, start: int, end: int, policy: StringPolicy | None = None """ Create string ``snip`` operation that removes codepoints from start to end. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -536,6 +549,8 @@ def replace(bin_name: str, needle: str, replacement: str, policy: StringPolicy | Create string ``replace`` operation that replaces the first occurrence of needle with replacement. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -559,6 +574,8 @@ def replace_all(bin_name: str, needle: str, replacement: str, policy: StringPoli Create string ``replace_all`` operation that replaces every occurrence of needle with replacement. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -581,6 +598,8 @@ def upper(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None """ Create string ``upper`` operation that uppercases the bin in place. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -599,6 +618,8 @@ def lower(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None """ Create string ``lower`` operation that lowercases the bin in place. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -616,7 +637,10 @@ def lower(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None def casefold(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``case_fold`` operation that applies locale-independent case folding - (lowercase) to the bin. This is useful for normalized comparison keys. + (lowercase) to the bin. + + This is useful for normalized comparison keys. + If the bin doesn't exist, this operation will be a no-op. Args: @@ -635,7 +659,9 @@ def casefold(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = N def normalize_nfc(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``normalize_nfc`` operation that normalizes the bin to Unicode NFC. + Already-normalized strings are unchanged. + If the bin doesn't exist, this operation will be a no-op. Args: @@ -656,6 +682,8 @@ def trim_start(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = Create string ``trim_start`` operation that removes whitespace from the start of the bin. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -675,6 +703,8 @@ def trim_end(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = N Create string ``trim_end`` operation that removes whitespace from the end of the bin. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -693,6 +723,8 @@ def trim(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None) """ Create string ``trim`` operation that removes whitespace from both ends of the bin. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. @@ -719,6 +751,8 @@ def pad_start( the bin reaches ``target_length`` codepoints. No-op when the bin is already at or above the target length. + If the bin doesn't exist, this operation will create a new bin. + Args: bin_name: name of string bin. @@ -749,6 +783,8 @@ def pad_end( bin reaches ``target_length`` codepoints. No-op when the bin is already at or above the target length. + If the bin doesn't exist, this operation will create a new bin. + Args: bin_name: name of string bin. @@ -771,6 +807,8 @@ def repeat(bin_name: str, count: int, policy: StringPolicy | None = None, ctx: T """ Create string ``repeat`` operation that repeats the bin contents count times. + If the bin doesn't exist, this operation will create a new bin. + Args: bin_name: name of string bin. @@ -800,6 +838,8 @@ def regex_replace( with replacement. Pass :py:attr:`~aerospike_helpers.string_helpers.RegexFlags.GLOBAL` to replace every match. This server operation accepts regex flags but not string policy flags. + If the bin doesn't exist, this operation will be a no-op. + Args: bin_name: name of string bin. diff --git a/aerospike_helpers/string_helpers.py b/aerospike_helpers/string_helpers.py index 9ffc27b144..5cce0ce82c 100644 --- a/aerospike_helpers/string_helpers.py +++ b/aerospike_helpers/string_helpers.py @@ -50,20 +50,7 @@ class WriteFlags(IntEnum): DEFAULT = 0 """ - TODO - what about string expressions - - Default. These additive string operations will create a new bin: - - - :py:meth:`~aerospike_helpers.operations.string_operations.insert` - - :py:meth:`~aerospike_helpers.operations.string_operations.overwrite` - - :py:meth:`~aerospike_helpers.operations.string_operations.concat` - - :py:meth:`~aerospike_helpers.operations.string_operations.append` - - :py:meth:`~aerospike_helpers.operations.string_operations.prepend` - - :py:meth:`~aerospike_helpers.operations.string_operations.pad_start` - - :py:meth:`~aerospike_helpers.operations.string_operations.pad_end` - - :py:meth:`~aerospike_helpers.operations.string_operations.repeat` - - All other string operations will be no-ops. + Default. Create or replace. """ NO_FAIL = 4 From 028688a4f5ebc5e140770929978acc550cf47382 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:31:26 -0700 Subject: [PATCH 11/18] Address to_string expression and operation not being implemented... TODO - to_string expression currently failing so need to debug it. --- aerospike_helpers/expressions/string.py | 34 +++++++++--------- .../operations/string_operations.py | 36 ++++++++++--------- src/include/policy.h | 3 +- src/main/client/operate.c | 2 +- src/main/client/operate_helper.c | 4 ++- src/main/convert_expressions.c | 4 +++ test/new_tests/string_helpers.py | 4 ++- test/new_tests/test_expressions_string.py | 4 +++ test/new_tests/test_string_operations.py | 12 +++++++ 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/aerospike_helpers/expressions/string.py b/aerospike_helpers/expressions/string.py index 10cc005725..f952ae2b63 100644 --- a/aerospike_helpers/expressions/string.py +++ b/aerospike_helpers/expressions/string.py @@ -401,6 +401,23 @@ def __init__(self, pattern: str, bin: "TypeBinName", regex_flags: RegexFlags = R self._children = (_convert_bin_name_to_expr(bin),) +class ToString(_BaseExpr): + _op = aerospike._AS_EXP_CODE_CALL + + def __init__(self, bin: "TypeBinName"): + """ + Args: + + bin: A bin expression to apply this function to. + + Returns: + + The string in the bin with the value converted to a string. + + """ + self._children = (_convert_bin_name_to_expr(bin),) + + class _WriteOp(_BaseExpr): def __init__(self, policy: StringPolicy): self._fixed = { @@ -823,23 +840,6 @@ def __init__( self._children = (_convert_bin_name_to_expr(bin),) -class ToString(_WriteOp): - _op = aerospike._AS_EXP_CODE_CALL - - def __init__(self, bin: "TypeBinName"): - """ - Args: - - bin: A bin expression to apply this function to. - - Returns: - - The string in the bin with the value converted to a string. - - """ - self._children = (_convert_bin_name_to_expr(bin),) - - __exp_class_to_op_func = { StrLen: str_ops.strlen, SubStr: str_ops.substr, diff --git a/aerospike_helpers/operations/string_operations.py b/aerospike_helpers/operations/string_operations.py index 4dbdb3fe7f..447a721322 100644 --- a/aerospike_helpers/operations/string_operations.py +++ b/aerospike_helpers/operations/string_operations.py @@ -397,6 +397,25 @@ def regex_compare(bin_name: str, pattern: str, regex_flags: RegexFlags = RegexFl } +def to_string(bin_name: str): + """ + Create ``to_string`` operation that converts an integer, double, string, or blob + bin to its string representation. + + Raises :exc:`~aerospike.exception.BinIncompatibleType` for + any other bin type. This top-level operation does not accept ctx and does not + send a msgpack payload. + + Args: + + bin_name: name of string bin. + """ + return { + "op": aerospike._OP_STRING_TO_STRING, + "bin": bin_name, + } + + def insert(bin_name: str, index: int, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``insert`` operation that splices value into the bin at codepoint @@ -858,20 +877,3 @@ def regex_replace( "policy": policy, "ctx": ctx } - - -def to_string(bin_name: str): - """ - Create ``to_string`` operation that converts an integer, double, string, or blob - bin to its string representation. Raises :exc:`~aerospike.exception.BinIncompatibleType` for - any other bin type. This top-level operation does not accept ctx and does not - send a msgpack payload. - - Args: - - bin_name: name of string bin. - """ - return { - "op": aerospike._OP_STRING_TO_STRING, - "bin": bin_name, - } diff --git a/src/include/policy.h b/src/include/policy.h index 1a82671647..620b9fdcdd 100644 --- a/src/include/policy.h +++ b/src/include/policy.h @@ -102,7 +102,8 @@ enum Aerospike_send_bool_as_values { X(STRING_REPEAT), \ X(STRING_REGEX_REPLACE), \ X(STRING_APPEND), \ - X(STRING_PREPEND), + X(STRING_PREPEND), \ + X(STRING_TO_STRING), // clang-format on enum { diff --git a/src/main/client/operate.c b/src/main/client/operate.c index b85bd6619e..7b96b916cb 100644 --- a/src/main/client/operate.c +++ b/src/main/client/operate.c @@ -218,7 +218,7 @@ static inline bool use_operate_conversion_helper(int op) op == OP_LIST_REMOVE_BY_VALUE_RANGE || op == OP_LIST_SET_ORDER || op == OP_LIST_SORT || op == OP_LIST_REMOVE_BY_VALUE_RANK_RANGE_REL || op == OP_LIST_GET_BY_VALUE_RANK_RANGE_REL || op == OP_LIST_CREATE || - (op >= OP_STRING_STRLEN && op <= OP_STRING_PREPEND) || + (op >= OP_STRING_STRLEN && op <= OP_STRING_TO_STRING) || (op == OP_MAP_REMOVE_BY_KEY_INDEX_RANGE_REL || op == OP_MAP_REMOVE_BY_VALUE_RANK_RANGE_REL || op == OP_MAP_GET_BY_VALUE_RANK_RANGE_REL || diff --git a/src/main/client/operate_helper.c b/src/main/client/operate_helper.c index f641d933e5..623f759fd3 100644 --- a/src/main/client/operate_helper.c +++ b/src/main/client/operate_helper.c @@ -766,7 +766,9 @@ as_status as_operations_add_from_pyobject(AerospikeClient *self, as_error *err, success = as_operations_string_prepend(ops, bin, ctx_ref, &str_policy, str_attr_value1); break; - + case OP_STRING_TO_STRING: + success = as_operations_to_string(ops, bin); + break; case OP_MAP_REMOVE_BY_VALUE_RANK_RANGE_REL: { if (range_specified) { success = as_operations_map_remove_by_value_rel_rank_range( diff --git a/src/main/convert_expressions.c b/src/main/convert_expressions.c index 133ef9e84a..6f55943c14 100644 --- a/src/main/convert_expressions.c +++ b/src/main/convert_expressions.c @@ -473,6 +473,7 @@ static as_status get_expr_size(int *size_to_alloc, int *intermediate_exprs_size, [OP_STRING_B64_DECODE] = EXP_SZ(as_exp_string_b64_decode(NIL)), [OP_STRING_REGEX_COMPARE] = EXP_SZ(as_exp_string_regex_compare_flags("", 0, NIL)), + [OP_STRING_TO_STRING] = EXP_SZ(as_exp_to_string(NIL)), [OP_STRING_INSERT] = EXP_SZ(as_exp_string_insert(NULL, 0, "", NIL)), [OP_STRING_OVERWRITE] = EXP_SZ(as_exp_string_overwrite(NULL, 0, "", NIL)), @@ -1908,6 +1909,9 @@ add_expr_macros(AerospikeClient *self, as_static_pool *static_pool, case OP_STRING_B64_DECODE: APPEND_ARRAY(1, as_exp_string_b64_decode(NIL)); break; + case OP_STRING_TO_STRING: + APPEND_ARRAY(1, as_exp_to_string(NIL)); + break; case OP_STRING_REGEX_REPLACE: case OP_STRING_REGEX_COMPARE: { char *pattern = NULL; diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index 5a12f89cf5..08df198857 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -8,6 +8,7 @@ NON_STR_BIN_NAME = "nonstr" STR_BIN_NAME = "str" +INT_BIN_NAME = "int" UPPERCASE_STR_BIN_NAME = "uppercase_str" NESTED_STR_BIN_NAME = "nested_str" STR_WITH_INT_BIN_NAME = "str_with_int" @@ -47,7 +48,8 @@ BASE64_ENCODED_BIN_NAME: BASE64_ENCODED_STR, SURROUNDING_WHITESPACE_BIN_NAME: EXAMPLE_STR_WITH_SURROUNDING_WHITESPACE, MULTILINE_STR_BIN_NAME: MULTILINE_STR, - MULTILINE_STR_WITH_CR_BIN_NAME: MULTILINE_STR_WITH_CR + MULTILINE_STR_WITH_CR_BIN_NAME: MULTILINE_STR_WITH_CR, + INT_BIN_NAME: 1 } START_IDX = 1 diff --git a/test/new_tests/test_expressions_string.py b/test/new_tests/test_expressions_string.py index 157fe2638c..f6d1306534 100644 --- a/test/new_tests/test_expressions_string.py +++ b/test/new_tests/test_expressions_string.py @@ -67,6 +67,10 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa ( str_expr.RegexCompare(pattern="π", regex_flags=RegexFlags.DEFAULT, bin=MULTIBYTE_CODEPOINT_BIN_NAME), False + ), + ( + str_expr.ToString(bin=INT_BIN_NAME), + str(BINS[INT_BIN_NAME]) ) ] ) diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 7be5405c39..c61c1ecc4a 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -271,6 +271,17 @@ def test_regex_compare(self, pattern: str, expected_result: bool): assert bins[MULTIBYTE_CODEPOINT_BIN_NAME] is expected_result + @expect_server_version_earlier_than_8_1_3_to_fail + def test_to_string(self): + ops = [ + str_ops.to_string(bin_name=INT_BIN_NAME) + ] + + with self.expected_context_for_pos_tests: + _, _, bins = self.as_connection.operate(KEY, ops) + + assert bins[INT_BIN_NAME] == str(BINS[INT_BIN_NAME]) + # Write operations def add_read_op(self, ops, bin_name): @@ -488,6 +499,7 @@ def test_string_policy_no_fail(self): (str_ops.trim_end, {}, False), (str_ops.trim, {}, False), (str_ops.regex_replace, {"pattern": "a", "replacement": "b"}, False), + (str_ops.to_string, {}, False), ] ) @expect_server_version_earlier_than_8_1_3_to_fail From b08d986201e61ef2f164b0c2ed5d4aaac781bf93 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:57:23 -0700 Subject: [PATCH 12/18] Fully implement to_string() expression and operation. TODO - to_string expression tests currently fail locally, so run in CI/CD --- aerospike_helpers/expressions/string.py | 2 +- src/include/policy.h | 1 + src/main/aerospike.c | 3 ++- src/main/convert_expressions.c | 4 ++-- test/new_tests/string_helpers.py | 10 ++++++---- test/new_tests/test_expressions_string.py | 14 +++++++++++++- test/new_tests/test_string_operations.py | 21 +++++++++++++++------ 7 files changed, 40 insertions(+), 15 deletions(-) diff --git a/aerospike_helpers/expressions/string.py b/aerospike_helpers/expressions/string.py index f952ae2b63..bc53eea44b 100644 --- a/aerospike_helpers/expressions/string.py +++ b/aerospike_helpers/expressions/string.py @@ -402,7 +402,7 @@ def __init__(self, pattern: str, bin: "TypeBinName", regex_flags: RegexFlags = R class ToString(_BaseExpr): - _op = aerospike._AS_EXP_CODE_CALL + _op = aerospike._AS_EXP_TO_STRING def __init__(self, bin: "TypeBinName"): """ diff --git a/src/include/policy.h b/src/include/policy.h index 620b9fdcdd..e68c2f8413 100644 --- a/src/include/policy.h +++ b/src/include/policy.h @@ -221,6 +221,7 @@ enum { _AS_EXP_LOOPVAR_HLL, _AS_EXP_CODE_CALL_SELECT, _AS_EXP_CODE_CALL_APPLY, + _AS_EXP_TO_STRING }; // Can be either for select or apply diff --git a/src/main/aerospike.c b/src/main/aerospike.c index c592105045..cd2203228d 100644 --- a/src/main/aerospike.c +++ b/src/main/aerospike.c @@ -589,11 +589,12 @@ static struct module_constant_name_to_value module_constants[] = { EXPOSE_MACRO(_AS_EXP_LOOPVAR_GEOJSON), EXPOSE_MACRO(_AS_EXP_LOOPVAR_HLL), - // C client uses the same expression code for these two expressions + // C client uses the same expression code for some expressions // so we define unique ones in the Python client code EXPOSE_MACRO(_AS_EXP_CODE_CALL), EXPOSE_MACRO(_AS_EXP_CODE_CALL_SELECT), EXPOSE_MACRO(_AS_EXP_CODE_CALL_APPLY), + EXPOSE_MACRO(_AS_EXP_TO_STRING), EXPOSE_MACRO(_AS_EXP_CODE_REMOVE_RESULT), EXPOSE_MACRO(_AS_EXP_CODE_IN_LIST), diff --git a/src/main/convert_expressions.c b/src/main/convert_expressions.c index 6f55943c14..2e100ebcf4 100644 --- a/src/main/convert_expressions.c +++ b/src/main/convert_expressions.c @@ -473,7 +473,7 @@ static as_status get_expr_size(int *size_to_alloc, int *intermediate_exprs_size, [OP_STRING_B64_DECODE] = EXP_SZ(as_exp_string_b64_decode(NIL)), [OP_STRING_REGEX_COMPARE] = EXP_SZ(as_exp_string_regex_compare_flags("", 0, NIL)), - [OP_STRING_TO_STRING] = EXP_SZ(as_exp_to_string(NIL)), + [_AS_EXP_TO_STRING] = EXP_SZ(as_exp_to_string(NIL)), [OP_STRING_INSERT] = EXP_SZ(as_exp_string_insert(NULL, 0, "", NIL)), [OP_STRING_OVERWRITE] = EXP_SZ(as_exp_string_overwrite(NULL, 0, "", NIL)), @@ -1909,7 +1909,7 @@ add_expr_macros(AerospikeClient *self, as_static_pool *static_pool, case OP_STRING_B64_DECODE: APPEND_ARRAY(1, as_exp_string_b64_decode(NIL)); break; - case OP_STRING_TO_STRING: + case _AS_EXP_TO_STRING: APPEND_ARRAY(1, as_exp_to_string(NIL)); break; case OP_STRING_REGEX_REPLACE: diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index 08df198857..350fe078b1 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -6,9 +6,10 @@ KEY = KEYS[0] -NON_STR_BIN_NAME = "nonstr" +INT_BIN_NAME = "nonstr" STR_BIN_NAME = "str" -INT_BIN_NAME = "int" +DOUBLE_BIN_NAME = "double" +BLOB_BIN_NAME = "blob" UPPERCASE_STR_BIN_NAME = "uppercase_str" NESTED_STR_BIN_NAME = "nested_str" STR_WITH_INT_BIN_NAME = "str_with_int" @@ -39,7 +40,7 @@ BINS = { STR_BIN_NAME: EXAMPLE_STR, - NON_STR_BIN_NAME: 1, + INT_BIN_NAME: 1, UPPERCASE_STR_BIN_NAME: UPPERCASE_STR, NESTED_STR_BIN_NAME: [EXAMPLE_STR], STR_WITH_INT_BIN_NAME: STRING_WITH_INT, @@ -49,7 +50,8 @@ SURROUNDING_WHITESPACE_BIN_NAME: EXAMPLE_STR_WITH_SURROUNDING_WHITESPACE, MULTILINE_STR_BIN_NAME: MULTILINE_STR, MULTILINE_STR_WITH_CR_BIN_NAME: MULTILINE_STR_WITH_CR, - INT_BIN_NAME: 1 + DOUBLE_BIN_NAME: 1.0, + BLOB_BIN_NAME: b'123' } START_IDX = 1 diff --git a/test/new_tests/test_expressions_string.py b/test/new_tests/test_expressions_string.py index f6d1306534..c48890c9b9 100644 --- a/test/new_tests/test_expressions_string.py +++ b/test/new_tests/test_expressions_string.py @@ -71,7 +71,19 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa ( str_expr.ToString(bin=INT_BIN_NAME), str(BINS[INT_BIN_NAME]) - ) + ), + ( + str_expr.ToString(bin=DOUBLE_BIN_NAME), + str(BINS[INT_BIN_NAME]) + ), + ( + str_expr.ToString(bin=STR_BIN_NAME), + str(BINS[STR_BIN_NAME]) + ), + ( + str_expr.ToString(bin=BLOB_BIN_NAME), + bytes.decode(BINS[BLOB_BIN_NAME]) + ), ] ) @expect_server_version_earlier_than_8_1_3_to_fail diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index c61c1ecc4a..2694a041e9 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -272,15 +272,24 @@ def test_regex_compare(self, pattern: str, expected_result: bool): assert bins[MULTIBYTE_CODEPOINT_BIN_NAME] is expected_result @expect_server_version_earlier_than_8_1_3_to_fail - def test_to_string(self): + @pytest.mark.parametrize( + "bin_name, expected_result", + [ + (INT_BIN_NAME, str(BINS[INT_BIN_NAME])), + (DOUBLE_BIN_NAME, str(BINS[INT_BIN_NAME])), + (STR_BIN_NAME, BINS[STR_BIN_NAME]), + (BLOB_BIN_NAME, bytes.decode(BINS[BLOB_BIN_NAME])) + ] + ) + def test_to_string(self, bin_name: str, expected_result: str): ops = [ - str_ops.to_string(bin_name=INT_BIN_NAME) + str_ops.to_string(bin_name=bin_name) ] with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) - assert bins[INT_BIN_NAME] == str(BINS[INT_BIN_NAME]) + assert bins[bin_name] == expected_result # Write operations @@ -465,14 +474,14 @@ def test_regex_flags(self, bin_name: str, regex_flags: RegexFlags, pattern: str, def test_string_policy_no_fail(self): policy = StringPolicy(write_flags=WriteFlags.NO_FAIL) ops = [ - str_ops.insert(bin_name=NON_STR_BIN_NAME, index=0, value="a", policy=policy) + str_ops.insert(bin_name=INT_BIN_NAME, index=0, value="a", policy=policy) ] - self.add_read_op(ops, NON_STR_BIN_NAME) + self.add_read_op(ops, INT_BIN_NAME) with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) - assert bins[NON_STR_BIN_NAME] == BINS[NON_STR_BIN_NAME] + assert bins[INT_BIN_NAME] == BINS[INT_BIN_NAME] @pytest.mark.parametrize( "op, kwargs, creates_bin", From 1a14d52bcb607d58c434080aa56d80b023782488 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:11:56 -0700 Subject: [PATCH 13/18] To make simpler, just have ToString expression reuse aerospike._OP_STRING_TO_STRING like the to_string() operation helper. --- aerospike_helpers/expressions/string.py | 2 +- src/include/policy.h | 3 +-- src/main/aerospike.c | 1 - src/main/convert_expressions.c | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/aerospike_helpers/expressions/string.py b/aerospike_helpers/expressions/string.py index bc53eea44b..199e07f89a 100644 --- a/aerospike_helpers/expressions/string.py +++ b/aerospike_helpers/expressions/string.py @@ -402,7 +402,7 @@ def __init__(self, pattern: str, bin: "TypeBinName", regex_flags: RegexFlags = R class ToString(_BaseExpr): - _op = aerospike._AS_EXP_TO_STRING + _op = aerospike._OP_STRING_TO_STRING def __init__(self, bin: "TypeBinName"): """ diff --git a/src/include/policy.h b/src/include/policy.h index e68c2f8413..7897af4f2f 100644 --- a/src/include/policy.h +++ b/src/include/policy.h @@ -220,8 +220,7 @@ enum { _AS_EXP_LOOPVAR_GEOJSON, _AS_EXP_LOOPVAR_HLL, _AS_EXP_CODE_CALL_SELECT, - _AS_EXP_CODE_CALL_APPLY, - _AS_EXP_TO_STRING + _AS_EXP_CODE_CALL_APPLY }; // Can be either for select or apply diff --git a/src/main/aerospike.c b/src/main/aerospike.c index cd2203228d..8d030e311f 100644 --- a/src/main/aerospike.c +++ b/src/main/aerospike.c @@ -594,7 +594,6 @@ static struct module_constant_name_to_value module_constants[] = { EXPOSE_MACRO(_AS_EXP_CODE_CALL), EXPOSE_MACRO(_AS_EXP_CODE_CALL_SELECT), EXPOSE_MACRO(_AS_EXP_CODE_CALL_APPLY), - EXPOSE_MACRO(_AS_EXP_TO_STRING), EXPOSE_MACRO(_AS_EXP_CODE_REMOVE_RESULT), EXPOSE_MACRO(_AS_EXP_CODE_IN_LIST), diff --git a/src/main/convert_expressions.c b/src/main/convert_expressions.c index 2e100ebcf4..6f55943c14 100644 --- a/src/main/convert_expressions.c +++ b/src/main/convert_expressions.c @@ -473,7 +473,7 @@ static as_status get_expr_size(int *size_to_alloc, int *intermediate_exprs_size, [OP_STRING_B64_DECODE] = EXP_SZ(as_exp_string_b64_decode(NIL)), [OP_STRING_REGEX_COMPARE] = EXP_SZ(as_exp_string_regex_compare_flags("", 0, NIL)), - [_AS_EXP_TO_STRING] = EXP_SZ(as_exp_to_string(NIL)), + [OP_STRING_TO_STRING] = EXP_SZ(as_exp_to_string(NIL)), [OP_STRING_INSERT] = EXP_SZ(as_exp_string_insert(NULL, 0, "", NIL)), [OP_STRING_OVERWRITE] = EXP_SZ(as_exp_string_overwrite(NULL, 0, "", NIL)), @@ -1909,7 +1909,7 @@ add_expr_macros(AerospikeClient *self, as_static_pool *static_pool, case OP_STRING_B64_DECODE: APPEND_ARRAY(1, as_exp_string_b64_decode(NIL)); break; - case _AS_EXP_TO_STRING: + case OP_STRING_TO_STRING: APPEND_ARRAY(1, as_exp_to_string(NIL)); break; case OP_STRING_REGEX_REPLACE: From 3f97c8d9406150c028fcdfa2aaed7f7692790c3d Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:21:18 -0700 Subject: [PATCH 14/18] The problem was passing in the bin name as a string to string expressions assumes that the bin contains a string. This usually would not be true for the ToString expression. This issue may also happen in other expressions like list, map etc. --- test/new_tests/test_expressions_string.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/new_tests/test_expressions_string.py b/test/new_tests/test_expressions_string.py index c48890c9b9..f2c7d8d7c8 100644 --- a/test/new_tests/test_expressions_string.py +++ b/test/new_tests/test_expressions_string.py @@ -1,7 +1,7 @@ import pytest import base64 -from aerospike_helpers.expressions import string as str_expr +from aerospike_helpers.expressions import string as str_expr, IntBin, FloatBin, BlobBin from aerospike_helpers.operations import expression_operations as expr_ops from aerospike_helpers.operations import operations from aerospike_helpers.string_helpers import NumericType, RegexFlags @@ -69,11 +69,11 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa False ), ( - str_expr.ToString(bin=INT_BIN_NAME), + str_expr.ToString(bin=IntBin(INT_BIN_NAME)), str(BINS[INT_BIN_NAME]) ), ( - str_expr.ToString(bin=DOUBLE_BIN_NAME), + str_expr.ToString(bin=FloatBin(DOUBLE_BIN_NAME)), str(BINS[INT_BIN_NAME]) ), ( @@ -81,7 +81,7 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa str(BINS[STR_BIN_NAME]) ), ( - str_expr.ToString(bin=BLOB_BIN_NAME), + str_expr.ToString(bin=BlobBin(BLOB_BIN_NAME)), bytes.decode(BINS[BLOB_BIN_NAME]) ), ] From 9012927c767bb5c51aec1670263b67da6c3a557c Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:27:31 -0700 Subject: [PATCH 15/18] Revert changes that have nothing to do with append or prepend. --- .../operations/string_operations.py | 48 ++-------------- aerospike_helpers/string_helpers.py | 9 +-- src/include/policy.h | 2 +- src/main/aerospike.c | 2 +- test/new_tests/test_string_operations.py | 55 +------------------ 5 files changed, 12 insertions(+), 104 deletions(-) diff --git a/aerospike_helpers/operations/string_operations.py b/aerospike_helpers/operations/string_operations.py index 447a721322..62b7ed6cf6 100644 --- a/aerospike_helpers/operations/string_operations.py +++ b/aerospike_helpers/operations/string_operations.py @@ -419,10 +419,7 @@ def to_string(bin_name: str): def insert(bin_name: str, index: int, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``insert`` operation that splices value into the bin at codepoint - index. - - Negative indexes count from the end of the string. - If the bin doesn't exist, this operation will create a new bin. + index. Negative indexes count from the end of the string. Args: @@ -445,10 +442,8 @@ def insert(bin_name: str, index: int, value: str, policy: StringPolicy | None = def overwrite(bin_name: str, index: int, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``overwrite`` operation that overwrites codepoints starting at index - with value. - - The result may grow beyond the original length when value extends past the end. - If the bin doesn't exist, this operation will create a new bin. + with value. The result may grow beyond the original length when value extends + past the end. Args: @@ -471,10 +466,8 @@ def overwrite(bin_name: str, index: int, value: str, policy: StringPolicy | None def append(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``append`` operation that appends value to the end of the bin. - Unlike :func:`~aerospike_helpers.operations.operations.append`, this string-package operation uses Unicode codepoint semantics and supports string policy and ctx. - If the bin doesn't exist, this operation will create a new bin. Args: @@ -495,10 +488,8 @@ def append(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: T def prepend(bin_name: str, value: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``prepend`` operation that prepend value to the start of the bin. - Unlike :func:`~aerospike_helpers.operations.operations.prepend`, this string-package operation uses Unicode codepoint semantics and supports string policy and ctx. - If the bin doesn't exist, this operation will create a new bin. Args: @@ -521,8 +512,6 @@ def concat(bin_name: str, value_list: list[str], policy: StringPolicy | None = N Create string ``concat`` operation that appends each string element in values to the bin in order. - If the bin doesn't exist, this operation will create a new bin. - Args: bin_name: name of string bin. @@ -543,8 +532,6 @@ def snip(bin_name: str, start: int, end: int, policy: StringPolicy | None = None """ Create string ``snip`` operation that removes codepoints from start to end. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -568,8 +555,6 @@ def replace(bin_name: str, needle: str, replacement: str, policy: StringPolicy | Create string ``replace`` operation that replaces the first occurrence of needle with replacement. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -593,8 +578,6 @@ def replace_all(bin_name: str, needle: str, replacement: str, policy: StringPoli Create string ``replace_all`` operation that replaces every occurrence of needle with replacement. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -617,8 +600,6 @@ def upper(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None """ Create string ``upper`` operation that uppercases the bin in place. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -637,8 +618,6 @@ def lower(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None """ Create string ``lower`` operation that lowercases the bin in place. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -656,10 +635,7 @@ def lower(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None def casefold(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``case_fold`` operation that applies locale-independent case folding - (lowercase) to the bin. - - This is useful for normalized comparison keys. - If the bin doesn't exist, this operation will be a no-op. + (lowercase) to the bin. This is useful for normalized comparison keys. Args: @@ -678,9 +654,7 @@ def casefold(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = N def normalize_nfc(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None): """ Create string ``normalize_nfc`` operation that normalizes the bin to Unicode NFC. - Already-normalized strings are unchanged. - If the bin doesn't exist, this operation will be a no-op. Args: @@ -701,8 +675,6 @@ def trim_start(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = Create string ``trim_start`` operation that removes whitespace from the start of the bin. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -722,8 +694,6 @@ def trim_end(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = N Create string ``trim_end`` operation that removes whitespace from the end of the bin. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -742,8 +712,6 @@ def trim(bin_name: str, policy: StringPolicy | None = None, ctx: TypeCTX = None) """ Create string ``trim`` operation that removes whitespace from both ends of the bin. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. @@ -770,8 +738,6 @@ def pad_start( the bin reaches ``target_length`` codepoints. No-op when the bin is already at or above the target length. - If the bin doesn't exist, this operation will create a new bin. - Args: bin_name: name of string bin. @@ -802,8 +768,6 @@ def pad_end( bin reaches ``target_length`` codepoints. No-op when the bin is already at or above the target length. - If the bin doesn't exist, this operation will create a new bin. - Args: bin_name: name of string bin. @@ -826,8 +790,6 @@ def repeat(bin_name: str, count: int, policy: StringPolicy | None = None, ctx: T """ Create string ``repeat`` operation that repeats the bin contents count times. - If the bin doesn't exist, this operation will create a new bin. - Args: bin_name: name of string bin. @@ -857,8 +819,6 @@ def regex_replace( with replacement. Pass :py:attr:`~aerospike_helpers.string_helpers.RegexFlags.GLOBAL` to replace every match. This server operation accepts regex flags but not string policy flags. - If the bin doesn't exist, this operation will be a no-op. - Args: bin_name: name of string bin. diff --git a/aerospike_helpers/string_helpers.py b/aerospike_helpers/string_helpers.py index 5cce0ce82c..d0fe73b512 100644 --- a/aerospike_helpers/string_helpers.py +++ b/aerospike_helpers/string_helpers.py @@ -48,16 +48,13 @@ class WriteFlags(IntEnum): String operation policy write bit flags. Use bitwise OR to combine flags. """ + #: Default. Allow create or update. DEFAULT = 0 - """ - Default. Create or replace. - """ NO_FAIL = 4 """ - Suppress an operation failure with the bin unchanged. - - Does not suppress wrong-type errors. + Do not raise an error if a modify operation cannot be applied because + the target bin does not exist. The record is left unchanged. """ diff --git a/src/include/policy.h b/src/include/policy.h index 7897af4f2f..620b9fdcdd 100644 --- a/src/include/policy.h +++ b/src/include/policy.h @@ -220,7 +220,7 @@ enum { _AS_EXP_LOOPVAR_GEOJSON, _AS_EXP_LOOPVAR_HLL, _AS_EXP_CODE_CALL_SELECT, - _AS_EXP_CODE_CALL_APPLY + _AS_EXP_CODE_CALL_APPLY, }; // Can be either for select or apply diff --git a/src/main/aerospike.c b/src/main/aerospike.c index 8d030e311f..c592105045 100644 --- a/src/main/aerospike.c +++ b/src/main/aerospike.c @@ -589,7 +589,7 @@ static struct module_constant_name_to_value module_constants[] = { EXPOSE_MACRO(_AS_EXP_LOOPVAR_GEOJSON), EXPOSE_MACRO(_AS_EXP_LOOPVAR_HLL), - // C client uses the same expression code for some expressions + // C client uses the same expression code for these two expressions // so we define unique ones in the Python client code EXPOSE_MACRO(_AS_EXP_CODE_CALL), EXPOSE_MACRO(_AS_EXP_CODE_CALL_SELECT), diff --git a/test/new_tests/test_string_operations.py b/test/new_tests/test_string_operations.py index 2694a041e9..3ac4042e9f 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -474,60 +474,11 @@ def test_regex_flags(self, bin_name: str, regex_flags: RegexFlags, pattern: str, def test_string_policy_no_fail(self): policy = StringPolicy(write_flags=WriteFlags.NO_FAIL) ops = [ - str_ops.insert(bin_name=INT_BIN_NAME, index=0, value="a", policy=policy) + str_ops.insert(bin_name=NON_STR_BIN_NAME, index=0, value="a", policy=policy) ] - self.add_read_op(ops, INT_BIN_NAME) + self.add_read_op(ops, NON_STR_BIN_NAME) with self.expected_context_for_pos_tests: _, _, bins = self.as_connection.operate(KEY, ops) - assert bins[INT_BIN_NAME] == BINS[INT_BIN_NAME] - - @pytest.mark.parametrize( - "op, kwargs, creates_bin", - [ - # Positive - (str_ops.append, {"value": NEEDLE}, True), - (str_ops.prepend, {"value": NEEDLE}, True), - (str_ops.concat, {"value_list": [NEEDLE]}, True), - (str_ops.overwrite, {"index": 0, "value": NEEDLE}, True), - (str_ops.insert, {"index": 0, "value": NEEDLE}, True), - (str_ops.pad_start, {"target_length": 4, "pad_string": NEEDLE}, True), - (str_ops.pad_end, {"target_length": 4, "pad_string": NEEDLE}, True), - (str_ops.repeat, {"count": 2}, True), - (str_ops.repeat, {"count": 2}, True), - # Negative - (str_ops.snip, {"start": 0, "end": 1}, False), - (str_ops.replace, {"needle": "a", "replacement": "b"}, False), - (str_ops.replace_all, {"needle": "a", "replacement": "b"}, False), - (str_ops.upper, {}, False), - (str_ops.lower, {}, False), - (str_ops.casefold, {}, False), - (str_ops.normalize_nfc, {}, False), - (str_ops.trim_start, {}, False), - (str_ops.trim_end, {}, False), - (str_ops.trim, {}, False), - (str_ops.regex_replace, {"pattern": "a", "replacement": "b"}, False), - (str_ops.to_string, {}, False), - ] - ) - @expect_server_version_earlier_than_8_1_3_to_fail - def test_string_ops_on_nonexistent_bin(self, op, kwargs: dict, creates_bin: bool): - ops = [ - op(bin_name=NON_EXISTENT_BIN_NAME, **kwargs), - operations.read(NON_EXISTENT_BIN_NAME) - ] - - with self.expected_context_for_pos_tests: - _, _, bins = self.as_connection.operate(KEY, ops) - - if creates_bin is False: - assert bins[NON_EXISTENT_BIN_NAME] is None - return - - assert NON_EXISTENT_BIN_NAME in bins - - if op == str_ops.repeat: - assert bins[NON_EXISTENT_BIN_NAME] == "" - else: - assert bins[NON_EXISTENT_BIN_NAME] == NEEDLE + assert bins[NON_STR_BIN_NAME] == BINS[NON_STR_BIN_NAME] From 09f48eb278d6bb7b4b8cfd8f24fa732b59a45abe Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:30:28 -0700 Subject: [PATCH 16/18] Clear up that passing the bin name as a Python str will expect the bin to contain a string. This is because Aerospike expressions are strictly typed and we must pass in the bin value type --- aerospike_helpers/expressions/string.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aerospike_helpers/expressions/string.py b/aerospike_helpers/expressions/string.py index 199e07f89a..b778fcf64f 100644 --- a/aerospike_helpers/expressions/string.py +++ b/aerospike_helpers/expressions/string.py @@ -409,6 +409,7 @@ def __init__(self, bin: "TypeBinName"): Args: bin: A bin expression to apply this function to. + If this argument is a string, the bin must contain a string. Returns: From 0bbf0b52a33a87fba46e4f6949b81a8632fa8059 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:37:55 -0700 Subject: [PATCH 17/18] Address test issue. --- test/new_tests/string_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index 350fe078b1..febb3f0ca0 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -6,7 +6,7 @@ KEY = KEYS[0] -INT_BIN_NAME = "nonstr" +NON_STR_BIN_NAME = INT_BIN_NAME = "nonstr" STR_BIN_NAME = "str" DOUBLE_BIN_NAME = "double" BLOB_BIN_NAME = "blob" From 8d9e20a933ad484fe76c939aca446defd2748774 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:38:51 -0700 Subject: [PATCH 18/18] Remove unused helper variable for string ops tests. --- test/new_tests/string_helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index febb3f0ca0..1cef3d127c 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -19,7 +19,6 @@ SURROUNDING_WHITESPACE_BIN_NAME = "whitespace" MULTILINE_STR_BIN_NAME = "multiline" MULTILINE_STR_WITH_CR_BIN_NAME = "multiline_cr" -NON_EXISTENT_BIN_NAME = "nonexistent" PAD_STRING = " " SINGLE_CHAR = "z"