From c5636591addda6bccdb88b252ea52dd2145ba319 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:20:29 -0700 Subject: [PATCH 01/30] Update C client to revision that supports enhanced error messages. --- aerospike-client-c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aerospike-client-c b/aerospike-client-c index 49e704236d..ae201bf16c 160000 --- a/aerospike-client-c +++ b/aerospike-client-c @@ -1 +1 @@ -Subproject commit 49e704236d38667d0e77bf2d8d9c95b10250230b +Subproject commit ae201bf16c02977d16a9bc90931184fe6a40cf00 From a8b5ab9ff9df6571286e0f5694e5bc09d39f2e1f Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:48:23 -0700 Subject: [PATCH 02/30] Add missing op local variable to represent op code. --- .../operations/string_operations.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/aerospike_helpers/operations/string_operations.py b/aerospike_helpers/operations/string_operations.py index ddc2ec45ea..0c89b1a7ce 100644 --- a/aerospike_helpers/operations/string_operations.py +++ b/aerospike_helpers/operations/string_operations.py @@ -34,6 +34,7 @@ Python strings, but they cannot embedded NULL bytes. """ +import aerospike from ..string_helpers import NumericType, RegexFlags, StringPolicy, __generate_docstrings_for_all_func_members import sys @@ -55,6 +56,7 @@ def strlen(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_STRLEN return locals() @@ -71,6 +73,7 @@ def substr(bin_name: str, start: int, length: int | None = None, ctx: TypeCTX = length (int): Number of codepoints to return. {ctx} """ + op = aerospike._OP_STRING_SUBSTR return locals() @@ -85,6 +88,7 @@ def char_at(bin_name: str, index: int, ctx: TypeCTX = None): index (int): Index of the codepoint to return. {ctx} """ + op = aerospike._OP_STRING_CHAR_AT return locals() @@ -101,6 +105,7 @@ def find(bin_name: str, needle: str, occurrence: int = 1, ctx: TypeCTX = None): index (int): Index of the codepoint to return. {ctx} """ + op = aerospike._OP_STRING_FIND return locals() @@ -114,6 +119,7 @@ def contains(bin_name: str, needle: int, ctx: TypeCTX = None): {needle_get} {ctx} """ + op = aerospike._OP_STRING_CONTAINS return locals() @@ -128,6 +134,7 @@ def starts_with(bin_name: str, prefix: str, ctx: TypeCTX = None): prefix (str): The string to search for. {ctx} """ + op = aerospike._OP_STRING_STARTS_WITH return locals() @@ -142,6 +149,7 @@ def ends_with(bin_name: str, suffix: str, ctx: TypeCTX = None): suffix (str): The string to search for. {ctx} """ + op = aerospike._OP_STRING_ENDS_WITH return locals() @@ -155,6 +163,7 @@ def to_integer(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TO_INTEGER return locals() @@ -168,6 +177,7 @@ def to_double(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TO_DOUBLE return locals() @@ -181,6 +191,7 @@ def byte_length(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_BYTE_LENGTH return locals() @@ -195,6 +206,7 @@ def is_numeric(bin_name: str, numeric_type: NumericType = NumericType.ANY, ctx: numeric_type (:py:class:`~aerospike_helpers.string_helpers.NumericType`): The numeric type to filter for. {ctx} """ + op = aerospike._OP_STRING_IS_NUMERIC return locals() @@ -208,6 +220,7 @@ def is_upper(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_IS_UPPER return locals() @@ -221,6 +234,7 @@ def is_lower(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_IS_LOWER return locals() @@ -234,6 +248,7 @@ def to_blob(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TO_BLOB return locals() @@ -248,6 +263,7 @@ def split(bin_name: str, separator: str = None, ctx: TypeCTX = None): becomes one string element in the returned list. {ctx} """ + op = aerospike._OP_STRING_SPLIT return locals() @@ -261,6 +277,7 @@ def base64_decode(bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_B64_DECODE return locals() @@ -276,6 +293,7 @@ def regex_compare(bin_name: str, pattern: str, regex_flags: RegexFlags = RegexFl {regex_flags} {ctx} """ + op = aerospike._OP_STRING_REGEX_COMPARE return locals() @@ -292,6 +310,7 @@ def insert(policy: StringPolicy, bin_name: str, index: int, value: str, ctx: Typ value (str): The value to insert. {ctx} """ + op = aerospike._OP_STRING_INSERT return locals() @@ -309,6 +328,7 @@ def overwrite(policy: StringPolicy, bin_name: str, index: int, value: str, ctx: value (str): The value to overwrite. {ctx} """ + op = aerospike._OP_STRING_OVERWRITE return locals() @@ -323,6 +343,7 @@ def concat(policy: StringPolicy, bin_name: str, value: str, ctx: TypeCTX = None) value (str): The value to append. {ctx} """ + op = aerospike._OP_STRING_CONCAT return locals() @@ -338,7 +359,8 @@ def concat_list(policy: StringPolicy, bin_name: str, value_list: list[str], ctx: value_list (str): The list of values to append. {ctx} """ - pass + op = aerospike._OP_STRING_CONCAT_LIST + return locals() def snip(policy: StringPolicy, bin_name: str, start: int, end: int | None = None, ctx: TypeCTX = None): @@ -354,6 +376,7 @@ def snip(policy: StringPolicy, bin_name: str, start: int, end: int | None = None remove from start through the end of the string. {ctx} """ + op = aerospike._OP_STRING_SNIP return locals() @@ -370,6 +393,7 @@ def replace(policy: StringPolicy, bin_name: str, needle: str, replacement: str, {replacement} {ctx} """ + op = aerospike._OP_STRING_REPLACE return locals() @@ -386,6 +410,7 @@ def replace_all(policy: StringPolicy, bin_name: str, needle: str, replacement: s {replacement} {ctx} """ + op = aerospike._OP_STRING_REPLACE_ALL return locals() @@ -399,6 +424,7 @@ def upper(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_UPPER return locals() @@ -412,6 +438,7 @@ def lower(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_LOWER return locals() @@ -427,6 +454,7 @@ def casefold(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_CASE_FOLD return locals() @@ -442,6 +470,7 @@ def normalize_nfc(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_NORMALIZE_NFC return locals() @@ -456,6 +485,7 @@ def trim_start(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TRIM_START return locals() @@ -470,6 +500,7 @@ def trim_end(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TRIM_END return locals() @@ -483,6 +514,7 @@ def trim(policy: StringPolicy, bin_name: str, ctx: TypeCTX = None): {bin_name} {ctx} """ + op = aerospike._OP_STRING_TRIM return locals() @@ -500,6 +532,7 @@ def pad_start(policy: StringPolicy, bin_name: str, target_length: int, pad_strin {pad_string} {ctx} """ + op = aerospike._OP_STRING_PAD_START return locals() @@ -517,6 +550,7 @@ def pad_end(policy: StringPolicy, bin_name: str, target_length: int, pad_string: {pad_string} {ctx} """ + op = aerospike._OP_STRING_PAD_END return locals() @@ -531,6 +565,7 @@ def repeat(policy: StringPolicy, bin_name: str, count: int, ctx: TypeCTX = None) count (int): The number of times to repeat the string. Must be non-negative. {ctx} """ + op = aerospike._OP_STRING_REPEAT return locals() @@ -557,6 +592,7 @@ def regex_replace( {regex_flags} {ctx} """ + op = aerospike._OP_STRING_REGEX_REPLACE return locals() @@ -571,6 +607,7 @@ def to_string(bin_name: str): {bin_name} """ + op = aerospike._OP_STRING_TO_STRING return locals() From 21f0d739a0eabf77593daa5e2be4fff43ac9fca2 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:49:23 -0700 Subject: [PATCH 03/30] Document new subcode attribute in exception class. --- doc/exception.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/exception.rst b/doc/exception.rst index 3ad3f3b2f1..7725705cba 100644 --- a/doc/exception.rst +++ b/doc/exception.rst @@ -40,6 +40,13 @@ Base Class The associated status code. + .. py:attribute:: subcode + + Server error detail subcode. When ``error_detail_verbosity`` is greater than or equal to ``1`` on the command's + base policy and the server returns structured error details, this field contains the numeric subcode. + + Set to ``0`` when no subcode was returned. + .. py:attribute:: msg The human-readable error message. From e66716d77aba55428db4b8ea83c1716991401740 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:56:08 -0700 Subject: [PATCH 04/30] Add docs for base policy error_detail_verbosity option --- doc/client.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/client.rst b/doc/client.rst index 2e18adf07f..28b4c95022 100755 --- a/doc/client.rst +++ b/doc/client.rst @@ -1751,6 +1751,17 @@ Base Policies Default: :py:obj:`None` + * **error_detail_verbosity** (:class:`int`) + + Request server error detail fields in responses. + + ``0`` - disabled (no error details returned). Default. + ``1`` - return subcode only. + ``2`` - return subcode and human-readable message. + + When enabled and the server returns error details, :py:attr:`aerospike.exception.AerospikeError.subcode` will contain the + numeric subcode and :py:attr:`aerospike.exception.AerospikeError.msg` will contain the server-authored message. + .. _aerospike_write_policies: Write Policies From 70cf6842988175eafecaed3593a38ceaac710db2 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:02:24 -0700 Subject: [PATCH 05/30] Fix list formatting for error_detail_verbosity --- doc/client.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/client.rst b/doc/client.rst index 28b4c95022..85afa909d0 100755 --- a/doc/client.rst +++ b/doc/client.rst @@ -1755,9 +1755,9 @@ Base Policies Request server error detail fields in responses. - ``0`` - disabled (no error details returned). Default. - ``1`` - return subcode only. - ``2`` - return subcode and human-readable message. + - ``0``: disabled (no error details returned). Default. + - ``1``: return subcode only. + - ``2``: return subcode and human-readable message. When enabled and the server returns error details, :py:attr:`aerospike.exception.AerospikeError.subcode` will contain the numeric subcode and :py:attr:`aerospike.exception.AerospikeError.msg` will contain the server-authored message. From c2192d92e2d06810f1c8348224af14e5f38462d5 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:29:36 -0700 Subject: [PATCH 06/30] Add happy path tests. Add missing subcode attribute to AerospikeError class. --- aerospike-stubs/exception.pyi | 1 + test/new_tests/test_exception_subcode.py | 52 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 test/new_tests/test_exception_subcode.py diff --git a/aerospike-stubs/exception.pyi b/aerospike-stubs/exception.pyi index c3f11713da..e5a3b1fc52 100644 --- a/aerospike-stubs/exception.pyi +++ b/aerospike-stubs/exception.pyi @@ -3,6 +3,7 @@ from typing import Union class AerospikeError(Exception): # When attributes are first assigned to exception class, they have an initial value of None code: Union[int, None] + subcode: int msg: Union[str, None] file: Union[str, None] line: Union[int, None] diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py new file mode 100644 index 0000000000..8e6b59e576 --- /dev/null +++ b/test/new_tests/test_exception_subcode.py @@ -0,0 +1,52 @@ +import pytest +from .conftest import KEYS +from aerospike import exception as e +from aerospike_helpers.operations import list_operations as list_ops +from . import as_errors + + +KEY = KEYS[0] +OPS = [ + list_ops.list_get_by_index(99) +] +ERROR_DETAIL_VERBOSITY_SETTING = "error_detail_verbosity" + + +class TestExceptionSubcode: + # TODO: need to reuse fixture in conftest.py using indirect params to set num of records + def setup(self): + self.as_connection.put(KEY, bins={"a": []}) + yield + self.as_connection.remove(KEY) + + @pytest.mark.parametrize( + "policy", + [ + {}, + {ERROR_DETAIL_VERBOSITY_SETTING: 0}, + {ERROR_DETAIL_VERBOSITY_SETTING: 1}, + {ERROR_DETAIL_VERBOSITY_SETTING: 2}, + ] + ) + def test_minimum_error_verbosity(self, policy: dict): + with pytest.raises(e.InvalidRequest) as excinfo: + self.as_connection.operate(KEYS[0], OPS, policy=policy) + + # Make sure there's no regression with the parent error code + assert excinfo.value.code == as_errors.AEROSPIKE_ERR_REQUEST_INVALID + if policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0: + assert excinfo.value.subcode == 0 + else: + assert excinfo.value.subcode > 0 + + SUBCODE_IN_MESSAGE = "subcode=" + if policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0: + assert SUBCODE_IN_MESSAGE not in excinfo.value.msg + elif policy[ERROR_DETAIL_VERBOSITY_SETTING] == 1: + # Make sure there's no regression with the server error message + # with lower verbosity + assert SUBCODE_IN_MESSAGE in excinfo.value.msg + else: + # There should be a message before the subcode + SUBCODE_IN_QUOTES = "({})".format(SUBCODE_IN_MESSAGE) + assert SUBCODE_IN_QUOTES in excinfo.value.msg From 6a2ad1131f53ec01290f577a23fc06ce9f441d81 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:35:42 -0700 Subject: [PATCH 07/30] Add negative test for invalid verbosity. Don't see any client side check for verbosity levels > 2, so I guess the server will return an error. --- test/new_tests/test_exception_subcode.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index 8e6b59e576..f4aa104c45 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -50,3 +50,10 @@ def test_minimum_error_verbosity(self, policy: dict): # There should be a message before the subcode SUBCODE_IN_QUOTES = "({})".format(SUBCODE_IN_MESSAGE) assert SUBCODE_IN_QUOTES in excinfo.value.msg + + def test_invalid_verbosity(self): + policy = { + ERROR_DETAIL_VERBOSITY_SETTING: 3 + } + with pytest.raises(e.ServerError): + self.as_connection.operate(KEYS[0], OPS, policy=policy) From c5c7d937ba9b3dddcdf51c81bd7341514cb844db Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:04:25 -0700 Subject: [PATCH 08/30] Add implementation for error_detail_verbosity --- src/main/policy.c | 1 + src/main/policy_config.c | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/policy.c b/src/main/policy.c index f80cf2e9c0..231cca2890 100644 --- a/src/main/policy.c +++ b/src/main/policy.c @@ -322,6 +322,7 @@ static inline as_status pyobject_to_policy_base(AerospikeClient *self, POLICY_SET_FIELD(sleep_between_retries, uint32_t); POLICY_SET_FIELD(compress, bool); POLICY_SET_FIELD(connect_timeout, uint32_t); + POLICY_SET_FIELD(error_detail_verbosity, uint8_t); // Setting txn field to a non-NULL value in a query or scan policy is a no-op, // so this is safe to call for a scan/query policy's base policy diff --git a/src/main/policy_config.c b/src/main/policy_config.c index 6a76b45818..c9cfba056a 100644 --- a/src/main/policy_config.c +++ b/src/main/policy_config.c @@ -18,6 +18,7 @@ #include "policy_config.h" #include "types.h" #include "policy.h" +#include "conversions.h" as_status set_optional_key(as_policy_key *target_ptr, PyObject *py_policy, const char *name); @@ -900,6 +901,27 @@ as_status set_batch_remove_policy(as_error *err, return AEROSPIKE_OK; } +as_status set_optional_uint8_property(uint8_t *target_ptr, PyObject *py_policy, + const char *name) +{ + // Assume py_policy is a Python dictionary + PyObject *py_policy_val = PyDict_GetItemString(py_policy, name); + if (!py_policy_val) { + // Key doesn't exist in policy + return AEROSPIKE_OK; + } + + uint8_t int_value = convert_pyobject_to_uint8_t(py_policy_val); + + if (PyErr_Occurred()) { + PyErr_Clear(); + return AEROSPIKE_ERR_PARAM; + } + + *target_ptr = int_value; + return AEROSPIKE_OK; +} + as_status set_base_policy(as_policy_base *base_policy, PyObject *py_policy) { @@ -956,6 +978,11 @@ as_status set_base_policy(as_policy_base *base_policy, PyObject *py_policy) return status; } + status = set_optional_uint8_property(&base_policy->error_detail_verbosity, + py_policy, "error_detail_verbosity"); + if (status != AEROSPIKE_OK) { + return status; + } return AEROSPIKE_OK; } From 3ec37bef9ad0f3934dd4ffb283a9c555f0a783c5 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:11:49 -0700 Subject: [PATCH 09/30] Finish implementation for AerospikeError's subcode attribute. --- src/main/conversions.c | 19 +++++++++++++------ src/main/exception.c | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/conversions.c b/src/main/conversions.c index 61d55e0570..1f8e3e0339 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -54,11 +54,15 @@ #define PY_KEYT_KEY 2 #define PY_KEYT_DIGEST 3 -#define PY_EXCEPTION_CODE 0 -#define PY_EXCEPTION_MSG 1 -#define PY_EXCEPTION_FILE 2 -#define PY_EXCEPTION_LINE 3 -#define AS_PY_EXCEPTION_IN_DOUBT 4 +enum { + PY_EXCEPTION_CODE = 0, + PY_EXCEPTION_MSG, + PY_EXCEPTION_FILE, + PY_EXCEPTION_LINE, + AS_PY_EXCEPTION_IN_DOUBT, + PY_EXCEPTION_SUBCODE, + EXCEPTION_TUPLE_MEMBER_COUNT +}; #define CTX_KEY "ctx" #define CDT_CTX_ORDER_KEY "order_key" @@ -2287,12 +2291,15 @@ void error_to_pyobject(const as_error *err, PyObject **obj) PyObject *py_in_doubt = err->in_doubt ? Py_True : Py_False; Py_INCREF(py_in_doubt); - PyObject *py_err = PyTuple_New(5); + PyObject *py_subcode = PyLong_FromLong(err->subcode); + + PyObject *py_err = PyTuple_New(EXCEPTION_TUPLE_MEMBER_COUNT); PyTuple_SetItem(py_err, PY_EXCEPTION_CODE, py_code); PyTuple_SetItem(py_err, PY_EXCEPTION_MSG, py_message); PyTuple_SetItem(py_err, PY_EXCEPTION_FILE, py_file); PyTuple_SetItem(py_err, PY_EXCEPTION_LINE, py_line); PyTuple_SetItem(py_err, AS_PY_EXCEPTION_IN_DOUBT, py_in_doubt); + PyTuple_SetItem(py_err, PY_EXCEPTION_SUBCODE, py_subcode); *obj = py_err; } diff --git a/src/main/exception.c b/src/main/exception.c index 9ec4b89066..e1db02a537 100644 --- a/src/main/exception.c +++ b/src/main/exception.c @@ -68,8 +68,8 @@ struct exception_def { #define NO_ERROR_CODE 0 // Same order as the tuple of args passed into the exception -const char *const aerospike_err_attrs[] = {"code", "msg", "file", - "line", "in_doubt", NULL}; +const char *const aerospike_err_attrs[] = { + "code", "msg", "file", "line", "in_doubt", "subcode", NULL}; const char *const record_err_attrs[] = {"key", "bin", NULL}; const char *const index_err_attrs[] = {"name", NULL}; const char *const udf_err_attrs[] = {"module", "func", NULL}; From 961ddbed1da18d60e7581645c7d47a6bb2a7df0d Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:56:06 -0700 Subject: [PATCH 10/30] Address test syntax error. --- test/new_tests/test_exception_subcode.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index f4aa104c45..a89e81db2b 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -1,5 +1,6 @@ import pytest -from .conftest import KEYS +from .conftest import KEYS, BIN_NAME +import aerospike from aerospike import exception as e from aerospike_helpers.operations import list_operations as list_ops from . import as_errors @@ -7,7 +8,7 @@ KEY = KEYS[0] OPS = [ - list_ops.list_get_by_index(99) + list_ops.list_get_by_index(BIN_NAME, index=99, return_type=aerospike.LIST_RETURN_VALUE) ] ERROR_DETAIL_VERBOSITY_SETTING = "error_detail_verbosity" @@ -15,7 +16,7 @@ class TestExceptionSubcode: # TODO: need to reuse fixture in conftest.py using indirect params to set num of records def setup(self): - self.as_connection.put(KEY, bins={"a": []}) + self.as_connection.put(KEY, bins={BIN_NAME: []}) yield self.as_connection.remove(KEY) From 8ee462f24dbc9755e2ab78e75a20d9024eeb9681 Mon Sep 17 00:00:00 2001 From: juliannguyen4 <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:13:33 +0000 Subject: [PATCH 11/30] Address all test failures. --- src/main/aerospike.c | 3 ++- test/new_tests/as_errors.py | 2 ++ test/new_tests/test_exception_subcode.py | 14 ++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/aerospike.c b/src/main/aerospike.c index 612a5cae83..7652e3d4ed 100644 --- a/src/main/aerospike.c +++ b/src/main/aerospike.c @@ -704,7 +704,8 @@ DEFINE_SET_OF_VALID_KEYS(client_config_tls, "enable", "cafile", "capath", #define BASE_POLICY_KEYS \ "total_timeout", "socket_timeout", "max_retries", "sleep_between_retries", \ - "compress", "txn", "expressions", "connect_timeout", "timeout_delay" + "compress", "txn", "expressions", "connect_timeout", "timeout_delay", \ + "error_detail_verbosity" DEFINE_SET_OF_VALID_KEYS(apply_policy, BASE_POLICY_KEYS, "key", "replica", "commit_level", "durable_delete", "ttl", diff --git a/test/new_tests/as_errors.py b/test/new_tests/as_errors.py index c12574049e..5eadffd67e 100644 --- a/test/new_tests/as_errors.py +++ b/test/new_tests/as_errors.py @@ -214,6 +214,8 @@ AEROSPIKE_ERR_FAIL_ELEMENT_EXISTS = 24 +AEROSPIKE_ERR_OP_NOT_APPLICABLE = 26 + AEROSPIKE_FILTERED_OUT = 27 # diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index a89e81db2b..afa70ec448 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -15,7 +15,8 @@ class TestExceptionSubcode: # TODO: need to reuse fixture in conftest.py using indirect params to set num of records - def setup(self): + @pytest.fixture(autouse=True) + def setup(self, as_connection): self.as_connection.put(KEY, bins={BIN_NAME: []}) yield self.as_connection.remove(KEY) @@ -30,18 +31,19 @@ def setup(self): ] ) def test_minimum_error_verbosity(self, policy: dict): - with pytest.raises(e.InvalidRequest) as excinfo: + with pytest.raises(e.OpNotApplicable) as excinfo: self.as_connection.operate(KEYS[0], OPS, policy=policy) # Make sure there's no regression with the parent error code - assert excinfo.value.code == as_errors.AEROSPIKE_ERR_REQUEST_INVALID - if policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0: + assert excinfo.value.code == as_errors.AEROSPIKE_ERR_OP_NOT_APPLICABLE + err_verbosity_is_zero = ERROR_DETAIL_VERBOSITY_SETTING not in policy or policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0 + if err_verbosity_is_zero: assert excinfo.value.subcode == 0 else: assert excinfo.value.subcode > 0 SUBCODE_IN_MESSAGE = "subcode=" - if policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0: + if err_verbosity_is_zero: assert SUBCODE_IN_MESSAGE not in excinfo.value.msg elif policy[ERROR_DETAIL_VERBOSITY_SETTING] == 1: # Make sure there's no regression with the server error message @@ -49,7 +51,7 @@ def test_minimum_error_verbosity(self, policy: dict): assert SUBCODE_IN_MESSAGE in excinfo.value.msg else: # There should be a message before the subcode - SUBCODE_IN_QUOTES = "({})".format(SUBCODE_IN_MESSAGE) + SUBCODE_IN_QUOTES = "({}".format(SUBCODE_IN_MESSAGE) assert SUBCODE_IN_QUOTES in excinfo.value.msg def test_invalid_verbosity(self): From 4fb264fd8234a535ba765c781207b87f5fd1a451 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:17:52 -0700 Subject: [PATCH 12/30] Move subcode attribute to be last since it's the last element in the tuple of exception args. --- doc/exception.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/exception.rst b/doc/exception.rst index 7725705cba..e0055c295b 100644 --- a/doc/exception.rst +++ b/doc/exception.rst @@ -40,13 +40,6 @@ Base Class The associated status code. - .. py:attribute:: subcode - - Server error detail subcode. When ``error_detail_verbosity`` is greater than or equal to ``1`` on the command's - base policy and the server returns structured error details, this field contains the numeric subcode. - - Set to ``0`` when no subcode was returned. - .. py:attribute:: msg The human-readable error message. @@ -63,6 +56,13 @@ Base Class ``True`` if it is possible that the command succeeded. See :ref:`indoubt`. + .. py:attribute:: subcode + + Server error detail subcode. When ``error_detail_verbosity`` is greater than or equal to ``1`` on the command's + base policy and the server returns structured error details, this field contains the numeric subcode. + + Set to ``0`` when no subcode was returned. + In addition to accessing these attributes by their names, \ they can also be checked by calling ``exc.args[i]``, where ``exc`` is the exception object and \ ``i`` is the index of the attribute in the order they appear above. \ From 81eeeaab9e3464705e1deba6e769e16a689699f3 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:26:42 -0700 Subject: [PATCH 13/30] Have tests expect the returned error subcode to be zero for unsupported server versions. This is to make sure undefined behavior doesn't happen in the client (e.g uninitialized reads) --- test/new_tests/test_exception_subcode.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index afa70ec448..738ccba623 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -3,6 +3,7 @@ import aerospike from aerospike import exception as e from aerospike_helpers.operations import list_operations as list_ops +from .test_base_class import TestBaseClass from . import as_errors @@ -36,7 +37,16 @@ def test_minimum_error_verbosity(self, policy: dict): # Make sure there's no regression with the parent error code assert excinfo.value.code == as_errors.AEROSPIKE_ERR_OP_NOT_APPLICABLE - err_verbosity_is_zero = ERROR_DETAIL_VERBOSITY_SETTING not in policy or policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0 + + err_verbosity_is_zero = ( + ERROR_DETAIL_VERBOSITY_SETTING not in policy + or + policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0 + or + # If running against a unsupported version, we expect subcode to always return 0 + # (and no undefined behavior) + (TestBaseClass.major_ver, TestBaseClass.minor_ver, TestBaseClass.patch_ver) < (8, 1, 3) + ) if err_verbosity_is_zero: assert excinfo.value.subcode == 0 else: From bddc408da38ca2bddc8b1401e2c4eb7991ac91c4 Mon Sep 17 00:00:00 2001 From: juliannguyen4 <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:41:58 +0000 Subject: [PATCH 14/30] Now that Nate's feature branch for string ops is rebased onto master, the server now has the correct 8.1.3 version. --- test/new_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/new_tests/conftest.py b/test/new_tests/conftest.py index 24106e01b5..bbfaa2e3c0 100644 --- a/test/new_tests/conftest.py +++ b/test/new_tests/conftest.py @@ -326,7 +326,7 @@ def expect_earlier_than_server_version_to_fail(as_connection, request): expect_server_version_earlier_than_8_1_3_to_fail = pytest.mark.parametrize( "expect_earlier_than_server_version_to_fail", [ - (8, 1, 2) + (8, 1, 3) ], indirect=True ) From 6cdcb85403a207aa0a03a454c62949e5c9d533f2 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:50:16 -0700 Subject: [PATCH 15/30] Address failing spellcheck --- doc/spelling_wordlist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/spelling_wordlist.txt b/doc/spelling_wordlist.txt index 48c700fbad..f5c69a015d 100644 --- a/doc/spelling_wordlist.txt +++ b/doc/spelling_wordlist.txt @@ -103,3 +103,4 @@ lowercases lowercased msgpack precomposed +subcode From ae388db250106d69a363dec0d39ae50cfb709542 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:01:14 -0700 Subject: [PATCH 16/30] Refactor some deprecation message work. Deprecate operations.prepend() and append() only when strings are passed as arguments in favor of the string ops version. --- src/include/client.h | 3 +++ src/main/client/operate.c | 26 ++++++++++++++++++++++++++ src/main/conversions.c | 13 ++++++------- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/include/client.h b/src/include/client.h index d4aecf92a6..d5498d418c 100644 --- a/src/include/client.h +++ b/src/include/client.h @@ -591,3 +591,6 @@ PyObject *AerospikeClient_Abort(AerospikeClient *self, PyObject *args, "Operations and bin names are mutually exclusive." \ "In the next major client release, when this %s object is executed, a " \ "ParamError will be raised." + +#define DEPRECATION_MESSAGE_TEMPLATE \ + "%s is deprecated and will be removed in the next client major release" diff --git a/src/main/client/operate.c b/src/main/client/operate.c index c95e338de8..633f791198 100644 --- a/src/main/client/operate.c +++ b/src/main/client/operate.c @@ -310,6 +310,10 @@ bool opRequiresKey(int op) op == OP_MAP_GET_BY_KEY_RANGE); } +#define DEPRECATED_PREPEND_NAME \ + "aerospike_helpers.operations.operations.prepend" +#define DEPRECATED_APPEND_NAME "aerospike_helpers.operations.operations.append" + as_status add_op(AerospikeClient *self, as_error *err, PyObject *py_operation_dict, as_vector *unicodeStrVector, as_static_pool *static_pool, as_operations *ops, long *op, @@ -615,6 +619,17 @@ as_status add_op(AerospikeClient *self, as_error *err, } case AS_OPERATOR_APPEND: if (PyUnicode_Check(py_value)) { + int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, + DEPRECATION_MESSAGE_TEMPLATE, + DEPRECATED_APPEND_NAME); + if (retval == -1) { + // This handles the codepath where warnings are converted into errors from pytest/python cli + // TODO: this does NOT handle the codepath where the warning mechanism itself fails + return as_error_update(err, AEROSPIKE_ERR, + DEPRECATION_MESSAGE_TEMPLATE, + DEPRECATED_APPEND_NAME); + } + py_ustr1 = PyUnicode_AsUTF8String(py_value); val = strdup(PyBytes_AsString(py_ustr1)); as_operations_add_append_str(ops, bin, val); @@ -648,6 +663,17 @@ as_status add_op(AerospikeClient *self, as_error *err, break; case AS_OPERATOR_PREPEND: if (PyUnicode_Check(py_value)) { + int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, + DEPRECATION_MESSAGE_TEMPLATE, + DEPRECATED_PREPEND_NAME); + if (retval == -1) { + // This handles the codepath where warnings are converted into errors from pytest/python cli + // TODO: this does NOT handle the codepath where the warning mechanism itself fails + return as_error_update(err, AEROSPIKE_ERR, + DEPRECATION_MESSAGE_TEMPLATE, + DEPRECATED_PREPEND_NAME); + } + py_ustr1 = PyUnicode_AsUTF8String(py_value); val = strdup(PyBytes_AsString(py_ustr1)); as_operations_add_prepend_str(ops, bin, val); diff --git a/src/main/conversions.c b/src/main/conversions.c index 1f8e3e0339..addd219524 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -2379,9 +2379,7 @@ void initialize_bin_for_strictypes(AerospikeClient *self, as_error *err, strcpy(binop_bin->name, bin); } -#define META_TTL_DEPRECATION_MESSAGE \ - "meta[\"ttl\"] is deprecated and will be removed in " \ - "the next client major release." +#define META_TTL_FOR_DEPRECATION_WARNING "meta[\"ttl\"]" /** ******************************************************************************************************* @@ -2421,14 +2419,15 @@ as_status check_and_set_meta(PyObject *py_meta, uint32_t *ttl_ref, uint32_t ttl = 0; uint16_t gen = 0; if (py_ttl) { - int retval = - PyErr_WarnEx(PyExc_DeprecationWarning, - META_TTL_DEPRECATION_MESSAGE, STACK_LEVEL); + int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, + DEPRECATION_MESSAGE_TEMPLATE, + META_TTL_FOR_DEPRECATION_WARNING); if (retval == -1) { // This handles the codepath where warnings are converted into errors from pytest/python cli // TODO: this does NOT handle the codepath where the warning mechanism itself fails return as_error_update(err, AEROSPIKE_ERR, - META_TTL_DEPRECATION_MESSAGE); + DEPRECATION_MESSAGE_TEMPLATE, + META_TTL_FOR_DEPRECATION_WARNING); } if (PyLong_Check(py_ttl)) { From 093731e1b33b106a99719811bb91e180a0d23293 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:11:26 -0700 Subject: [PATCH 17/30] Revert "Refactor some deprecation message work. Deprecate operations.prepend() and append() only when strings are passed as arguments in favor of the string ops version." This reverts commit ae388db250106d69a363dec0d39ae50cfb709542. --- src/include/client.h | 3 --- src/main/client/operate.c | 26 -------------------------- src/main/conversions.c | 13 +++++++------ 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/include/client.h b/src/include/client.h index d5498d418c..d4aecf92a6 100644 --- a/src/include/client.h +++ b/src/include/client.h @@ -591,6 +591,3 @@ PyObject *AerospikeClient_Abort(AerospikeClient *self, PyObject *args, "Operations and bin names are mutually exclusive." \ "In the next major client release, when this %s object is executed, a " \ "ParamError will be raised." - -#define DEPRECATION_MESSAGE_TEMPLATE \ - "%s is deprecated and will be removed in the next client major release" diff --git a/src/main/client/operate.c b/src/main/client/operate.c index 633f791198..c95e338de8 100644 --- a/src/main/client/operate.c +++ b/src/main/client/operate.c @@ -310,10 +310,6 @@ bool opRequiresKey(int op) op == OP_MAP_GET_BY_KEY_RANGE); } -#define DEPRECATED_PREPEND_NAME \ - "aerospike_helpers.operations.operations.prepend" -#define DEPRECATED_APPEND_NAME "aerospike_helpers.operations.operations.append" - as_status add_op(AerospikeClient *self, as_error *err, PyObject *py_operation_dict, as_vector *unicodeStrVector, as_static_pool *static_pool, as_operations *ops, long *op, @@ -619,17 +615,6 @@ as_status add_op(AerospikeClient *self, as_error *err, } case AS_OPERATOR_APPEND: if (PyUnicode_Check(py_value)) { - int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, - DEPRECATION_MESSAGE_TEMPLATE, - DEPRECATED_APPEND_NAME); - if (retval == -1) { - // This handles the codepath where warnings are converted into errors from pytest/python cli - // TODO: this does NOT handle the codepath where the warning mechanism itself fails - return as_error_update(err, AEROSPIKE_ERR, - DEPRECATION_MESSAGE_TEMPLATE, - DEPRECATED_APPEND_NAME); - } - py_ustr1 = PyUnicode_AsUTF8String(py_value); val = strdup(PyBytes_AsString(py_ustr1)); as_operations_add_append_str(ops, bin, val); @@ -663,17 +648,6 @@ as_status add_op(AerospikeClient *self, as_error *err, break; case AS_OPERATOR_PREPEND: if (PyUnicode_Check(py_value)) { - int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, - DEPRECATION_MESSAGE_TEMPLATE, - DEPRECATED_PREPEND_NAME); - if (retval == -1) { - // This handles the codepath where warnings are converted into errors from pytest/python cli - // TODO: this does NOT handle the codepath where the warning mechanism itself fails - return as_error_update(err, AEROSPIKE_ERR, - DEPRECATION_MESSAGE_TEMPLATE, - DEPRECATED_PREPEND_NAME); - } - py_ustr1 = PyUnicode_AsUTF8String(py_value); val = strdup(PyBytes_AsString(py_ustr1)); as_operations_add_prepend_str(ops, bin, val); diff --git a/src/main/conversions.c b/src/main/conversions.c index addd219524..1f8e3e0339 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -2379,7 +2379,9 @@ void initialize_bin_for_strictypes(AerospikeClient *self, as_error *err, strcpy(binop_bin->name, bin); } -#define META_TTL_FOR_DEPRECATION_WARNING "meta[\"ttl\"]" +#define META_TTL_DEPRECATION_MESSAGE \ + "meta[\"ttl\"] is deprecated and will be removed in " \ + "the next client major release." /** ******************************************************************************************************* @@ -2419,15 +2421,14 @@ as_status check_and_set_meta(PyObject *py_meta, uint32_t *ttl_ref, uint32_t ttl = 0; uint16_t gen = 0; if (py_ttl) { - int retval = PyErr_WarnFormat(PyExc_DeprecationWarning, STACK_LEVEL, - DEPRECATION_MESSAGE_TEMPLATE, - META_TTL_FOR_DEPRECATION_WARNING); + int retval = + PyErr_WarnEx(PyExc_DeprecationWarning, + META_TTL_DEPRECATION_MESSAGE, STACK_LEVEL); if (retval == -1) { // This handles the codepath where warnings are converted into errors from pytest/python cli // TODO: this does NOT handle the codepath where the warning mechanism itself fails return as_error_update(err, AEROSPIKE_ERR, - DEPRECATION_MESSAGE_TEMPLATE, - META_TTL_FOR_DEPRECATION_WARNING); + META_TTL_DEPRECATION_MESSAGE); } if (PyLong_Check(py_ttl)) { From 2011bc31c081604f5b78b961be0289bbcfc1b510 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:17:39 -0700 Subject: [PATCH 18/30] Revert adding subcode to AerospikeError.args tuple to avoid an API breaking change per product team's request. --- src/main/conversions.c | 2 -- src/main/exception.c | 20 +++++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/conversions.c b/src/main/conversions.c index 1f8e3e0339..e4948ab228 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -60,7 +60,6 @@ enum { PY_EXCEPTION_FILE, PY_EXCEPTION_LINE, AS_PY_EXCEPTION_IN_DOUBT, - PY_EXCEPTION_SUBCODE, EXCEPTION_TUPLE_MEMBER_COUNT }; @@ -2299,7 +2298,6 @@ void error_to_pyobject(const as_error *err, PyObject **obj) PyTuple_SetItem(py_err, PY_EXCEPTION_FILE, py_file); PyTuple_SetItem(py_err, PY_EXCEPTION_LINE, py_line); PyTuple_SetItem(py_err, AS_PY_EXCEPTION_IN_DOUBT, py_in_doubt); - PyTuple_SetItem(py_err, PY_EXCEPTION_SUBCODE, py_subcode); *obj = py_err; } diff --git a/src/main/exception.c b/src/main/exception.c index e1db02a537..dbdfd6df24 100644 --- a/src/main/exception.c +++ b/src/main/exception.c @@ -470,9 +470,12 @@ void raise_exception_base(as_error *err, PyObject *py_as_key, PyObject *py_bin, goto CHAIN_PREV_EXC_AND_RETURN; } - for (unsigned long i = 0; - i < sizeof(aerospike_err_attrs) / sizeof(aerospike_err_attrs[0]) - 1; - i++) { + Py_ssize_t tuple_size = PyTuple_Size(py_err_tuple); + if (tuple_size == -1) { + goto CHAIN_PREV_EXC_AND_RETURN; + } + + for (Py_ssize_t i = 0; i < tuple_size; i++) { // Here, we are assuming the number of attrs is the same as the number of tuple members PyObject *py_arg = PyTuple_GetItem(py_err_tuple, i); if (py_arg == NULL) { @@ -485,6 +488,17 @@ void raise_exception_base(as_error *err, PyObject *py_as_key, PyObject *py_bin, } } + PyObject *py_subcode = PyLong_FromUInt32(err->subcode); + if (!py_subcode) { + goto CHAIN_PREV_EXC_AND_RETURN; + } + + int retval = PyObject_SetAttrString( + py_exc_class, aerospike_err_attrs[tuple_size], py_subcode); + if (retval == -1) { + goto CHAIN_PREV_EXC_AND_RETURN; + } + // Raise exception PyErr_SetObject(py_exc_class, py_err_tuple); Py_DECREF(py_err_tuple); From 9ca94b49d3854e0a41f8ae047ef7fe8e04823cba Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:22:03 -0700 Subject: [PATCH 19/30] Rm dead code. --- src/main/conversions.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/conversions.c b/src/main/conversions.c index e4948ab228..5eea7e2e7b 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -2290,8 +2290,6 @@ void error_to_pyobject(const as_error *err, PyObject **obj) PyObject *py_in_doubt = err->in_doubt ? Py_True : Py_False; Py_INCREF(py_in_doubt); - PyObject *py_subcode = PyLong_FromLong(err->subcode); - PyObject *py_err = PyTuple_New(EXCEPTION_TUPLE_MEMBER_COUNT); PyTuple_SetItem(py_err, PY_EXCEPTION_CODE, py_code); PyTuple_SetItem(py_err, PY_EXCEPTION_MSG, py_message); From d8ec58bf5ddab62c4c27b7eb86ac6930d9f7251a Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:37:08 -0700 Subject: [PATCH 20/30] Update C client to include the latest revisions to the string op API. --- aerospike-client-c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aerospike-client-c b/aerospike-client-c index ae201bf16c..e84494241b 160000 --- a/aerospike-client-c +++ b/aerospike-client-c @@ -1 +1 @@ -Subproject commit ae201bf16c02977d16a9bc90931184fe6a40cf00 +Subproject commit e84494241be7c4e9a0c6bd8951f277caf71d96bc From f56a4e202e83cdc1b120ec553f45431fdf788dc6 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:22:14 -0700 Subject: [PATCH 21/30] Revert "Update C client to include the latest revisions to the string op API." This reverts commit d8ec58bf5ddab62c4c27b7eb86ac6930d9f7251a. --- aerospike-client-c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aerospike-client-c b/aerospike-client-c index e84494241b..ae201bf16c 160000 --- a/aerospike-client-c +++ b/aerospike-client-c @@ -1 +1 @@ -Subproject commit e84494241be7c4e9a0c6bd8951f277caf71d96bc +Subproject commit ae201bf16c02977d16a9bc90931184fe6a40cf00 From 6e03d250702766f3a1e641f0cf652f70f2959e0f Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Wed, 17 Jun 2026 19:12:48 -0700 Subject: [PATCH 22/30] Fix crash --- src/main/client/list_and_string_operate.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/client/list_and_string_operate.c b/src/main/client/list_and_string_operate.c index 06fb59069b..13e8f0fd33 100644 --- a/src/main/client/list_and_string_operate.c +++ b/src/main/client/list_and_string_operate.c @@ -344,7 +344,6 @@ as_status add_list_or_string_op(AerospikeClient *self, as_error *err, case OP_STRING_REGEX_COMPARE: case OP_STRING_INSERT: case OP_STRING_OVERWRITE: - case OP_STRING_CONCAT: case OP_STRING_REPLACE: case OP_STRING_REPLACE_ALL: case OP_STRING_PAD_START: From 31441f92494caf58e1b1b7e47fe1636bdc171ab1 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:44:44 -0700 Subject: [PATCH 23/30] Address test failure. --- test/new_tests/test_expressions_string.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/new_tests/test_expressions_string.py b/test/new_tests/test_expressions_string.py index bc91a2648f..0e3f3f38c7 100644 --- a/test/new_tests/test_expressions_string.py +++ b/test/new_tests/test_expressions_string.py @@ -26,8 +26,8 @@ def setup(self, request, as_connection, expect_earlier_than_server_version_to_fa "expr, expected_result", [ (str_expr.StrLen(bin=STR_BIN_NAME), len(BINS[STR_BIN_NAME])), - (str_expr.SubStr(start=START_IDX, end=None, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][START_IDX:]), - (str_expr.SubStr(start=START_IDX, end=START_IDX + 2, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][START_IDX:(START_IDX + 2)]), + (str_expr.SubStr(start=START_IDX, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][START_IDX:]), + (str_expr.SubStrRange(start=START_IDX, end=START_IDX + 2, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][START_IDX:(START_IDX + 2)]), (str_expr.CharAt(index=START_IDX, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][START_IDX]), (str_expr.CharAt(index=-1, bin=STR_BIN_NAME), BINS[STR_BIN_NAME][-1]), (str_expr.Find(needle=NEEDLE, occurrence=1, bin=STR_BIN_NAME), BINS[STR_BIN_NAME].find(NEEDLE)), From 3ea35faf7575f7210dadeba4bed75acc0a1543f6 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:41:13 -0700 Subject: [PATCH 24/30] Address stubtest failure. --- aerospike-stubs/exception.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aerospike-stubs/exception.pyi b/aerospike-stubs/exception.pyi index e5a3b1fc52..48021b69c8 100644 --- a/aerospike-stubs/exception.pyi +++ b/aerospike-stubs/exception.pyi @@ -3,7 +3,7 @@ from typing import Union class AerospikeError(Exception): # When attributes are first assigned to exception class, they have an initial value of None code: Union[int, None] - subcode: int + subcode: Union[int, None] msg: Union[str, None] file: Union[str, None] line: Union[int, None] From 72d42b65ff785c0ac02d283709a227af4f85f8b0 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:27:10 -0700 Subject: [PATCH 25/30] Improve helper function naming. Clear up why subcode attribute name is at index tuple_size --- src/include/conversions.h | 2 +- src/main/conversions.c | 2 +- src/main/exception.c | 3 ++- src/main/query/where.c | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/include/conversions.h b/src/include/conversions.h index b891bcffe6..e33f1cdc88 100644 --- a/src/include/conversions.h +++ b/src/include/conversions.h @@ -127,7 +127,7 @@ as_status metadata_to_pyobject(as_error *err, const as_record *rec, as_status bins_to_pyobject(AerospikeClient *self, as_error *err, const as_record *rec, PyObject **obj); -void error_to_pyobject(const as_error *err, PyObject **obj); +void create_py_tuple_from_as_error(const as_error *err, PyObject **obj); as_status as_privilege_to_pyobject(as_error *err, as_privilege privileges[], PyObject *py_as_privilege, diff --git a/src/main/conversions.c b/src/main/conversions.c index 3d026b76f3..6e6e06ca99 100644 --- a/src/main/conversions.c +++ b/src/main/conversions.c @@ -2263,7 +2263,7 @@ as_status metadata_to_pyobject(as_error *err, const as_record *rec, return err->code; } -void error_to_pyobject(const as_error *err, PyObject **obj) +void create_py_tuple_from_as_error(const as_error *err, PyObject **obj) { PyObject *py_file = NULL; if (err->file) { diff --git a/src/main/exception.c b/src/main/exception.c index dbdfd6df24..16b30dbdab 100644 --- a/src/main/exception.c +++ b/src/main/exception.c @@ -465,7 +465,7 @@ void raise_exception_base(as_error *err, PyObject *py_as_key, PyObject *py_bin, // Convert C error to Python exception PyObject *py_err_tuple = NULL; - error_to_pyobject(err, &py_err_tuple); + create_py_tuple_from_as_error(err, &py_err_tuple); if (!py_err_tuple) { goto CHAIN_PREV_EXC_AND_RETURN; } @@ -493,6 +493,7 @@ void raise_exception_base(as_error *err, PyObject *py_as_key, PyObject *py_bin, goto CHAIN_PREV_EXC_AND_RETURN; } + // Subcode is not included as last element in tuple int retval = PyObject_SetAttrString( py_exc_class, aerospike_err_attrs[tuple_size], py_subcode); if (retval == -1) { diff --git a/src/main/query/where.c b/src/main/query/where.c index f9eb0e37d5..dcd50de34f 100644 --- a/src/main/query/where.c +++ b/src/main/query/where.c @@ -281,7 +281,7 @@ static int AerospikeQuery_Where_Add(AerospikeQuery *self, PyObject *py_ctx, // If it ain't supported, raise and error as_error_update(&err, AEROSPIKE_ERR_PARAM, "unknown predicate type"); PyObject *py_err = NULL; - error_to_pyobject(&err, &py_err); + create_py_tuple_from_as_error(&err, &py_err); PyErr_SetObject(PyExc_Exception, py_err); goto CLEANUP_VALUES_ON_ERROR; } From 25806afcdda973a8451a3b4578e08bb0acd14069 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:34:30 -0700 Subject: [PATCH 26/30] Address misleading test case name. --- test/new_tests/test_exception_subcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index 738ccba623..3122665e32 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -31,7 +31,7 @@ def setup(self, as_connection): {ERROR_DETAIL_VERBOSITY_SETTING: 2}, ] ) - def test_minimum_error_verbosity(self, policy: dict): + def test_error_verbosity_levels(self, policy: dict): with pytest.raises(e.OpNotApplicable) as excinfo: self.as_connection.operate(KEYS[0], OPS, policy=policy) From fa3359aadeefaabca3d5919d635d51b6d494dffa Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:39:58 -0700 Subject: [PATCH 27/30] Rm confusing comment. Improve var naming in tests. --- test/new_tests/test_exception_subcode.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index 3122665e32..e3545709b7 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -38,7 +38,7 @@ def test_error_verbosity_levels(self, policy: dict): # Make sure there's no regression with the parent error code assert excinfo.value.code == as_errors.AEROSPIKE_ERR_OP_NOT_APPLICABLE - err_verbosity_is_zero = ( + subcode_should_be_zero = ( ERROR_DETAIL_VERBOSITY_SETTING not in policy or policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0 @@ -47,21 +47,19 @@ def test_error_verbosity_levels(self, policy: dict): # (and no undefined behavior) (TestBaseClass.major_ver, TestBaseClass.minor_ver, TestBaseClass.patch_ver) < (8, 1, 3) ) - if err_verbosity_is_zero: + if subcode_should_be_zero: assert excinfo.value.subcode == 0 else: assert excinfo.value.subcode > 0 - SUBCODE_IN_MESSAGE = "subcode=" - if err_verbosity_is_zero: - assert SUBCODE_IN_MESSAGE not in excinfo.value.msg + EXPECTED_SUBCODE_IN_MESSAGE = "subcode=" + if subcode_should_be_zero: + assert EXPECTED_SUBCODE_IN_MESSAGE not in excinfo.value.msg elif policy[ERROR_DETAIL_VERBOSITY_SETTING] == 1: - # Make sure there's no regression with the server error message - # with lower verbosity - assert SUBCODE_IN_MESSAGE in excinfo.value.msg + assert EXPECTED_SUBCODE_IN_MESSAGE in excinfo.value.msg else: # There should be a message before the subcode - SUBCODE_IN_QUOTES = "({}".format(SUBCODE_IN_MESSAGE) + SUBCODE_IN_QUOTES = "({}".format(EXPECTED_SUBCODE_IN_MESSAGE) assert SUBCODE_IN_QUOTES in excinfo.value.msg def test_invalid_verbosity(self): From a1c3203d0fb7ebb773c98c07e1946ada6ffac2b7 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:41:04 -0700 Subject: [PATCH 28/30] Make test logic less confusing. --- test/new_tests/test_exception_subcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index e3545709b7..82e72e7682 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -53,7 +53,7 @@ def test_error_verbosity_levels(self, policy: dict): assert excinfo.value.subcode > 0 EXPECTED_SUBCODE_IN_MESSAGE = "subcode=" - if subcode_should_be_zero: + if excinfo.value.subcode == 0: assert EXPECTED_SUBCODE_IN_MESSAGE not in excinfo.value.msg elif policy[ERROR_DETAIL_VERBOSITY_SETTING] == 1: assert EXPECTED_SUBCODE_IN_MESSAGE in excinfo.value.msg From e5930b7d66862b1a4513708ed94912d155002124 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:16:31 -0700 Subject: [PATCH 29/30] Add test cases for client config option to increase code coverage. --- test/new_tests/test_exception_subcode.py | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/new_tests/test_exception_subcode.py b/test/new_tests/test_exception_subcode.py index 82e72e7682..132d597f48 100644 --- a/test/new_tests/test_exception_subcode.py +++ b/test/new_tests/test_exception_subcode.py @@ -23,7 +23,7 @@ def setup(self, as_connection): self.as_connection.remove(KEY) @pytest.mark.parametrize( - "policy", + "policy_w_verbosity_setting", [ {}, {ERROR_DETAIL_VERBOSITY_SETTING: 0}, @@ -31,17 +31,33 @@ def setup(self, as_connection): {ERROR_DETAIL_VERBOSITY_SETTING: 2}, ] ) - def test_error_verbosity_levels(self, policy: dict): + @pytest.mark.parametrize( + "set_in_client_config", + [False, True] + ) + def test_error_verbosity_levels(self, policy_w_verbosity_setting: dict, set_in_client_config: bool): + if set_in_client_config: + config = { + "policies": { + "operate": policy_w_verbosity_setting + } + } + self.as_connection = TestBaseClass.get_new_connection(config) + with pytest.raises(e.OpNotApplicable) as excinfo: - self.as_connection.operate(KEYS[0], OPS, policy=policy) + cmd_policy = {} + if not set_in_client_config: + cmd_policy |= policy_w_verbosity_setting + + self.as_connection.operate(KEYS[0], OPS, policy=cmd_policy) # Make sure there's no regression with the parent error code assert excinfo.value.code == as_errors.AEROSPIKE_ERR_OP_NOT_APPLICABLE subcode_should_be_zero = ( - ERROR_DETAIL_VERBOSITY_SETTING not in policy + ERROR_DETAIL_VERBOSITY_SETTING not in policy_w_verbosity_setting or - policy[ERROR_DETAIL_VERBOSITY_SETTING] == 0 + policy_w_verbosity_setting[ERROR_DETAIL_VERBOSITY_SETTING] == 0 or # If running against a unsupported version, we expect subcode to always return 0 # (and no undefined behavior) @@ -55,7 +71,7 @@ def test_error_verbosity_levels(self, policy: dict): EXPECTED_SUBCODE_IN_MESSAGE = "subcode=" if excinfo.value.subcode == 0: assert EXPECTED_SUBCODE_IN_MESSAGE not in excinfo.value.msg - elif policy[ERROR_DETAIL_VERBOSITY_SETTING] == 1: + elif policy_w_verbosity_setting[ERROR_DETAIL_VERBOSITY_SETTING] == 1: assert EXPECTED_SUBCODE_IN_MESSAGE in excinfo.value.msg else: # There should be a message before the subcode From c8703199517f3cf98fb5aaf55c368625f06f1023 Mon Sep 17 00:00:00 2001 From: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:22:25 -0700 Subject: [PATCH 30/30] Make TestBaseClass.get_new_connection() set the server version digits as integers to be consistent with the as_connection fixture. --- test/new_tests/test_base_class.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/new_tests/test_base_class.py b/test/new_tests/test_base_class.py index 6256e32dd5..0519ba5705 100644 --- a/test/new_tests/test_base_class.py +++ b/test/new_tests/test_base_class.py @@ -178,9 +178,9 @@ def get_new_connection(add_config=None): if res is not None: break res = res.split(".") - TestBaseClass.major_ver = res[0] - TestBaseClass.minor_ver = res[1] - TestBaseClass.minor_ver = res[2] + TestBaseClass.major_ver = int(res[0]) + TestBaseClass.minor_ver = int(res[1]) + TestBaseClass.minor_ver = int(res[2]) # print("major_ver:", TestBaseClass.major_ver, "minor_ver:", TestBaseClass.minor_ver) return client