diff --git a/CHANGELOG.md b/CHANGELOG.md index 0abfe65..4870864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). ==================== +# 5.5.0 - 2026-02-06 + +## Added + +- support for row creation time +- support for last write metadata of a row + # 5.4.3 - 2025-08-15 ## Fixed diff --git a/LICENSE.txt b/LICENSE.txt index 327d6a7..28f849e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2018, 2025 Oracle and/or its affiliates. +Copyright (c) 2018, 2026 Oracle and/or its affiliates. The Universal Permissive License (UPL), Version 1.0 diff --git a/README.md b/README.md index 5083cba..253ae8d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ install the oci package:: pip install oci -See [the installation guide](https://nosql-python-sdk.readthedocs.io/en/stable/installation.html) for additional requirements and and alternative install +See [the installation guide](https://nosql-python-sdk.readthedocs.io/en/stable/installation.html) for additional requirements and alternative install methods. ## Examples @@ -81,7 +81,7 @@ that the *borneo* package has been installed. ``` python # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -405,7 +405,7 @@ operate correctly. A secure configuration requires a secure proxy and more complex configuration. 1. Start the Oracle NoSQL Database and proxy server based on instructions above. - Note the HTTP port used. By default the endpoint is *localhost:80*. + Note the HTTP port used. By default, the endpoint is *localhost:80*. 2. The *quickstart.py* program defaults to *localhost:80*. If the proxy was started using a different host or port edit the settings accordingly. @@ -426,7 +426,7 @@ Please consult the [security guide](./SECURITY.md) for our responsible security ## License -Copyright (c) 2018, 2025 Oracle and/or its affiliates. +Copyright (c) 2018, 2026 Oracle and/or its affiliates. Released under the Universal Permissive License v1.0 as shown at . diff --git a/examples/config.py b/examples/config.py index 08c0f28..ececf2c 100644 --- a/examples/config.py +++ b/examples/config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/config_cloud.py b/examples/config_cloud.py index 43d1adc..4338813 100644 --- a/examples/config_cloud.py +++ b/examples/config_cloud.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/config_cloudsim.py b/examples/config_cloudsim.py index 4d57722..2275f88 100644 --- a/examples/config_cloudsim.py +++ b/examples/config_cloudsim.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/config_onprem.py b/examples/config_onprem.py index ac95e84..f0e5e31 100644 --- a/examples/config_onprem.py +++ b/examples/config_onprem.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/multi_data_ops.py b/examples/multi_data_ops.py index 0d0566a..d5985ee 100644 --- a/examples/multi_data_ops.py +++ b/examples/multi_data_ops.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/parameters.py b/examples/parameters.py index 55627ec..4c4a21d 100644 --- a/examples/parameters.py +++ b/examples/parameters.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/rate_limiting.py b/examples/rate_limiting.py index ab04d64..f139da6 100644 --- a/examples/rate_limiting.py +++ b/examples/rate_limiting.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/single_data_ops.py b/examples/single_data_ops.py index fe1c276..8996b6d 100644 --- a/examples/single_data_ops.py +++ b/examples/single_data_ops.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/table_ops.py b/examples/table_ops.py index dd7e515..db7de82 100644 --- a/examples/table_ops.py +++ b/examples/table_ops.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/examples/utils.py b/examples/utils.py index 71b9496..08a14ef 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/__init__.py b/src/borneo/__init__.py index 3a695c0..8d32616 100644 --- a/src/borneo/__init__.py +++ b/src/borneo/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/auth.py b/src/borneo/auth.py index 31d9054..c271828 100644 --- a/src/borneo/auth.py +++ b/src/borneo/auth.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/client.py b/src/borneo/client.py index 57e7664..d2fd4e4 100644 --- a/src/borneo/client.py +++ b/src/borneo/client.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -22,6 +22,7 @@ OperationNotSupportedException, RequestSizeLimitException) from .http import RateLimiterMap, RequestUtils from .kv import StoreAccessTokenProvider +from .nson_protocol import LAST_WRITE_METADATA from .operations import ( GetTableRequest, QueryRequest, QueryResult, TableRequest, WriteRequest) from .query import QueryDriver @@ -35,6 +36,10 @@ class Client(object): LIMITER_REFRESH_NANOS = 600000000000 TRACE_LEVEL = 0 + # proxy enabled features flag bits, works on this._features + # Features: added in KV 26.1, SDK 5.4.4 + FEATURE_FLAG_LAST_WRITE_METADATA = 1 << 0 + # The HTTP driver client. def __init__(self, config, logger): self._logutils = LogUtils(logger) @@ -117,6 +122,9 @@ def __init__(self, config, logger): self._stats_control = StatsControl(config, logger, config.get_rate_limiting_enabled()) + # Keeps a set of bits each one corresponding to an enabled feature + # signaled by the httpproxy. See FEATURE_FLAG_LAST_WRITE_METADATA. + self._features = 0 @synchronized def background_update_limiters(self, table_name): @@ -178,6 +186,13 @@ def execute(self, request): CheckValue.check_not_none(request, 'request') request.set_defaults(self._config) request.validate() + + if (request.get_last_write_metadata() is not None) and \ + (not self.is_feature_enabled( + self.FEATURE_FLAG_LAST_WRITE_METADATA)): + raise OperationNotSupportedException('Last Write Metadata is not' + + 'supported on this server') + if request.is_query_request(): self._stats_control.observe_query(request) @@ -592,9 +607,35 @@ def get_kv_version(self): return self._kv_version def set_proxy_info(self, proxy_header): + """ + Format of the server version header string: + proxy=X.Y.Z kv=X.Y.Z[ features=XX] + + If "features" exists, its value is a long in hex. + """ if self._proxy_version is None and proxy_header is not None: versions = proxy_header.split() # bail if not of correct format - if len(versions) == 2: + if len(versions) >= 2: self._proxy_version = versions[0].split('=')[1] self._kv_version = versions[1].split('=')[1] + if (len(versions) >= 3 and + versions[2].split('=')[0] == 'features' and + len(versions[2].split('=')[1]) <= 16 ): + feat_str = versions[2].split('=')[1] + try: + self._features = int(feat_str, base=16) + except ValueError: + self._logutils.log_info( + f"Received invalid features flag from server: {feat_str}") + + def is_feature_enabled(self, feature_flag): + if self._proxy_version is None: + # there were no requests until now + request_utils = RequestUtils( + self._sess, self._logutils, None, self._retry_handler, + self, self._rate_limiter_map) + request_utils.do_head_request(self._request_uri, {}, + self._config.get_default_timeout()) + + return (self._features & feature_flag) != 0 diff --git a/src/borneo/common.py b/src/borneo/common.py index 38dc8e5..590a24f 100644 --- a/src/borneo/common.py +++ b/src/borneo/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -232,6 +232,14 @@ def check_str(data, name, allow_none=False): raise IllegalArgumentException( name + ' must be a string that is not empty.') + @staticmethod + def check_json_construct(data, name): + if not(data is None or isinstance(data, dict) or isinstance(data, list) + or isinstance(data, str) or isinstance(data, int) or + isinstance(data, float) or isinstance(data, Decimal) or + isinstance(data, bool)): + raise IllegalArgumentException(name + ' must be a jason construct.') + @staticmethod def is_digit(data): if (isinstance(data, int) or diff --git a/src/borneo/config.py b/src/borneo/config.py index 9f3cb21..78016e4 100644 --- a/src/borneo/config.py +++ b/src/borneo/config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/driver.py b/src/borneo/driver.py index 02829fb..d798153 100644 --- a/src/borneo/driver.py +++ b/src/borneo/driver.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/exception.py b/src/borneo/exception.py index 94329b1..e726c48 100644 --- a/src/borneo/exception.py +++ b/src/borneo/exception.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/http.py b/src/borneo/http.py index c1e23b8..40be27d 100644 --- a/src/borneo/http.py +++ b/src/borneo/http.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -172,6 +172,28 @@ def do_put_request(self, uri, headers, payload, timeout_ms): """ return self._do_request('PUT', uri, headers, payload, timeout_ms, None) + def do_head_request(self, uri, headers, timeout_ms): + """ + Issue HTTP HEAD request with retries and general error handling. + + It retries upon seeing following exceptions and response codes: + + HTTP response with status code larger than 500\n + Other throwable excluding RuntimeException, InterruptedException, + ExecutionException and TimeoutException + + :param uri: the request URI. + :type uri: str + :param headers: HTTP headers of this request. + :type headers: dict + :param timeout_ms: request timeout in milliseconds. + :type timeout_ms: int + :returns: HTTP response, a object encapsulate status code and response. + :rtype: HttpResponse or Result + """ + return self._do_request('HEAD', uri, headers, None, timeout_ms, None) + + def _do_request(self, method, uri, headers, payload, timeout_ms, stats_config): exception = None @@ -290,9 +312,10 @@ def _do_request(self, method, uri, headers, payload, timeout_ms, self._logutils.log_debug( 'Response: ' + self._request.__class__.__name__ + ', status: ' + str(response.status_code)) - if self._request is not None: + if self._client is not None: self._client.set_proxy_info( response.headers.get(HttpConstants.RESPONSE_PROXY_INFO)) + if self._request is not None: res = self._process_response( self._request, response.content, response.status_code) # set server's serial version if available diff --git a/src/borneo/iam/__init__.py b/src/borneo/iam/__init__.py index fe43c8c..1ab68ae 100644 --- a/src/borneo/iam/__init__.py +++ b/src/borneo/iam/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/iam/iam.py b/src/borneo/iam/iam.py index 3f25ef4..d549d7f 100644 --- a/src/borneo/iam/iam.py +++ b/src/borneo/iam/iam.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/kv/__init__.py b/src/borneo/kv/__init__.py index d63e837..404f788 100644 --- a/src/borneo/kv/__init__.py +++ b/src/borneo/kv/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/kv/exception.py b/src/borneo/kv/exception.py index 0f680cf..ac25aa6 100644 --- a/src/borneo/kv/exception.py +++ b/src/borneo/kv/exception.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/kv/kv.py b/src/borneo/kv/kv.py index 21b4e02..8feaba8 100644 --- a/src/borneo/kv/kv.py +++ b/src/borneo/kv/kv.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/nson.py b/src/borneo/nson.py index a620906..660501a 100644 --- a/src/borneo/nson.py +++ b/src/borneo/nson.py @@ -842,11 +842,13 @@ def deserialize(self, request, bis, serial_version): # "READ_KV" : # "WRITE_UNITS" : # } -# "ROW" : { # the row plus metadata -# "MODIFIED" : # last modified -# "EXPIRATION" : # expiration if using TTL -# "ROW_VERSION" : # kv version -# "VALUE" : { # the row's value in NSON +# "ROW" : { # the row plus metadata +# "CREATION_TIME" : # creation time +# "LAST_WRITE_METADATA" : # last write metadata +# "MODIFIED" : # last modified +# "EXPIRATION" : # expiration if using TTL +# "ROW_VERSION" : # kv version +# "VALUE" : { # the row's value in NSON # } # } # } @@ -989,6 +991,9 @@ def write_put_request(ns, request): if request.get_match_version() is not None: Proto.write_bin_map_field( ns, ROW_VERSION, request.get_match_version().get_bytes()) + if request.get_last_write_metadata() is not None: + Proto.write_string_map_field(ns, LAST_WRITE_METADATA, + json.dumps(request.get_last_write_metadata())) Proto.write_value(ns, request.get_value()) @@ -1067,6 +1072,9 @@ def write_delete_request(ns, request): if request.get_match_version() is not None: Proto.write_bin_map_field( ns, ROW_VERSION, request.get_match_version().get_bytes()) + if request.get_last_write_metadata() is not None: + Proto.write_string_map_field(ns, LAST_WRITE_METADATA, + json.dumps(request.get_last_write_metadata())) Proto.write_key(ns, request.get_key()) @@ -1530,6 +1538,9 @@ def serialize(self, request, bos, serial_version): Proto.write_bin_map_field(ns, CONTINUATION_KEY, request.get_continuation_key()) Proto.write_durability(ns, request) + if request.get_last_write_metadata() is not None: + Proto.write_string_map_field(ns, LAST_WRITE_METADATA, + json.dumps(request.get_last_write_metadata())) self._write_field_range(ns, request.get_range()) Proto.write_key(ns, request.get_key()) @@ -1892,6 +1903,9 @@ def serialize(self, request, bos, serial_version): request.get_query_name()) if request.get_virtual_scan() is not None: Proto.write_virtual_scan(ns, request.get_virtual_scan()) + if request.get_last_write_metadata() is not None: + Proto.write_string_map_field(ns, LAST_WRITE_METADATA, + json.dumps(request.get_last_write_metadata())) Proto.end_map(ns, PAYLOAD) @@ -2497,7 +2511,9 @@ def read_row(bis, result): while walker.has_next(): walker.next() name = walker.get_current_name() - if name == MODIFIED: + if name == CREATION_TIME: + result.set_creation_time(Nson.read_long(bis)) + elif name == MODIFIED: result.set_modification_time(Nson.read_long(bis)) elif name == EXPIRATION: result.set_expiration_time(Nson.read_long(bis)) @@ -2506,6 +2522,8 @@ def read_row(bis, result): Version.create_version(Nson.read_binary(bis))) elif name == VALUE: result.set_value(Proto.nson_to_value(bis)) + elif name == LAST_WRITE_METADATA: + result.set_last_write_metadata(json.loads(Nson.read_string(bis))) else: walker.skip() @@ -2588,13 +2606,17 @@ def read_return_info(bis, result): while walker.has_next(): walker.next() name = walker.get_current_name() - if name == EXISTING_MOD_TIME: + if name == CREATION_TIME: + result.set_existing_creation_time(Nson.read_long(bis)) + elif name == EXISTING_MOD_TIME: result.set_existing_modification_time(Nson.read_long(bis)) elif name == EXISTING_VERSION: result.set_existing_version(Version.create_version( Nson.read_binary(bis))) elif name == EXISTING_VALUE: result.set_existing_value(Proto.nson_to_value(bis)) + elif name == EXISTING_LAST_WRITE_METADATA: + result.set_existing_last_write_metadata(json.loads(Nson.read_string(bis))) else: walker.skip() diff --git a/src/borneo/nson_protocol.py b/src/borneo/nson_protocol.py index 00cd82c..30a5cfd 100644 --- a/src/borneo/nson_protocol.py +++ b/src/borneo/nson_protocol.py @@ -142,13 +142,16 @@ # # row metadata # +CREATION_TIME = 'ct' EXPIRATION = 'xp' +LAST_WRITE_METADATA = 'mt' MODIFIED = 'md' ROW = 'r' ROW_VERSION = 'rv' # # operation metadata # +EXISTING_LAST_WRITE_METADATA = 'ed' EXISTING_MOD_TIME = 'em' EXISTING_VALUE = 'el' EXISTING_VERSION = 'ev' @@ -164,6 +167,7 @@ NOT_TARGET_TABLES = 'nt' NUM_RESULTS = 'nr' PROXY_TOPO_SEQNUM = 'pn' +QUERY_NAME = 'qn' QUERY_OPERATION = 'qo' QUERY_PLAN_STRING = 'qs' QUERY_RESULTS = 'qr' diff --git a/src/borneo/operations.py b/src/borneo/operations.py index 576a26d..27d31f1 100644 --- a/src/borneo/operations.py +++ b/src/borneo/operations.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -439,6 +439,12 @@ def set_topo_seq_num(self, num): def get_topo_seq_num(self): return self._topo_seq_num + def get_last_write_metadata(self): + """ + Internal use only. + """ + return None + class WriteRequest(Request): """ @@ -447,7 +453,7 @@ class WriteRequest(Request): This class encapsulates the common parameters of table name and the return row boolean, which allows applications to get information about the existing - value of the target row on failure. By default no previous information is + value of the target row on failure. By default, no previous information is returned. """ @@ -455,6 +461,7 @@ def __init__(self): super(WriteRequest, self).__init__() self._return_row = False self._durability = None + self._last_write_metadata = None def __str__(self): return 'WriteRequest' @@ -499,6 +506,49 @@ def get_type_name(): # type: () -> str return "Write" + def get_last_write_metadata(self): + """ + Returns the last write metadata to be used for this request or None if + not set. + + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._last_write_metadata + + def set_last_write_metadata(self, last_write_metadata): + """ + Sets the write metadata to use for this request. + + This is an optional parameter. + + Last write metadata is associated to a certain version of a row. Any + subsequent write operation will use its own write metadata value. If not + specified null will be used by default. + NOTE that if you have previously written a record with metadata and a + subsequent write does not supply metadata, the metadata associated with + the row will be null. Therefore, if you wish to have metadata + associated with every write operation, you must supply a valid JSON + construct to this method. + + :param last_write_metadata: the write metadata, must be None or a valid + JSON construct: object, array, string, number, true, false or None, + otherwise an IllegalArgumentException is thrown. + :type last_write_metadata: int | float | str | bool | dict | list | None + :return: self + :raises IllegalArgumentException: if last_write_metadata not None nor + JSON construct + :versionadded:: 5.5.0 + """ + if last_write_metadata is None: + self._last_write_metadata = None + return self + + + CheckValue.check_json_construct(last_write_metadata, "last_write_metadata") + self._last_write_metadata = last_write_metadata + return self + class ReadRequest(Request): """ @@ -506,7 +556,7 @@ class ReadRequest(Request): :py:meth:`NoSQLHandle.get`. This class encapsulates the common parameters of table name and consistency. - By default read operations use Consistency.EVENTUAL. Use of + By default, read operations use Consistency.EVENTUAL. Use of Consistency.ABSOLUTE should be used only when required as it incurs additional cost. """ @@ -1469,6 +1519,7 @@ def __init__(self): self._range = None self._max_write_kb = 0 self._durability = None + self._last_write_metadata = None def __str__(self): return 'MultiDeleteRequest' @@ -1724,6 +1775,49 @@ def get_request_name(self): # type: () -> str return "MultiDelete" + def get_last_write_metadata(self): + """ + Returns the last write metadata to be used for this request or None if + not set. + + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._last_write_metadata + + def set_last_write_metadata(self, last_write_metadata): + """ + Sets the write metadata to use for this request. + + This is an optional parameter. + + Last write metadata is associated to a certain version of a row. Any + subsequent write operation will use its own write metadata value. If not + specified null will be used by default. + NOTE that if you have previously written a record with metadata and a + subsequent write does not supply metadata, the metadata associated with + the row will be null. Therefore, if you wish to have metadata + associated with every write operation, you must supply a valid JSON + construct to this method. + + :param last_write_metadata: the write metadata, must be None or a valid + JSON construct: object, array, string, number, true, false or None, + otherwise an IllegalArgumentException is thrown. + :type last_write_metadata: int | float | str | bool | dict | list | None + :return: self + :raises IllegalArgumentException: if last_write_metadata not None nor + JSON construct + :versionadded:: 5.5.0 + """ + if last_write_metadata is None: + self._last_write_metadata = None + return self + + + CheckValue.check_json_construct(last_write_metadata, "last_write_metadata") + self._last_write_metadata = last_write_metadata + return self + class PrepareRequest(Request): """ @@ -2462,6 +2556,7 @@ def __init__(self): self._driver_query_trace = None self._server_query_traces = None # dict if set self._batch_counter = 0 + self._last_write_metadata = None # dict, list, string, number, bool or None def __str__(self): return 'QueryRequest' @@ -3011,6 +3106,45 @@ def get_request_name(self): # type: () -> str return "Query" + def get_last_write_metadata(self): + """ + Returns the last write metadata to be used for this request or None if + not set. + + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._last_write_metadata + + def set_last_write_metadata(self, last_write_metadata): + """ + Sets the write metadata to use for the operation. This setting is + optional and only applies if the query modifies or deletes any rows + using an INSERT, UPDATE, UPSERT or DELETE statement. If the query is + read-only this setting is ignored. This is an optional parameter. + + Write metadata is associated to a certain version of a row. Any + subsequent write operation will use its own write metadata value. If not + specified null will be used by default. + NOTE that if you have previously written a record with metadata and a + subsequent write does not supply metadata, the metadata associated with + the row will be null. Therefore, if you wish to have metadata + associated with every write operation, you must supply a valid JSON + construct to this method. + + :param last_write_metadata: the write metadata, must be None or a valid + JSON construct: object, array, string, number, true, false or None, + otherwise an IllegalArgumentException is thrown. + :type last_write_metadata: int | float | str | bool | dict | list | None + :return: self + :raises IllegalArgumentException: if last_write_metadata not None nor + JSON construct + :versionadded:: 5.5.0 + """ + if last_write_metadata is None: + self._last_write_metadata = None + return self + class SystemRequest(Request): """ @@ -4729,7 +4863,9 @@ def __init__(self): super(WriteResult, self).__init__() self._existing_version = None self._existing_value = None + self._existing_creation_time = 0 self._existing_modification_time = 0 + self._existing_last_write_metadata = None def set_existing_value(self, existing_value): self._existing_value = existing_value @@ -4739,29 +4875,47 @@ def set_existing_version(self, existing_version): self._existing_version = existing_version return self + def set_existing_creation_time(self, existing_creation_time): + self._existing_creation_time = existing_creation_time + return self + def set_existing_modification_time(self, existing_modification_time): self._existing_modification_time = existing_modification_time return self + def set_existing_last_write_metadata(self, existing_last_write_metadata): + self._existing_last_write_metadata = existing_last_write_metadata + return self + def _get_existing_value(self): return self._existing_value def _get_existing_version(self): return self._existing_version + def _get_existing_creation_time(self): + return self._existing_creation_time + def _get_existing_modification_time(self): return self._existing_modification_time + def _get_existing_last_write_metadata(self): + return self._existing_last_write_metadata + + def _set_existing_last_write_metadata(self, value): + self._existing_last_write_metadata = value class DeleteResult(WriteResult): """ Represents the result of a :py:meth:`NoSQLHandle.delete` operation. - If the delete succeeded :py:meth:`get_success` returns True. Information - about the existing row on failure may be available using - :py:meth:`get_existing_value`, :py:meth:`get_existing_version` and - :py:meth:`get_existing_modification_time`, depending - on the use of :py:meth:`DeleteRequest.set_return_row`. + If the delete operation succeeded :py:meth:`get_success` returns True. + Information about the existing row on failure may be available using + :py:meth:`get_existing_value`, :py:meth:`get_existing_version`, + :py:meth:`get_existing_creation_time`, + :py:meth:`get_existing_modification_time` and + :py:meth:`get_existing_last_write_metadata`, depending on the use of + :py:meth:`DeleteRequest.set_return_row`. """ def __init__(self): @@ -4806,18 +4960,42 @@ def get_existing_version(self): """ return self._get_existing_version() + def get_existing_creation_time(self): + """ + Returns the existing row creation time if available. See + :py:meth:`DeleteRequest.set_return_row` for conditions under which + the information is available. + + :returns: the creation time in milliseconds since January 1, 1970, UTC + :rtype: int + :versionadded:: 5.5.0 + """ + return self._get_existing_creation_time() + def get_existing_modification_time(self): """ Returns the existing row modification time if available. See :py:meth:`DeleteRequest.set_return_row` for conditions under which the information is available. - :returns: the modification time in milliseconds since January 1, 1970 + :returns: the modification time in milliseconds since January 1, 1970, UTC :rtype: int :versionadded:: 5.3.0 """ return self._get_existing_modification_time() + def get_existing_last_write_metadata(self): + """ + Returns the existing last write metadata of the returned row if + available. See :py:meth:`DeleteRequest.set_return_row` for conditions + under which the information is available. + + :returns: the last write metadata. + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._get_existing_last_write_metadata() + def get_read_kb(self): """ Returns the read throughput consumed by this operation, in KBytes. This @@ -4875,7 +5053,9 @@ def __init__(self): self._value = None self._version = None self._expiration_time = 0 + self._creation_time = 0 self._modification_time = 0 + self._last_write_metadata = None def __str__(self): return 'None' if self._value is None else str(self._value) @@ -4921,12 +5101,28 @@ def get_expiration_time(self): row does not expire. This value is valid only if the operation successfully returned a row (:py:meth:`get_value` returns non-none). - :returns: the expiration time in milliseconds since January 1, 1970, or - zero if the row never expires or the row does not exist. + :returns: the expiration time in milliseconds since January 1, 1970, UTC, + or zero if the row never expires or the row does not exist. :rtype: int """ return self._expiration_time + def set_creation_time(self, creation_time): + # Sets the creation time, internal + self._creation_time = creation_time + return self + + def get_creation_time(self): + """ + Returns the creation time of the row. This value is valid only if + the operation successfully returned a row (:py:meth:`get_value` returns non-none). + + :returns: the creation time in milliseconds since January 1, 1970, UTC + :rtype: int + :versionadded: 5.5.0 + """ + return self._creation_time + def set_modification_time(self, modification_time): # Sets the modification time, internal self._modification_time = modification_time @@ -4937,8 +5133,9 @@ def get_modification_time(self): Returns the modification time of the row. This value is valid only if the operation successfully returned a row (:py:meth:`get_value` returns non-none). - :returns: the modification time in milliseconds since January 1, 1970 + :returns: the modification time in milliseconds since January 1, 1970, UTC :rtype: int + :versionadded: 5.5.0 """ return self._modification_time @@ -4983,6 +5180,40 @@ def get_write_units(self): """ return super(GetResult, self).get_write_units() + def get_last_write_metadata(self): + """ + Returns the last write metadata to be used for this request or None if + not set. + + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._last_write_metadata + + def set_last_write_metadata(self, last_write_metadata): + """ + Internal use only. + + Sets the write metadata of this object. + + :param last_write_metadata: the write metadata, must be None or a valid + JSON construct: object, array, string, number, true, false or None, + otherwise an IllegalArgumentException is thrown. + :type last_write_metadata: int | float | str | bool | dict | list | None + :return: self + :raises IllegalArgumentException: if last_write_metadata not None nor + JSON construct + :versionadded:: 5.5.0 + """ + if last_write_metadata is None: + self._last_write_metadata = None + return self + + + CheckValue.check_json_construct(last_write_metadata, "last_write_metadata") + self._last_write_metadata = last_write_metadata + return self + class GetIndexesResult(Result): """ @@ -5300,13 +5531,25 @@ def get_existing_value(self): """ return self._get_existing_value() + def get_existing_creation_time(self): + """ + Returns the existing row creation time if available. See + :py:meth:`PutRequest.set_return_row` for conditions under which + the information is available. + + :returns: the creation time in milliseconds since January 1, 1970 UTC + :rtype: int + :versionadded:: 5.5.0 + """ + return self._get_existing_creation_time() + def get_existing_modification_time(self): """ - Returns the existing row modification timeif available. See + Returns the existing row modification time if available. See :py:meth:`PutRequest.set_return_row` for conditions under which the information is available. - :returns: the modification time in milliseconds since January 1, 1970 + :returns: the modification time in milliseconds since January 1, 1970 UTC :rtype: int :versionadded:: 5.3.0 """ @@ -5353,6 +5596,18 @@ def get_write_units(self): """ return super(PutResult, self).get_write_units() + def get_existing_last_write_metadata(self): + """ + Returns the existing row last write metadata if available. See + :py:meth:`PutRequest.set_return_row` for conditions under which + the information is available. + + :returns: the last write metadata + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._get_existing_last_write_metadata() + class QueryResult(Result): """ @@ -6576,16 +6831,36 @@ def get_existing_value(self): """ return self._get_existing_value() + def get_existing_creation_time(self): + """ + Returns the existing row creation time if available. + + :returns: the creation time in milliseconds since January 1, 1970, UTC + :rtype: int + :versionadded:: 5.5.0 + """ + return self._get_existing_creation_time() + def get_existing_modification_time(self): """ Returns the existing row modification time if available. - :returns: the modification time in milliseconds since January 1, 1970 + :returns: the modification time in milliseconds since January 1, 1970, UTC :rtype: int :versionadded:: 5.3.0 """ return self._get_existing_modification_time() + def get_existing_last_write_metadata(self): + """ + Returns the existing row last write metadata if available or None + otherwise. + + :returns: the last write metadata + :rtype: int | float | str | bool | dict | list | None + :versionadded:: 5.5.0 + """ + return self._get_existing_last_write_metadata() class ReplicaStatsResult(Result): """ diff --git a/src/borneo/query.py b/src/borneo/query.py index 5ac96fc..6b9755d 100644 --- a/src/borneo/query.py +++ b/src/borneo/query.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/serde.py b/src/borneo/serde.py index 8c70234..b50e4dc 100644 --- a/src/borneo/serde.py +++ b/src/borneo/serde.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/serdeutil.py b/src/borneo/serdeutil.py index 565c681..bb001d0 100644 --- a/src/borneo/serdeutil.py +++ b/src/borneo/serdeutil.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -147,7 +147,7 @@ def stop(self): # noinspection PyTypeChecker class SerdeUtil(object): """ - A class to encapsulte static methods used by serialization and + A class to encapsulate static methods used by serialization and deserialization of requests. These utility methods can be used by multiple protocols. It also includes constants that are shared across protocols. diff --git a/src/borneo/stats.py b/src/borneo/stats.py index d269a47..fbaa1b6 100644 --- a/src/borneo/stats.py +++ b/src/borneo/stats.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/src/borneo/version.py b/src/borneo/version.py index 7181bcb..a2589d2 100644 --- a/src/borneo/version.py +++ b/src/borneo/version.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/config_cloudsim.py b/test/config_cloudsim.py index 8f8838c..ac1d3c7 100644 --- a/test/config_cloudsim.py +++ b/test/config_cloudsim.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/config_onprem.py b/test/config_onprem.py index 41a1895..35afe13 100644 --- a/test/config_onprem.py +++ b/test/config_onprem.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/delete.py b/test/delete.py index dd765d2..614ccf7 100644 --- a/test/delete.py +++ b/test/delete.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/field_range.py b/test/field_range.py index d540861..4fee1e3 100644 --- a/test/field_range.py +++ b/test/field_range.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/get.py b/test/get.py index feb45fc..3c0e88c 100644 --- a/test/get.py +++ b/test/get.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/get_indexes.py b/test/get_indexes.py index ec396fb..fb51325 100644 --- a/test/get_indexes.py +++ b/test/get_indexes.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/get_table.py b/test/get_table.py index a62a8b2..c41560b 100644 --- a/test/get_table.py +++ b/test/get_table.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/handle_config.py b/test/handle_config.py index 6c99fe1..839d2c7 100644 --- a/test/handle_config.py +++ b/test/handle_config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/list_tables.py b/test/list_tables.py index 8d82663..28de822 100644 --- a/test/list_tables.py +++ b/test/list_tables.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/multi_delete.py b/test/multi_delete.py index 7054f4e..b43158f 100644 --- a/test/multi_delete.py +++ b/test/multi_delete.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/parameters.py b/test/parameters.py index c011f46..ce37f90 100644 --- a/test/parameters.py +++ b/test/parameters.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/prepare.py b/test/prepare.py index a4b5e45..df23b48 100644 --- a/test/prepare.py +++ b/test/prepare.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/put.py b/test/put.py index 8bd0465..de011c8 100644 --- a/test/put.py +++ b/test/put.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -10,13 +10,17 @@ from collections import OrderedDict from copy import deepcopy from dateutil import tz + +from borneo.client import Client from parameters import table_prefix from time import time from borneo import ( - DeleteRequest, Durability, GetRequest, IllegalArgumentException, IllegalStateException, + DeleteRequest, Durability, GetRequest, IllegalArgumentException, + IllegalStateException, PutOption, PutRequest, RequestSizeLimitException, TableLimits, - TableNotFoundException, TableRequest, TimeToLive, TimeUnit) + TableNotFoundException, TableRequest, TimeToLive, TimeUnit, PutResult, + GetResult) from parameters import is_onprem, table_name, tenant_id, timeout from test_base import TestBase from testutils import get_row @@ -27,6 +31,10 @@ class TestPut(unittest.TestCase, TestBase): + def __init__(self, methodName='runTest'): + unittest.TestCase.__init__(self, methodName) + TestBase.__init__(self) + @classmethod def setUpClass(cls): cls.set_up_class() @@ -166,10 +174,13 @@ def testPutSetTtlAndUseTableDefaultTtl(self): def testPutGets(self): identity_cache_size = 5 version = self.handle.put(self.put_request).get_version() - self.put_request.set_option(PutOption.IF_ABSENT).set_match_version( - version).set_ttl(self.ttl).set_use_table_default_ttl( - True).set_exact_match(True).set_identity_cache_size( - identity_cache_size).set_return_row(True) + self.put_request.set_option(PutOption.IF_ABSENT)\ + .set_match_version(version)\ + .set_ttl(self.ttl)\ + .set_use_table_default_ttl(True)\ + .set_exact_match(True)\ + .set_identity_cache_size(identity_cache_size)\ + .set_return_row(True) self.assertEqual(self.put_request.get_value(), self.row) self.assertEqual(self.put_request.get_compartment(), tenant_id) self.assertEqual(self.put_request.get_option(), PutOption.IF_ABSENT) @@ -184,6 +195,82 @@ def testPutGets(self): identity_cache_size) self.assertTrue(self.put_request.get_return_row()) + + def testPutLastWriteMetadata(self): + last_write_meta = {'a':1, 'b':[0, "str", None, [], {}, True, False]} + self.put_request.set_return_row(True)\ + .set_last_write_metadata(last_write_meta) + + self.assertEqual(self.put_request.get_last_write_metadata(), last_write_meta) + + try: + result = self.handle.put(self.put_request) + if not (self.handle.get_client() + .is_feature_enabled( + Client.FEATURE_FLAG_LAST_WRITE_METADATA)): + self.fail("handle.put(last_write_metadata) should have failed") + + else: + self.assertIsNotNone(result) + self.assertEqual(result.get_existing_last_write_metadata(), None) + + result = self.handle.get(self.get_request) + self.assertIsNotNone(result) + self.assertEqual(result.get_last_write_metadata(), last_write_meta) + + last_write_meta2 = {'abc': 123} + self.put_request.set_return_row(True) \ + .set_last_write_metadata(last_write_meta2) + result = self.handle.put(self.put_request) + + self.assertIsNotNone(result) + self.assertEqual(result.get_existing_last_write_metadata(), + last_write_meta) + + result = self.handle.get(self.get_request) + self.assertIsNotNone(result) + self.assertEqual(result.get_last_write_metadata(), last_write_meta2) + + except IllegalArgumentException: + if not (self.handle.get_client() + .is_feature_enabled( + Client.FEATURE_FLAG_LAST_WRITE_METADATA)): + # feature was not enabled so the put should have thrown + self.assertTrue(True) + else: + # feature was not enabled so the put should have NOT thrown + self.assertTrue(False) + + + def testPutCreationTime(self): + self.put_request.set_return_row(True) + + creation_time = time() * 1000 + + result = self.handle.put(self.put_request) + self.assertIsNotNone(result) + + # check creation time, since no flag exists it can be 0 or near now + self.assertTrue(result.get_existing_creation_time() == 0 or + result.get_existing_creation_time() - creation_time < 1000) + + result = self.handle.get(self.get_request) + self.assertIsNotNone(result) + + self.put_request.set_return_row(True) + result = self.handle.put(self.put_request) + + self.assertIsNotNone(result) + + result = self.handle.get(self.get_request) + self.assertIsNotNone(result) + + # check creation time, since no flag exists it can be 0 or near now + db_creation_time = result.get_creation_time() + self.assertTrue(db_creation_time == 0 or + (db_creation_time - creation_time) < 1000) + + def testPutIllegalRequest(self): self.assertRaises(IllegalArgumentException, self.handle.put, 'IllegalRequest') diff --git a/test/query.py b/test/query.py index b9da386..de94189 100644 --- a/test/query.py +++ b/test/query.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/rate_limiting.py b/test/rate_limiting.py index c4ec792..b381bd9 100644 --- a/test/rate_limiting.py +++ b/test/rate_limiting.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/signature_provider.py b/test/signature_provider.py index f56b92d..f66c6e1 100644 --- a/test/signature_provider.py +++ b/test/signature_provider.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/stats.py b/test/stats.py index 2c1c92f..85304bc 100644 --- a/test/stats.py +++ b/test/stats.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/store_at_provider.py b/test/store_at_provider.py index 2bf2847..f2355b0 100644 --- a/test/store_at_provider.py +++ b/test/store_at_provider.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/system_request.py b/test/system_request.py index e88213d..bbc73fd 100644 --- a/test/system_request.py +++ b/test/system_request.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/system_status.py b/test/system_status.py index 8f3de09..3579288 100644 --- a/test/system_status.py +++ b/test/system_status.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/table_limits.py b/test/table_limits.py index f5a374d..219b577 100644 --- a/test/table_limits.py +++ b/test/table_limits.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/table_request.py b/test/table_request.py index f930719..d67f717 100644 --- a/test/table_request.py +++ b/test/table_request.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/table_usage.py b/test/table_usage.py index 65c2737..ed18edf 100644 --- a/test/table_usage.py +++ b/test/table_usage.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/test_base.py b/test/test_base.py index 641e968..6548b1b 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ @@ -29,6 +29,8 @@ class TestBase(object): def __init__(self): self.handle = None + self.feature_creation_time_is_enabled = True + self.feature_last_write_metadata_is_enabled = True def check_cost(self, result, read_kb, read_units, write_kb, write_units, advance=False, multi_shards=False): @@ -58,7 +60,7 @@ def check_cost(self, result, read_kb, read_units, write_kb, write_units, def check_get_result(self, result, value=None, exp_version=None, expect_expiration=0, timeunit=None, ver_eq=True, - mod_time_recent=False): + mod_time_recent=False, serial_version=0): assert isinstance(self, TestCase) # check value self.assertEqual(result.get_value(), value) @@ -70,6 +72,7 @@ def check_get_result(self, result, value=None, exp_version=None, self.assertEqual(ver.get_bytes(), exp_version.get_bytes()) else: self.assertNotEqual(ver.get_bytes(), exp_version.get_bytes()) + # check expiration time if expect_expiration == 0: self.assertEqual(result.get_expiration_time(), 0) @@ -81,6 +84,8 @@ def check_get_result(self, result, value=None, exp_version=None, self.assertLess(actual_expect_diff, TestBase.HOUR_IN_MILLIS) else: self.assertLess(actual_expect_diff, TestBase.DAY_IN_MILLIS) + + # check modification time modtime = result.get_modification_time() if mod_time_recent: now = round(time() * 1000) @@ -224,4 +229,4 @@ def table_request(cls, request, test_handle=None): # if is_pod(): sleep(30) - test_handle.do_table_request(request, wait_timeout, 1000) + test_handle.do_table_request(request, wait_timeout, 1000) \ No newline at end of file diff --git a/test/testutils.py b/test/testutils.py index 738e960..46c2c33 100644 --- a/test/testutils.py +++ b/test/testutils.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/ttl.py b/test/ttl.py index 6a1d013..b6fd33f 100644 --- a/test/ttl.py +++ b/test/ttl.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/ diff --git a/test/write_multiple.py b/test/write_multiple.py index 2dd1695..4b14971 100644 --- a/test/write_multiple.py +++ b/test/write_multiple.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2025 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl/