diff --git a/aerospike_helpers/operations/string_operations.py b/aerospike_helpers/operations/string_operations.py index 62b7ed6cf6..447a721322 100644 --- a/aerospike_helpers/operations/string_operations.py +++ b/aerospike_helpers/operations/string_operations.py @@ -419,7 +419,10 @@ 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. + index. + + Negative indexes count from the end of the string. + If the bin doesn't exist, this operation will create a new bin. Args: @@ -442,8 +445,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: @@ -466,8 +471,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: @@ -488,8 +495,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: @@ -512,6 +521,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. @@ -532,6 +543,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. @@ -555,6 +568,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. @@ -578,6 +593,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. @@ -600,6 +617,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. @@ -618,6 +637,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. @@ -635,7 +656,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: @@ -654,7 +678,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: @@ -675,6 +701,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. @@ -694,6 +722,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. @@ -712,6 +742,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. @@ -738,6 +770,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. @@ -768,6 +802,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. @@ -790,6 +826,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. @@ -819,6 +857,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 d0fe73b512..5cce0ce82c 100644 --- a/aerospike_helpers/string_helpers.py +++ b/aerospike_helpers/string_helpers.py @@ -48,13 +48,16 @@ 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 """ - 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. """ diff --git a/src/include/policy.h b/src/include/policy.h index 620b9fdcdd..7897af4f2f 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 c592105045..8d030e311f 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 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), diff --git a/test/new_tests/string_helpers.py b/test/new_tests/string_helpers.py index 1cef3d127c..febb3f0ca0 100644 --- a/test/new_tests/string_helpers.py +++ b/test/new_tests/string_helpers.py @@ -19,6 +19,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 3ac4042e9f..2694a041e9 100644 --- a/test/new_tests/test_string_operations.py +++ b/test/new_tests/test_string_operations.py @@ -474,11 +474,60 @@ 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", + [ + # 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