diff --git a/python/aerospike_native/_native.pyi b/python/aerospike_native/_native.pyi index 48b8ae0..fea91db 100644 --- a/python/aerospike_native/_native.pyi +++ b/python/aerospike_native/_native.pyi @@ -555,6 +555,14 @@ class BitOperation: r""" Bit operations. Create bit operations used by the client's `operate()` method. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def resize(bin_name: builtins.str, byte_size: builtins.int, resize_flags: typing.Optional[_native.BitwiseResizeFlags], policy: _native.BitPolicy) -> _native.BitOperation: r""" @@ -834,6 +842,14 @@ class CdtOperation: Requires Aerospike Server version >= 8.1.1. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def select_by_path(bin_name: builtins.str, flag: builtins.int, ctx: typing.Sequence[_native.CTX]) -> _native.CdtOperation: r""" @@ -2324,6 +2340,14 @@ class ExpOperation: Expression operations allow evaluating expressions on the server and optionally storing the result in a bin. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def read(name: builtins.str, exp: _native.FilterExpression, flags: typing.Optional[typing.Any] = None) -> _native.ExpOperation: r""" @@ -4166,6 +4190,14 @@ class HllOperation: r""" HLL (HyperLogLog) operations. Create HLL operations used by the client's `operate()` method. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def init(bin_name: builtins.str, index_bit_count: builtins.int, min_hash_bit_count: builtins.int = -1, flags: typing.Optional[typing.Any] = None) -> _native.HllOperation: r""" @@ -4412,6 +4444,14 @@ class ListOperation: r""" List bin operations. Create list operations used by the client's `operate()` method. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def get(bin_name: builtins.str, index: builtins.int) -> _native.ListOperation: r""" @@ -4765,6 +4805,14 @@ class MapOperation: r""" Map bin operations. Create map operations used by the client's `operate()` method. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads are idempotent; + every modify operation of this class is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def size(bin_name: builtins.str) -> _native.MapOperation: r""" @@ -5281,6 +5329,19 @@ class Operation: Python wrapper for Operation enum. """ @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would, so a retry is safe. + + Reads, ``put``, ``delete``, and ``touch`` are idempotent. ``add``, + ``append``, ``prepend``, and every list, map, bit, HLL, string, and + expression modify operation are not, as a family: a few members are + idempotent in effect (list ``clear``, HLL ``add``), but retry + classification keys on the family. Every operation class carries + this property. + """ + @property def bin_name(self) -> typing.Optional[builtins.str]: r""" Name of the bin this operation targets, or ``None`` when it targets @@ -6203,6 +6264,14 @@ class StringOperation: trailing ``ctx`` argument selecting a string element nested inside a list/map bin. With ``ctx=None`` the op targets the bin itself. """ + @property + def is_idempotent(self) -> builtins.bool: + r""" + ``True`` when re-sending this operation after an in-doubt attempt + leaves the record as one application would. Reads and ``to_string`` + are idempotent; every modify operation is not. See + :attr:`Operation.is_idempotent` for the rule. + """ @staticmethod def strlen(bin: builtins.str, *, ctx: typing.Optional[typing.Sequence[_native.CTX]] = None) -> _native.StringOperation: r""" diff --git a/python/tests/unit/operation_test.py b/python/tests/unit/operation_test.py index 4af1833..e9a8688 100644 --- a/python/tests/unit/operation_test.py +++ b/python/tests/unit/operation_test.py @@ -21,10 +21,16 @@ MapOperation, BitOperation, HllOperation, + CdtOperation, + ExpOperation, + StringOperation, + FilterExpression as fe, ListPolicy, ListOrderType, ListReturnType, ListSortFlags, + ModifyFlags, + SelectFlags, MapPolicy, MapOrder, MapReturnType, @@ -412,3 +418,62 @@ def test_get_intersect_count(self): def test_get_similarity(self): op = HllOperation.get_similarity("hllbin", []) assert isinstance(op, HllOperation) + + +_IDEMPOTENT = [ + pytest.param(lambda: Operation.get(), id="get"), + pytest.param(lambda: Operation.get_bin("v"), id="get_bin"), + pytest.param(lambda: Operation.get_header(), id="get_header"), + pytest.param(lambda: Operation.put("v", 1), id="put"), + pytest.param(lambda: Operation.delete(), id="delete"), + pytest.param(lambda: Operation.touch(), id="touch"), + pytest.param(lambda: ListOperation.size("l"), id="list_size"), + pytest.param(lambda: ListOperation.get_by_index("l", 0, ListReturnType.VALUE), id="list_get_by_index"), + pytest.param(lambda: MapOperation.size("m"), id="map_size"), + pytest.param(lambda: MapOperation.get_by_key("m", "k", MapReturnType.VALUE), id="map_get_by_key"), + pytest.param(lambda: BitOperation.get("b", 0, 8), id="bit_get"), + pytest.param(lambda: BitOperation.count("b", 0, 8), id="bit_count"), + pytest.param(lambda: HllOperation.get_count("h"), id="hll_get_count"), + pytest.param(lambda: ExpOperation.read("r", fe.int_bin("v")), id="exp_read"), + pytest.param( + lambda: CdtOperation.select_by_path("c", SelectFlags.VALUE, [CTX.map_key("k")]), + id="cdt_select_by_path", + ), + pytest.param(lambda: StringOperation.strlen("s"), id="string_strlen"), + pytest.param(lambda: StringOperation.to_string("s"), id="string_to_string"), +] + +_NOT_IDEMPOTENT = [ + pytest.param(lambda: Operation.add("v", 1), id="add"), + pytest.param(lambda: Operation.append("v", "x"), id="append"), + pytest.param(lambda: Operation.prepend("v", "x"), id="prepend"), + pytest.param(lambda: ListOperation.append("l", 1, ListPolicy()), id="list_append"), + pytest.param(lambda: ListOperation.clear("l"), id="list_clear"), + pytest.param(lambda: ListOperation.remove_by_value("l", 1, ListReturnType.NONE), id="list_remove_by_value"), + pytest.param(lambda: MapOperation.put("m", "k", 1, MapPolicy()), id="map_put"), + pytest.param(lambda: MapOperation.clear("m"), id="map_clear"), + pytest.param(lambda: MapOperation.increment_value("m", "k", 1, MapPolicy()), id="map_increment"), + pytest.param(lambda: BitOperation.set("b", 0, 8, b"\xff", BitPolicy()), id="bit_set"), + pytest.param(lambda: HllOperation.add("h", [1, 2]), id="hll_add"), + pytest.param(lambda: HllOperation.init("h", 8), id="hll_init"), + pytest.param(lambda: ExpOperation.write("v", fe.int_val(1)), id="exp_write"), + pytest.param( + lambda: CdtOperation.modify_by_path( + "c", ModifyFlags.DEFAULT, fe.int_val(1), [CTX.map_key("k")]), + id="cdt_modify_by_path", + ), + pytest.param(lambda: StringOperation.append("s", "x"), id="string_append"), + pytest.param(lambda: StringOperation.upper("s"), id="string_upper"), +] + + +class TestIsIdempotent: + """Reads, put, delete, and touch may be re-sent; every modify family may not.""" + + @pytest.mark.parametrize("make", _IDEMPOTENT) + def test_idempotent(self, make): + assert make().is_idempotent is True + + @pytest.mark.parametrize("make", _NOT_IDEMPOTENT) + def test_not_idempotent(self, make): + assert make().is_idempotent is False diff --git a/src/operations.rs b/src/operations.rs index 3d07a31..b2dbddb 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -375,6 +375,184 @@ use crate::string_ops::{StringNumericType, StringOperation}; StringToString(String), } + impl OperationType { + /// Whether re-sending this operation after an in-doubt attempt leaves + /// the record as one application would. + /// + /// Reads, `Put`, `Delete`, and `Touch` are idempotent. `Add`, `Append`, + /// `Prepend`, and every list, map, bit, HLL, string, and expression + /// modify operation are reported non-idempotent as a family: a few + /// members are idempotent in effect (`ListClear`, `HllAdd`), but the + /// family is what retry classification keys on, and refining a member + /// is a change to this table alone. The match is exhaustive on + /// purpose, so a new variant has to be placed here before it builds. + pub(crate) fn is_idempotent(&self) -> bool { + match self { + OperationType::Get() + | OperationType::GetBin(_) + | OperationType::GetHeader() + | OperationType::Put(..) + | OperationType::Delete() + | OperationType::Touch() + | OperationType::ListGet(..) + | OperationType::ListSize(_) + | OperationType::ListJoin(..) + | OperationType::ListGetRange(..) + | OperationType::ListGetRangeFrom(..) + | OperationType::ListGetByIndex(..) + | OperationType::ListGetByIndexRange(..) + | OperationType::ListGetByRank(..) + | OperationType::ListGetByRankRange(..) + | OperationType::ListGetByValue(..) + | OperationType::ListGetByValueRange(..) + | OperationType::ListGetByValueList(..) + | OperationType::ListGetByValueRelativeRankRange(..) + | OperationType::MapSize(_) + | OperationType::MapGetByKey(..) + | OperationType::MapGetByKeyRange(..) + | OperationType::MapGetByIndex(..) + | OperationType::MapGetByIndexRange(..) + | OperationType::MapGetByIndexRangeFrom(..) + | OperationType::MapGetByRank(..) + | OperationType::MapGetByRankRange(..) + | OperationType::MapGetByRankRangeFrom(..) + | OperationType::MapGetByValue(..) + | OperationType::MapGetByValueRange(..) + | OperationType::MapGetByKeyList(..) + | OperationType::MapGetByValueList(..) + | OperationType::MapGetByKeyRelativeIndexRange(..) + | OperationType::MapGetByValueRelativeRankRange(..) + | OperationType::BitGet(..) + | OperationType::BitCount(..) + | OperationType::BitLScan(..) + | OperationType::BitRScan(..) + | OperationType::BitGetInt(..) + | OperationType::BitB64Encode(..) + | OperationType::HllGetCount(_) + | OperationType::HllDescribe(_) + | OperationType::HllGetUnion(..) + | OperationType::HllGetUnionCount(..) + | OperationType::HllGetIntersectCount(..) + | OperationType::HllGetSimilarity(..) + | OperationType::ExpRead(..) + | OperationType::CdtSelectByPath(..) + | OperationType::StringStrlen(_) + | OperationType::StringSubstr(..) + | OperationType::StringCharAt(..) + | OperationType::StringFind(..) + | OperationType::StringContains(..) + | OperationType::StringStartsWith(..) + | OperationType::StringEndsWith(..) + | OperationType::StringToInteger(_) + | OperationType::StringToDouble(_) + | OperationType::StringByteLength(_) + | OperationType::StringIsNumeric(..) + | OperationType::StringIsUpper(_) + | OperationType::StringIsLower(_) + | OperationType::StringToBlob(_) + | OperationType::StringSplit(..) + | OperationType::StringB64Decode(_) + | OperationType::StringRegexCompare(..) + | OperationType::StringToString(_) => true, + OperationType::Add(..) + | OperationType::Append(..) + | OperationType::Prepend(..) + | OperationType::ListPop(..) + | OperationType::ListClear(_) + | OperationType::ListSet(..) + | OperationType::ListRemove(..) + | OperationType::ListRemoveRange(..) + | OperationType::ListPopRange(..) + | OperationType::ListPopRangeFrom(..) + | OperationType::ListRemoveRangeFrom(..) + | OperationType::ListTrim(..) + | OperationType::ListAppend(..) + | OperationType::ListAppendItems(..) + | OperationType::ListInsert(..) + | OperationType::ListInsertItems(..) + | OperationType::ListIncrement(..) + | OperationType::ListSort(..) + | OperationType::ListSetOrder(..) + | OperationType::ListRemoveByIndex(..) + | OperationType::ListRemoveByIndexRange(..) + | OperationType::ListRemoveByRank(..) + | OperationType::ListRemoveByRankRange(..) + | OperationType::ListRemoveByValue(..) + | OperationType::ListRemoveByValueList(..) + | OperationType::ListRemoveByValueRange(..) + | OperationType::ListRemoveByValueRelativeRankRange(..) + | OperationType::ListCreate(..) + | OperationType::ListSetWithPolicy(..) + | OperationType::ListIncrementByOne(..) + | OperationType::ListIncrementByOneWithPolicy(..) + | OperationType::ListCreateWithIndex(..) + | OperationType::ListSetOrderWithIndex(..) + | OperationType::MapClear(_) + | OperationType::MapPut(..) + | OperationType::MapPutItems(..) + | OperationType::MapIncrementValue(..) + | OperationType::MapDecrementValue(..) + | OperationType::MapRemoveByKey(..) + | OperationType::MapRemoveByKeyRange(..) + | OperationType::MapRemoveByIndex(..) + | OperationType::MapRemoveByIndexRange(..) + | OperationType::MapRemoveByIndexRangeFrom(..) + | OperationType::MapRemoveByRank(..) + | OperationType::MapRemoveByRankRange(..) + | OperationType::MapRemoveByRankRangeFrom(..) + | OperationType::MapRemoveByValue(..) + | OperationType::MapRemoveByValueRange(..) + | OperationType::MapRemoveByKeyList(..) + | OperationType::MapRemoveByValueList(..) + | OperationType::MapSetMapPolicy(..) + | OperationType::MapRemoveByKeyRelativeIndexRange(..) + | OperationType::MapRemoveByValueRelativeRankRange(..) + | OperationType::MapCreate(..) + | OperationType::MapCreateWithIndex(..) + | OperationType::MapSetPolicy(..) + | OperationType::BitResize(..) + | OperationType::BitInsert(..) + | OperationType::BitRemove(..) + | OperationType::BitSet(..) + | OperationType::BitOr(..) + | OperationType::BitXor(..) + | OperationType::BitAnd(..) + | OperationType::BitNot(..) + | OperationType::BitLShift(..) + | OperationType::BitRShift(..) + | OperationType::BitAdd(..) + | OperationType::BitSubtract(..) + | OperationType::BitSetInt(..) + | OperationType::HllInit(..) + | OperationType::HllAdd(..) + | OperationType::HllRefreshCount(_) + | OperationType::HllFold(..) + | OperationType::HllSetUnion(..) + | OperationType::ExpWrite(..) + | OperationType::CdtModifyByPath(..) + | OperationType::StringInsert(..) + | OperationType::StringOverwrite(..) + | OperationType::StringConcat(..) + | OperationType::StringAppend(..) + | OperationType::StringPrepend(..) + | OperationType::StringSnip(..) + | OperationType::StringReplace(..) + | OperationType::StringReplaceAll(..) + | OperationType::StringUpper(..) + | OperationType::StringLower(..) + | OperationType::StringCaseFold(..) + | OperationType::StringNormalizeNfc(..) + | OperationType::StringTrimStart(..) + | OperationType::StringTrimEnd(..) + | OperationType::StringTrim(..) + | OperationType::StringPadStart(..) + | OperationType::StringPadEnd(..) + | OperationType::StringRepeat(..) + | OperationType::StringRegexReplace(..) => false, + } + } + } + /// Python wrapper for Operation enum. #[gen_stub_pyclass(module = "_native")] #[pyclass(from_py_object, subclass)] @@ -400,6 +578,20 @@ use crate::string_ops::{StringNumericType, StringOperation}; Self::default() } + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would, so a retry is safe. + /// + /// Reads, ``put``, ``delete``, and ``touch`` are idempotent. ``add``, + /// ``append``, ``prepend``, and every list, map, bit, HLL, string, and + /// expression modify operation are not, as a family: a few members are + /// idempotent in effect (list ``clear``, HLL ``add``), but retry + /// classification keys on the family. Every operation class carries + /// this property. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Name of the bin this operation targets, or ``None`` when it targets /// the whole record. /// @@ -514,6 +706,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl ListOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create a List get operation (gets element at index). #[staticmethod] pub fn get(bin_name: String, index: i64) -> Self { @@ -939,6 +1140,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl MapOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create a Map size operation (gets map size). #[staticmethod] pub fn size(bin_name: String) -> Self { @@ -1279,6 +1489,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl BitOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create a Bit resize operation (resizes byte array, requires BitPolicy). #[staticmethod] pub fn resize(bin_name: String, byte_size: i64, resize_flags: Option, policy: BitPolicy) -> Self { @@ -1475,6 +1694,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl HllOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create HLL init operation. /// Server creates a new HLL or resets an existing HLL. /// Server does not return a value. @@ -1647,6 +1875,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl ExpOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create expression read operation. /// /// Evaluates the expression and returns the result. The result is returned @@ -1728,6 +1965,15 @@ use crate::string_ops::{StringNumericType, StringOperation}; #[gen_stub_pymethods] #[pymethods] impl CdtOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads are idempotent; + /// every modify operation of this class is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + /// Create a CDT path select operation. /// /// Reads nested CDT data identified by the given path context, returning diff --git a/src/string_ops.rs b/src/string_ops.rs index 490672f..9d4508d 100644 --- a/src/string_ops.rs +++ b/src/string_ops.rs @@ -434,6 +434,15 @@ pub struct StringOperation { #[gen_stub_pymethods] #[pymethods] impl StringOperation { + /// ``True`` when re-sending this operation after an in-doubt attempt + /// leaves the record as one application would. Reads and ``to_string`` + /// are idempotent; every modify operation is not. See + /// :attr:`Operation.is_idempotent` for the rule. + #[getter] + pub fn is_idempotent(&self) -> bool { + self.op.is_idempotent() + } + // ----------------------------------------------------------------- // Read operations (STRING_READ, op-type 17, sub-ops 0..16) // -----------------------------------------------------------------