Repository: bureau14/qdb-api-python
Pull request #86: QDB-16270 - Deprecate push modes in qdbnp.write_dataframes
Author: login: rodp63
Created: 2025-04-01T21:11:36Z | Merged: 2025-04-04T06:32:23Z



Diff:
diff --git a/.gitignore b/.gitignore
index 9cd39a31..e574aa1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -112,6 +112,7 @@ celerybeat-schedule
 # Environments
 .env
 .venv
+venv
 
 # Spyder project settings
 .spyderproject
diff --git a/quasardb/numpy/__init__.py b/quasardb/numpy/__init__.py
index 29aa9ec0..cab194cc 100644
--- a/quasardb/numpy/__init__.py
+++ b/quasardb/numpy/__init__.py
@@ -29,6 +29,7 @@
 
 import logging
 import time
+import warnings
 
 import quasardb
 import quasardb.table_cache as table_cache
@@ -482,6 +483,33 @@ def _coerce_retries(retries) -> quasardb.RetryOptions:
         )
 
 
+def _kwarg_deprecation_warning(
+    old_kwarg, old_value, new_kwargs, new_values, stacklevel
+):
+    new_declaration = ", ".join(
+        f"{new_kwarg}={new_value}"
+        for new_kwarg, new_value in zip(new_kwargs, new_values)
+    )
+    warnings.warn(
+        f"The argument '{old_kwarg}' <{type(old_value).__name__}> is deprecated and will be removed in a future version. "
+        f"Please use '{new_declaration}' instead.",
+        DeprecationWarning,
+        stacklevel=stacklevel + 1,
+    )
+
+
+def _type_check(var, var_name, target_type, raise_error=True, allow_none=True):
+    if allow_none and var is None:
+        return True
+    if not isinstance(var, target_type):
+        if raise_error:
+            raise quasardb.quasardb.InvalidArgumentError(
+                f"Invalid '{var_name}' type, expected: {target_type}, got: {type(var)}"
+            )
+        return False
+    return True
+
+
 def ensure_ma(xs, dtype=None):
     if isinstance(dtype, list):
         assert isinstance(xs, list) == True
@@ -630,9 +658,12 @@ def write_arrays(
     *,
     dtype=None,
     index=None,
+    # TODO: Set the default push_mode after removing _async, fast and truncate
+    push_mode=None,
     _async=False,
     fast=False,
     truncate=False,
+    truncate_range=None,
     deduplicate=False,
     deduplication_mode="drop",
     infer_types=True,
@@ -705,13 +736,32 @@ def write_arrays(
       Defaults to True. For production use cases where you want to avoid implicit conversions,
       we recommend setting this to False.
 
+    push_mode: optional quasardb.WriterPushMode
+      The mode used for inserting data. Can be either a string or a `WriterPushMode` enumeration item.
+      Available options:
+      * `Truncate`: Truncate (also referred to as upsert) the data in-place. Will detect time range
+        to truncate from the time range inside the dataframe.
+      * `Async`: Uses asynchronous insertion API where commits are buffered server-side and
+        acknowledged before they are written to disk. If you insert to the same table from
+        multiple processes, setting this to True may improve performance.
+      * `Fast`: Whether to use 'fast push'. If you incrementally add small batches of data to table,
+        you may see better performance if you set this to True.
+      * `Transactional`: Ensures full transactional consistency.
+
+      Defaults to `Transactional`.
+
     truncate: optional bool
+      **DEPRECATED** – Use `push_mode=WriterPushMode.Truncate` instead.
       Truncate (also referred to as upsert) the data in-place. Will detect time range to truncate
       from the time range inside the dataframe.
 
       Defaults to False.
 
+    truncate_range: optional tuple
+      Time range to truncate from the time range inside the dataframe.
+
     _async: optional bool
+      **DEPRECATED** – Use `push_mode=WriterPushMode.Async` instead.
       If true, uses asynchronous insertion API where commits are buffered server-side and
       acknowledged before they are written to disk. If you insert to the same table from
       multiple processes, setting this to True may improve performance.
@@ -719,6 +769,7 @@ def write_arrays(
       Defaults to False.
 
     fast: optional bool
+      **DEPRECATED** – Use `push_mode=WriterPushMode.Fast` instead.
       Whether to use 'fast push'. If you incrementally add small batches of data to table,
       you may see better performance if you set this to True.
 
@@ -747,32 +798,57 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _type_check(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+    deprecation_stacklevel = kwargs.pop("deprecation_stacklevel", 1) + 1
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        _kwarg_deprecation_warning(
+            "truncate",
+            truncate,
+            ["push_mode", "truncate_range"],
+            [quasardb.WriterPushMode.Truncate, truncate],
+            deprecation_stacklevel,
         )
+        truncate_range = truncate_range or truncate
+        truncate = True
+
+    kwarg_to_mode = {
+        # "kwarg": (kwarg_type, kwarg_push_mode, is_deprecated)
+        "fast": (bool, quasardb.WriterPushMode.Fast, True),
+        "_async": (bool, quasardb.WriterPushMode.Async, True),
+        "truncate": (bool, quasardb.WriterPushMode.Truncate, True),
+        "truncate_range": (tuple, quasardb.WriterPushMode.Truncate, False),
+    }
 
-    ret = []
+    for kwarg, info in kwarg_to_mode.items():
+        expected_type, mode, deprecated = info
+        kwarg_value = locals()[kwarg]
+        _type_check(kwarg_value, kwarg, target_type=expected_type)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}' in kwargs, but push mode is already set to {push_mode}"
+                )
+            push_mode = mode
+            if deprecated:
+                _kwarg_deprecation_warning(
+                    kwarg, kwarg_value, ["push_mode"], [mode], deprecation_stacklevel
+                )
+
+    if not push_mode:
+        push_mode = quasardb.WriterPushMode.Transactional
 
     # Create batch column info from dataframe
     if writer is None:
         writer = cluster.writer()
 
+    ret = []
     n_rows = 0
-
     push_data = quasardb.WriterData()
 
     for table, data_ in data:
@@ -837,22 +913,13 @@ def write_arrays(
     push_kwargs["deduplication_mode"] = deduplication_mode
     push_kwargs["write_through"] = write_through
     push_kwargs["retries"] = retries
+    push_kwargs["push_mode"] = push_mode
+    if truncate_range:
+        push_kwargs["range"] = truncate_range
 
     logger.debug("pushing %d rows", n_rows)
     start = time.time()
 
-    if fast is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Fast
-    elif truncate is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-    elif isinstance(truncate, tuple):
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-        push_kwargs["range"] = truncate
-    elif _async is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Async
-    else:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Transactional
-
     writer.push(push_data, **push_kwargs)
 
     logger.debug("pushed %d rows in %s seconds", n_rows, (time.time() - start))
diff --git a/quasardb/pandas/__init__.py b/quasardb/pandas/__init__.py
index 624c1890..a48f552e 100644
--- a/quasardb/pandas/__init__.py
+++ b/quasardb/pandas/__init__.py
@@ -419,6 +419,7 @@ def write_dataframes(dfs, cluster, *, create=False, shard_size=None, **kwargs):
 
         data_by_table.append((table, data))
 
+    kwargs["deprecation_stacklevel"] = kwargs.get("deprecation_stacklevel", 1) + 1
     return qdbnp.write_arrays(data_by_table, cluster, table=None, index=None, **kwargs)
 
 
@@ -427,6 +428,7 @@ def write_dataframe(df, cluster, table, **kwargs):
     Store a single dataframe into a table. Takes the same arguments as `write_dataframes`, except only
     a single df/table combination.
     """
+    kwargs["deprecation_stacklevel"] = kwargs.get("deprecation_stacklevel", 1) + 1
     write_dataframes([(table, df)], cluster, **kwargs)
 
 
@@ -438,6 +440,7 @@ def write_pinned_dataframe(*args, **kwargs):
         "write_pinned_dataframe is deprecated and will be removed in a future release."
     )
     logger.warn("Please use write_dataframe directly instead")
+    kwargs["deprecation_stacklevel"] = 2
     return write_dataframe(*args, **kwargs)
 
 
diff --git a/tests/test_numpy.py b/tests/test_numpy.py
index 85555109..c3c2f761 100644
--- a/tests/test_numpy.py
+++ b/tests/test_numpy.py
@@ -89,7 +89,7 @@ def test_arrays_read_write_native_dtypes(array_with_index_and_table, qdbd_connec
         index=index,
         dtype=dtype,
         infer_types=False,
-        truncate=True,
+        push_mode=quasardb.WriterPushMode.Truncate,
     )
 
     res = qdbnp.read_array(table, col)
@@ -133,7 +133,7 @@ def test_arrays_read_write_data_as_dict(array_with_index_and_table, qdbd_connect
         index=index,
         dtype=dtype,
         infer_types=False,
-        truncate=True,
+        push_mode=quasardb.WriterPushMode.Truncate,
     )
 
     res = qdbnp.read_array(table, col)
diff --git a/tests/test_pandas.py b/tests/test_pandas.py
index 29018062..f9a2b0d2 100644
--- a/tests/test_pandas.py
+++ b/tests/test_pandas.py
@@ -211,7 +211,12 @@ def test_multiple_dataframe(
         input_dfs.append(df)
         tables.append(table)
 
-    qdbpd_writes_fn(payload, qdbd_connection, infer_types=True, fast=True)
+    qdbpd_writes_fn(
+        payload,
+        qdbd_connection,
+        infer_types=True,
+        push_mode=quasardb.WriterPushMode.Fast,
+    )
 
     xs = list(
         qdbpd.stream_dataframes(qdbd_connection, tables, batch_size=reader_batch_size)
@@ -293,7 +298,7 @@ def test_write_dataframe_push_fast(qdbpd_write_fn, qdbd_connection, df_with_tabl
     (_, _, df1, table) = df_with_table
 
     # Ensures that we can do a full-circle write and read of a dataframe
-    qdbpd_write_fn(df1, qdbd_connection, table, fast=True)
+    qdbpd_write_fn(df1, qdbd_connection, table, push_mode=quasardb.WriterPushMode.Fast)
 
     df2 = qdbpd.read_dataframe(qdbd_connection, table)
 
@@ -304,8 +309,12 @@ def test_write_dataframe_push_truncate(qdbpd_write_fn, qdbd_connection, df_with_
     (_, _, df1, table) = df_with_table
 
     # Ensures that we can do a full-circle write and read of a dataframe
-    qdbpd_write_fn(df1, qdbd_connection, table, truncate=True)
-    qdbpd_write_fn(df1, qdbd_connection, table, truncate=True)
+    qdbpd_write_fn(
+        df1, qdbd_connection, table, push_mode=quasardb.WriterPushMode.Truncate
+    )
+    qdbpd_write_fn(
+        df1, qdbd_connection, table, push_mode=quasardb.WriterPushMode.Truncate
+    )
 
     df2 = qdbpd.read_dataframe(qdbd_connection, table)
 
@@ -523,7 +532,13 @@ def test_regression_sc11337(
 
     df1[column_name][4] = pd.NA
 
-    qdbpd_write_fn(df1, qdbd_connection, table.get_name(), fast=True, infer_types=True)
+    qdbpd_write_fn(
+        df1,
+        qdbd_connection,
+        table.get_name(),
+        push_mode=quasardb.WriterPushMode.Fast,
+        infer_types=True,
+    )
 
     df2 = qdbpd.read_dataframe(qdbd_connection, table, column_names=[column_name])
 
diff --git a/tests/test_pandas_benchmark.py b/tests/test_pandas_benchmark.py
index 05e8f9dd..a91d80c9 100644
--- a/tests/test_pandas_benchmark.py
+++ b/tests/test_pandas_benchmark.py
@@ -123,7 +123,11 @@ def test_benchmark_dataframe_query(
     # Ensures that we can do a full-circle write and read of a dataframe
     df = pandaslib.gen_df(np.datetime64("2017-01-01"), row_count, unit="s")
     qdbpd.write_pinned_dataframe(
-        df, qdbd_connection, table, fast=True, infer_types=False
+        df,
+        qdbd_connection,
+        table,
+        push_mode=quasardb.WriterPushMode.Fast,
+        infer_types=False,
     )
 
     q = 'SELECT "{}" FROM "{}"'.format(column_name, table.get_name())
diff --git a/tests/test_properties.py b/tests/test_properties.py
index 18ca4cd4..f9d6d459 100644
--- a/tests/test_properties.py
+++ b/tests/test_properties.py
@@ -124,7 +124,7 @@ def test_properties_in_log(
     # XXX(leon): do we even need to write this at all? It doesn't appear that the test
     # even relies on this data being present?
     (_, _, df, table) = df_with_table
-    qdbpd_write_fn(df, qdbd_connection, table, fast=True)
+    qdbpd_write_fn(df, qdbd_connection, table, push_mode=quasardb.WriterPushMode.Fast)
 
     qdbd_connection.properties().clear()
     qdbd_connection.properties().put(random_identifier, random_string)


Reviews:
user: login: solatis
body: Two major feedbacks:
 * Don't use another "string" representation for the push mode, just use the actual enum types directly;
 * I don't think the `from_string` function is necessary anymore after we do this, this should simplify the python code;
 * Instead of a whole bunch of if/elif statements, use a lookup map `kw_to_push_mode`, iterate over that, and (potentially) use recursion.

I typically like recursion for situations like this, but the function argument parsing may need some refactoring for that to work nicely here without too much copy/paste.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T05:49:49Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T14:23:35Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T18:46:19Z
user: login: solatis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T04:26:42Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T12:16:08Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T12:20:07Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T12:26:48Z
user: login: solatis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T12:35:31Z
user: login: solatis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T12:36:01Z
user: login: rodp63
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T12:45:35Z
user: login: solatis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T06:32:10Z

Review comments:
diff_hunk: @@ -480,6 +482,23 @@ struct batch_push_mode
             return "invalid";
         };
     }
+    static qdb_exp_batch_push_mode_t from_string(std::string push_mode)
+    {
+	std::transform(push_mode.begin(), push_mode.end(), push_mode.begin(),
+		       [](unsigned char c){ return std::tolower(c); });
+
+	if (push_mode == "transactional") {
+	    return qdb_exp_batch_push_transactional;
+	} else if (push_mode == "fast") {
+	    return qdb_exp_batch_push_fast;
+	} else if (push_mode == "async") {
+	    return qdb_exp_batch_push_async;
+	} else if (push_mode == "truncate") {
+	    return qdb_exp_batch_push_truncate;
+	} else {
+	    throw qdb::invalid_argument_exception{"Invalid push mode"};
+	}
+    }
 };
path: quasardb/detail/writer.hpp
user: login: solatis
body: Is this executed a lot? This is not fast code, as a matter of fact, it's super slow. Just something to be aware of.
created_at: 2025-04-02T05:37:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -705,20 +708,35 @@ def write_arrays(
       Defaults to True. For production use cases where you want to avoid implicit conversions,
       we recommend setting this to False.
 
+    push_mode: optional str or quasardb.WriterPushMode
+      The mode used for inserting data. Can be either a string or a `WriterPushMode` enumeration item.
+      Available options:
+      * `'truncate'`: Truncate (also referred to as upsert) the data in-place. Will detect time range
+        to truncate from the time range inside the dataframe.
+      * `'async'`: Uses asynchronous insertion API where commits are buffered server-side and
+        acknowledged before they are written to disk. If you insert to the same table from
+        multiple processes, setting this to True may improve performance.
+      * `'fast'`: Whether to use 'fast push'. If you incrementally add small batches of data to table,
+        you may see better performance if you set this to True.
+      * `'transactional'`: Ensures full transactional consistency.
path: quasardb/numpy/__init__.py
user: login: solatis
body: Why not directly use the `quasardb.WriterPushMode.Truncate` etc enum and make use of pybind11's automatic conversion? Why use a string here when we have this enum type already?
created_at: 2025-04-02T05:38:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -838,20 +859,67 @@ def write_arrays(
     push_kwargs["write_through"] = write_through
     push_kwargs["retries"] = retries
 
-    logger.debug("pushing %d rows", n_rows)
-    start = time.time()
+    def deprecation_warning(kwarg, mode):
+        warnings.warn(
+            f"The argument '{kwarg}' is deprecated and will be removed in a future version. "
+            f"Please use push_mode='{mode}' instead.",
+            DeprecationWarning,
+            stacklevel=deprecation_stacklevel + 1,
+        )
 
-    if fast is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Fast
-    elif truncate is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-    elif isinstance(truncate, tuple):
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-        push_kwargs["range"] = truncate
-    elif _async is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Async
+    if push_mode:
+        if isinstance(push_mode, str):
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.from_string(push_mode)
+        elif isinstance(push_mode, quasardb.WriterPushMode):
+            push_kwargs["push_mode"] = push_mode
+        else:
+            raise quasardb.quasardb.InvalidArgumentError(
+                f"Invalid 'push_mode' type, "
+                f"expected: {str} or {quasardb.WriterPushMode}, got: {type(push_mode)}"
+            )
+        if push_kwargs["push_mode"] == quasardb.WriterPushMode.Truncate and isinstance(
+            truncate, tuple
+        ):
+            push_kwargs["range"] = truncate
     else:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Transactional
+        if sum(1 for it in [fast, truncate, _async] if it) > 1:
+            raise quasardb.quasardb.InvalidArgumentError(
+                "The arguments 'fast', '_async', and 'truncate' are mutually exclusive. Please set only one."
+            )
+
+        if fast is True:
+            deprecation_warning("fast", "fast")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Fast
+
+        elif truncate is True:
+            deprecation_warning("truncate", "truncate")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
+
+        elif isinstance(truncate, tuple):
+            warnings.warn(
+                "Using 'truncate' as a tuple to define the push mode is deprecated."
+                "Please explicitly set push_mode='truncate'.",
+                DeprecationWarning,
+                stacklevel=deprecation_stacklevel,
+            )
+            push_kwargs["range"] = truncate
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
+
+        elif _async is True:
+            deprecation_warning("_async", "async")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Async
path: quasardb/numpy/__init__.py
user: login: solatis
body: This code can be done much more elegantly by using recursion.

Get rid of the check for `sum()` at the top.

I would then do this:

```py
kwarg_to_push_mode = {'_async': quasardb.WriterPushMode.Async,
                      'fast': quasardb.WriterPushMode.Fast,
                      ...}
                                             
for k,v in kwarg_to_push_mode.items():
   if k in kwargs:
      if push_mode is not None:
          raise InvalidArgumentError("found '{}' in keywords, but push mode is already set to: {}".format(k, push_mode)
       kwargs['push_mode'] = v
       return         return write_arrays(data, cluster, table=table,
            dtype=dtype,
            index=index,
            _async=_async,
            fast=fast,
            truncate=truncate,
            deduplicate=deduplicate,
            deduplication_mode=deduplication_mode,
            infer_types=infer_types,
            write_through=write_through,
            writer=writer,
            retries=retries,
            **kwargs,
        )
```

The recursion can probably be handled more elegantly, but I'm seeing too many if-else statements here that can be replaced by a simple lookup map/dict + recursion. I
created_at: 2025-04-02T05:45:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -328,7 +328,8 @@ static void register_writer(py::module_ & m)
         .value("Transactional", qdb_exp_batch_push_transactional)                            //
         .value("Fast", qdb_exp_batch_push_fast)                                              //
         .value("Truncate", qdb_exp_batch_push_truncate)                                      //
-        .value("Async", qdb_exp_batch_push_async);                                           //
+        .value("Async", qdb_exp_batch_push_async)                                            //
+        .def_static("from_string", &detail::batch_push_mode::from_string);                   //
path: quasardb/writer.hpp
user: login: solatis
body: I don't like this, exposing this static c++ function like this, it's too and more of an internal C++ utility function. I don't think this function is that useful if we just use the enum type we already have.
created_at: 2025-04-02T05:47:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -480,6 +482,23 @@ struct batch_push_mode
             return "invalid";
         };
     }
+    static qdb_exp_batch_push_mode_t from_string(std::string push_mode)
+    {
+	std::transform(push_mode.begin(), push_mode.end(), push_mode.begin(),
+		       [](unsigned char c){ return std::tolower(c); });
+
+	if (push_mode == "transactional") {
+	    return qdb_exp_batch_push_transactional;
+	} else if (push_mode == "fast") {
+	    return qdb_exp_batch_push_fast;
+	} else if (push_mode == "async") {
+	    return qdb_exp_batch_push_async;
+	} else if (push_mode == "truncate") {
+	    return qdb_exp_batch_push_truncate;
+	} else {
+	    throw qdb::invalid_argument_exception{"Invalid push mode"};
+	}
+    }
 };
path: quasardb/detail/writer.hpp
user: login: rodp63
body: I agree on not using string conversion, let's use directly the enumeration
created_at: 2025-04-02T14:23:35Z
author_association: MEMBER
diff_hunk: @@ -838,20 +859,67 @@ def write_arrays(
     push_kwargs["write_through"] = write_through
     push_kwargs["retries"] = retries
 
-    logger.debug("pushing %d rows", n_rows)
-    start = time.time()
+    def deprecation_warning(kwarg, mode):
+        warnings.warn(
+            f"The argument '{kwarg}' is deprecated and will be removed in a future version. "
+            f"Please use push_mode='{mode}' instead.",
+            DeprecationWarning,
+            stacklevel=deprecation_stacklevel + 1,
+        )
 
-    if fast is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Fast
-    elif truncate is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-    elif isinstance(truncate, tuple):
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
-        push_kwargs["range"] = truncate
-    elif _async is True:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Async
+    if push_mode:
+        if isinstance(push_mode, str):
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.from_string(push_mode)
+        elif isinstance(push_mode, quasardb.WriterPushMode):
+            push_kwargs["push_mode"] = push_mode
+        else:
+            raise quasardb.quasardb.InvalidArgumentError(
+                f"Invalid 'push_mode' type, "
+                f"expected: {str} or {quasardb.WriterPushMode}, got: {type(push_mode)}"
+            )
+        if push_kwargs["push_mode"] == quasardb.WriterPushMode.Truncate and isinstance(
+            truncate, tuple
+        ):
+            push_kwargs["range"] = truncate
     else:
-        push_kwargs["push_mode"] = quasardb.WriterPushMode.Transactional
+        if sum(1 for it in [fast, truncate, _async] if it) > 1:
+            raise quasardb.quasardb.InvalidArgumentError(
+                "The arguments 'fast', '_async', and 'truncate' are mutually exclusive. Please set only one."
+            )
+
+        if fast is True:
+            deprecation_warning("fast", "fast")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Fast
+
+        elif truncate is True:
+            deprecation_warning("truncate", "truncate")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
+
+        elif isinstance(truncate, tuple):
+            warnings.warn(
+                "Using 'truncate' as a tuple to define the push mode is deprecated."
+                "Please explicitly set push_mode='truncate'.",
+                DeprecationWarning,
+                stacklevel=deprecation_stacklevel,
+            )
+            push_kwargs["range"] = truncate
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Truncate
+
+        elif _async is True:
+            deprecation_warning("_async", "async")
+            push_kwargs["push_mode"] = quasardb.WriterPushMode.Async
path: quasardb/numpy/__init__.py
user: login: rodp63
body: Now we're using a lookup rather than direct `if / else` statements. All the cases are handled, specially the `truncate` one, since it allowed the pass of a truncation range.
created_at: 2025-04-02T18:46:19Z
author_association: MEMBER
diff_hunk: @@ -705,20 +733,40 @@ def write_arrays(
       Defaults to True. For production use cases where you want to avoid implicit conversions,
       we recommend setting this to False.
 
+    push_mode: optional quasardb.WriterPushMode
+      The mode used for inserting data. Can be either a string or a `WriterPushMode` enumeration item.
+      Available options:
+      * `Truncate`: Truncate (also referred to as upsert) the data in-place. Will detect time range
+        to truncate from the time range inside the dataframe.
+      * `Async`: Uses asynchronous insertion API where commits are buffered server-side and
+        acknowledged before they are written to disk. If you insert to the same table from
+        multiple processes, setting this to True may improve performance.
+      * `Fast`: Whether to use 'fast push'. If you incrementally add small batches of data to table,
+        you may see better performance if you set this to True.
+      * `Transactional`: Ensures full transactional consistency.
+
+      Defaults to `Transactional`.
path: quasardb/numpy/__init__.py
user: login: solatis
body: I would make this explicit by setting push_mode=quasardb.WriterPushMode.Transactional in the function declaration
created_at: 2025-04-03T04:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
path: quasardb/numpy/__init__.py
user: login: solatis
body: I don't understand the need for this? Wouldn't a check for `if push_mode` and updating the push_mode instead be more concise? 

I don't understand exactly what case this covers...
created_at: 2025-04-03T04:26:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
+            selected_push_mode, selected_kwarg = mode, kwarg
+            _kwarg_deprecation_warning(kwarg, ["push_mode"], [mode], 2)
+
+    if not push_mode:
+        push_mode = selected_push_mode or quasardb.WriterPushMode.Transactional
 
     # Create batch column info from dataframe
     if writer is None:
         writer = cluster.writer()
 
+    ret = []
path: quasardb/numpy/__init__.py
user: login: solatis
body: What is this doing here?
created_at: 2025-04-03T04:26:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -705,20 +733,40 @@ def write_arrays(
       Defaults to True. For production use cases where you want to avoid implicit conversions,
       we recommend setting this to False.
 
+    push_mode: optional quasardb.WriterPushMode
+      The mode used for inserting data. Can be either a string or a `WriterPushMode` enumeration item.
+      Available options:
+      * `Truncate`: Truncate (also referred to as upsert) the data in-place. Will detect time range
+        to truncate from the time range inside the dataframe.
+      * `Async`: Uses asynchronous insertion API where commits are buffered server-side and
+        acknowledged before they are written to disk. If you insert to the same table from
+        multiple processes, setting this to True may improve performance.
+      * `Fast`: Whether to use 'fast push'. If you incrementally add small batches of data to table,
+        you may see better performance if you set this to True.
+      * `Transactional`: Ensures full transactional consistency.
+
+      Defaults to `Transactional`.
path: quasardb/numpy/__init__.py
user: login: rodp63
body: If we set the value of `push_mode`, then we won't be able to use the `_async`, `fast` and `truncate` kwargs, since we prioritize `push_mode` over the others.
However, once we remove the support for `_async`, `fast` and `truncate`, we should set this default value. I can add a `TODO` comment to not forget this.
created_at: 2025-04-03T12:16:08Z
author_association: MEMBER
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
path: quasardb/numpy/__init__.py
user: login: rodp63
body: The covered case is when we set 2 deprecated kwargs (e.g. `fast` and `_async` at the same time), it's a matter of displaying a proper error message. However, we can have a more general message to do fewer verifications.
created_at: 2025-04-03T12:20:07Z
author_association: MEMBER
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
+            selected_push_mode, selected_kwarg = mode, kwarg
+            _kwarg_deprecation_warning(kwarg, ["push_mode"], [mode], 2)
+
+    if not push_mode:
+        push_mode = selected_push_mode or quasardb.WriterPushMode.Transactional
 
     # Create batch column info from dataframe
     if writer is None:
         writer = cluster.writer()
 
+    ret = []
path: quasardb/numpy/__init__.py
user: login: rodp63
body: The declaration has been moved to keep all the `push_mode` verifications at the top. No harm in this, but keeping the declarations together
created_at: 2025-04-03T12:26:48Z
author_association: MEMBER
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
path: quasardb/numpy/__init__.py
user: login: solatis
body: I'd prefer less code in this case, keep it simple. 
created_at: 2025-04-03T12:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
+            selected_push_mode, selected_kwarg = mode, kwarg
+            _kwarg_deprecation_warning(kwarg, ["push_mode"], [mode], 2)
+
+    if not push_mode:
+        push_mode = selected_push_mode or quasardb.WriterPushMode.Transactional
 
     # Create batch column info from dataframe
     if writer is None:
         writer = cluster.writer()
 
+    ret = []
path: quasardb/numpy/__init__.py
user: login: solatis
body: I don't understand, I don't see this variable being used anywhere?
created_at: 2025-04-03T12:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -747,32 +795,53 @@ def write_arrays(
 
     if table:
         logger.debug("table explicitly provided, assuming single-table write")
-        return write_arrays(
-            [(table, data)],
-            cluster,
-            table=None,
-            dtype=dtype,
-            index=index,
-            _async=_async,
-            fast=fast,
-            truncate=truncate,
-            deduplicate=deduplicate,
-            deduplication_mode=deduplication_mode,
-            infer_types=infer_types,
-            write_through=write_through,
-            writer=writer,
-            retries=retries,
-            **kwargs,
+        data = [(table, data)]
+        table = None
+
+    _check_correct_type(push_mode, "push_mode", target_type=quasardb.WriterPushMode)
+
+    if isinstance(truncate, tuple):
+        # Especial case, truncate might be a tuple indicating the range.
+        truncate = False
+        truncate_range = truncate
+        push_mode = quasardb.WriterPushMode.Truncate
+        _kwarg_deprecation_warning(
+            "truncate", ["push_mode", "truncate_range"], [push_mode, "<RANGE>"], 2
         )
 
-    ret = []
+    deprecated_kwargs = {
+        "fast": quasardb.WriterPushMode.Fast,
+        "truncate": quasardb.WriterPushMode.Truncate,
+        "_async": quasardb.WriterPushMode.Async,
+    }
+    selected_push_mode, selected_kwarg = None, None
+
+    for kwarg, mode in deprecated_kwargs.items():
+        kwarg_value = locals()[kwarg]
+        _check_correct_type(kwarg_value, kwarg, target_type=bool, allow_none=False)
+
+        if kwarg_value:
+            if push_mode and push_mode != mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but push_mode={push_mode} is already set"
+                )
+            if selected_push_mode:
+                raise quasardb.quasardb.InvalidArgumentError(
+                    f"Found '{kwarg}=True' in kwargs, but '{selected_kwarg}' is already set. "
+                    "Please set only one, or use 'push_mode' instead"
+                )
+            selected_push_mode, selected_kwarg = mode, kwarg
+            _kwarg_deprecation_warning(kwarg, ["push_mode"], [mode], 2)
+
+    if not push_mode:
+        push_mode = selected_push_mode or quasardb.WriterPushMode.Transactional
 
     # Create batch column info from dataframe
     if writer is None:
         writer = cluster.writer()
 
+    ret = []
path: quasardb/numpy/__init__.py
user: login: rodp63
body: It's the returned variable, a list of tables
created_at: 2025-04-03T12:45:35Z
author_association: MEMBER

PR comments:
user: login: rodp63
created_at: 2025-04-02T20:05:45Z
body: Deprecation warnings and kwargs combinations:
```python
qdbnp.write_arrays(data, conn, index=idx, fast=True)
# DeprecationWarning: The argument 'fast' is deprecated and will be removed in a future version. Please use 'push_mode=WriterPushMode.Fast' instead.
```

```python
qdbnp.write_arrays(data, conn, index=idx, truncate=(0,0))
# DeprecationWarning: The argument 'truncate' is deprecated and will be removed in a future version. Please use 'push_mode=WriterPushMode.Truncate, truncate_range=<RANGE>' instead.
```

```python
qdbnp.write_arrays(data, conn, index=idx, fast=True, push_mode=quasardb.WriterPushMode.Truncate)
# InvalidArgumentError: Found 'fast=True' in kwargs, but push_mode=WriterPushMode.Truncate is already set
```

```python
qdbnp.write_arrays(data, conn, index=idx, _async=True, fast=True)
# InvalidArgumentError: Found '_async=True' in kwargs, but 'fast' is already set. Please set only one, or use 'push_mode' instead
```

Verbose type checks:
```python
qdbnp.write_arrays(data, conn, index=idx, _async=63)
# InvalidArgumentError: Invalid '_async' type, expected: <class 'bool'>, got: <class 'int'>
```

```python
qdbnp.write_arrays(data, conn, index=idx, push_mode="fast")
# InvalidArgumentError: Invalid 'push_mode' type, expected: <class 'quasardb.quasardb.WriterPushMode'>, got: <class 'str'>
```
author_association: MEMBER
user: login: solatis
created_at: 2025-04-03T12:38:48Z
body: Could you also adjust the tests to replace any use of "async" or "fast" or "truncate" parameters with the push_mode variant? 
author_association: CONTRIBUTOR