Repository: single-cell-data/TileDB-SOMA (language: python)

Issue:
[r] Expose timestamp ranges for libtiledbsoma
Improved handling of timestamp and timestamp ranges for libtiledbsoma. When users provide a timestamp to `SOMA*Open()`, it is expected to be a scalar, defined as "read up to this point"; as libtiledbsoma expects a vector of timestamps in the form of `<t0, t1>`, add a new `tiledb_timestamp_range` active binding to automagically convert the scalar `tiledb_timestamp` to a vector, or `NULL` if no timestamp was specified

New SOMA properites:
 - `TileDBObject$tiledb_timestamp_range`: new active binding to return the timestamp as a range `<t0, t1>` for libtiledbsoma; exposed as an active binding for use outside of the R6 objects

Modified SOMA methods:
 - `TileDBObject$initialize(tiledb_timestamp = )`: now requires `tiledb_timestamp` to be either `NULL` or a scalar, finite `POSIXct` object
 - `TileDBOBject$reopen(tiledb_timestamp = )`: same as `$initialize()`
[python] CI `mypy` issue as of 2024-08-05
To be investigated:

* Example CI fail https://github.com/single-cell-data/TileDB-SOMA/pull/2834/checks (not just that PR)
  * Lint ok on python 3.11 + ubuntu
  * Lint not ok on python 3.11 + macos
  * Maybe related to mypy 1.11.1 [just dropped a few days ago](https://pypi.org/project/mypy/1.11.1/) -- ? :thinking_face:
* Large-scale post-tag CI fail on [1.13.0rc0](https://github.com/single-cell-data/TileDB-SOMA/releases/tag/1.13.0rc0): https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10257321585/job/28378154410

On the one hand this might seem to be a minor annoyance; on the other, it's clouding CI just when we're trying to make the TileDB-SOMA 1.13.0 release. Thus we should solve this sooner than later.

See also #2838 (just created) which has significant narrative by @ryan-williams on it.

Some background info (something that's been in place for a year now, I am reading up on now):

* https://github.com/single-cell-data/TileDB-SOMA/blame/1.13.0rc0/.github/workflows/python-ci-single.yml#L45-L49
* https://github.com/single-cell-data/TileDB-SOMA/pull/756

[[sc-52471]](https://app.shortcut.com/tiledb-inc/story/52471)
[python] pin `codecov/uploader==0.7.3` in `codecov-action`
**Issue and/or context:** work-around for https://github.com/codecov/uploader/issues/1673

[uploader@0.8.0](https://github.com/codecov/uploader/releases/tag/v0.8.0) was released 4h ago, and broke our invocations of codecov-action@v3 ([example job](https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10201203260/job/28222497397#step:14:18) that passed yesterday, and [failed on re-run](https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10201203260/job/28277665063#step:14:18) today)

Failures look like:
```
==> macos OS detected
https://uploader.codecov.io/latest/macos/codecov.SHA256SUM
==> SHASUM file signed by key id 806bb28aed779869
==> Uploader SHASUM verified (75086ee436f6f74c4e8f65448eaa399e38bc93bb27a53d057b24b275b9f78c89  codecov)
==> Running version latest
==> Running version v0.8.0
/Users/runner/work/_actions/codecov/codecov-action/v3/dist/codecov -n  -Q github-action-3.1.6 -F python -C 25949be8d2ce35ef5beafd55bb1fd83c96e894b0
Error: spawn Unknown system error -86
    at ChildProcess.spawn (node:internal/child_process:421:11)
    at Object.spawn (node:child_process:761:9)
    at ToolRunner.<anonymous> (/Users/runner/work/_actions/codecov/codecov-action/v3/webpack:/codecov-action/node_modules/@actions/exec/lib/toolrunner.js:4[13](https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10201203260/job/28277665063#step:14:14):1)
    at Generator.next (<anonymous>)
    at /Users/runner/work/_actions/codecov/codecov-action/v3/webpack:/codecov-action/node_modules/@actions/exec/lib/toolrunner.js:27:1
    at new Promise (<anonymous>)
    at __webpack_modules__.8[15](https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10201203260/job/28277665063#step:14:16)9.__awaiter (/Users/runner/work/_actions/codecov/codecov-action/v3/webpack:/codecov-action/node_modules/@actions/exec/lib/toolrunner.js:23:1)
    at /Users/runner/work/_actions/codecov/codecov-action/v3/webpack:/codecov-action/node_modules/@actions/exec/lib/toolrunner.js:395:1
    at new Promise (<anonymous>)
    at ToolRunner.<anonymous> (/Users/runner/work/_actions/codecov/codecov-action/v3/webpack:/codecov-action/node_modules/@actions/exec/lib/toolrunner.js:395:1)
```

[python] pin `pandas-stubs>=2` during pre-commit `mypy` hook
also simplify some `if TYPE_CHECKING` blocks

**Issue and/or context:** #2839, #2853

The unpinned `pandas-stubs` dep in `.pre-commit-config.yaml` allowed existing local environments (and old cached `setup-python` environments, which in some cases were being incorrectly used in our GHAs, per https://github.com/actions/setup-python/issues/919) to run the pre-commit `mypy` hook using `pandas-stubs<2`, where `pd.Series` is the type, and `pd.Series[Any]` requires a `# type: ignore[misc]` annotation.

With `pandas-stubs>=2` (which new environments have been picking up), `pd.Series[Any]` is the correct type, and the `# type: ignore[misc]` annotation was flagged as "unused" ([example](https://github.com/single-cell-data/TileDB-SOMA/actions/runs/10291856865/job/28485041039?pr=2853#step:7:43)).

Pinning `pandas-stubs>=2`, and using the corresponding `pd.{Series,Index}[Any]` annotations when `TYPE_CHECKING`, should resolve this conflict, in both new and existing environments (in existing environments, `pre-commit` will recognize the new `pandas-stubs>=2` pin, and create a new venv that incorporates it).


Discussion / hints:

Re-assigning to @ryan-williams 
## [Codecov](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.06%. Comparing base [(`1fd58d1`)](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/commit/1fd58d141318388a1e03340aed7aadbed1d2a118?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) to head [(`1c2eef9`)](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/commit/1c2eef98df964ee649b457861001fa1bc9ad435e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2827   +/-   ##
=======================================
  Coverage   90.06%   90.06%           
=======================================
  Files          37       37           
  Lines        3937     3937           
=======================================
  Hits         3546     3546           
  Misses        391      391           
```

| [Flag](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) | Coverage Δ | |
|---|---|---|
| [python](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) | `90.06% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Components](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) | Coverage Δ | |
|---|---|---|
| [python_api](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) | `90.06% <ø> (ø)` | |
| [libtiledbsoma](https://app.codecov.io/gh/single-cell-data/TileDB-SOMA/pull/2827/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=single-cell-data) | `∅ <ø> (∅)` | |

</details>


Fix patch:
diff --git a/apis/r/DESCRIPTION b/apis/r/DESCRIPTION
index 94b71968ce..34fdd30b0f 100644
--- a/apis/r/DESCRIPTION
+++ b/apis/r/DESCRIPTION
@@ -6,7 +6,7 @@ Description: Interface for working with 'TileDB'-based Stack of Matrices,
     like those commonly used for single cell data analysis. It is documented at
     <https://github.com/single-cell-data>; a formal specification available is at
     <https://github.com/single-cell-data/SOMA/blob/main/abstract_specification.md>.
-Version: 1.13.99.2
+Version: 1.13.99.3
 Authors@R: c(
     person(given = "Aaron", family = "Wolen",
            role = c("cre", "aut"), email = "aaron@tiledb.com",
diff --git a/apis/r/NEWS.md b/apis/r/NEWS.md
index 9396e99ca2..d2db784ac7 100644
--- a/apis/r/NEWS.md
+++ b/apis/r/NEWS.md
@@ -2,6 +2,8 @@
 
 ## Changes
 
+* Make use of timestamp ranges in libtiledbsoma
+
 # tiledbsoma 1.13.0
 
 ## Changes
diff --git a/apis/r/R/Factory.R b/apis/r/R/Factory.R
index 92706f8eed..17eff71158 100644
--- a/apis/r/R/Factory.R
+++ b/apis/r/R/Factory.R
@@ -49,6 +49,7 @@ SOMADataFrameCreate <- function(
       schema,
       index_column_names = index_column_names,
       platform_config = platform_config,
+      timestamps = rep(tiledb_timestamp, 2),
       internal_use_only = "allowed_use"
     )
   }
@@ -75,6 +76,7 @@ SOMADataFrameOpen <- function(
   tiledbsoma_ctx = NULL,
   tiledb_timestamp = NULL
 ) {
+  spdl::debug("[SOMADataFrameOpen] uri {} ts ({},{})", uri, tiledb_timestamp[1], tiledb_timestamp[2])
   sdf <- SOMADataFrame$new(
     uri,
     platform_config,
@@ -125,6 +127,7 @@ SOMASparseNDArrayCreate <- function(
       type,
       shape,
       platform_config = platform_config,
+      timestamps = rep(tiledb_timestamp, 2),
       internal_use_only = "allowed_use"
     )
   }
@@ -173,6 +176,7 @@ SOMADenseNDArrayCreate <- function(
   tiledbsoma_ctx = NULL,
   tiledb_timestamp = NULL
 ) {
+  spdl::debug("[SOMADenseNDArrayCreate] tstamp ({},{})", tiledb_timestamp[1], tiledb_timestamp[2])
   dnda <- SOMADenseNDArray$new(
     uri,
     platform_config,
@@ -184,6 +188,7 @@ SOMADenseNDArrayCreate <- function(
     type,
     shape,
     platform_config = platform_config,
+    timestamps = rep(tiledb_timestamp, 2),
     internal_use_only = "allowed_use"
   )
   return(dnda)
diff --git a/apis/r/R/RcppExports.R b/apis/r/R/RcppExports.R
index 271411287c..4f20189acf 100644
--- a/apis/r/R/RcppExports.R
+++ b/apis/r/R/RcppExports.R
@@ -5,12 +5,12 @@ createSOMAContext <- function(config = NULL) {
     .Call(`_tiledbsoma_createSOMAContext`, config)
 }
 
-createSchemaFromArrow <- function(uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp) {
-    invisible(.Call(`_tiledbsoma_createSchemaFromArrow`, uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp))
+createSchemaFromArrow <- function(uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp, tsvec = NULL) {
+    invisible(.Call(`_tiledbsoma_createSchemaFromArrow`, uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp, tsvec))
 }
 
-writeArrayFromArrow <- function(uri, naap, nasp, arraytype = "", config = NULL) {
-    invisible(.Call(`_tiledbsoma_writeArrayFromArrow`, uri, naap, nasp, arraytype, config))
+writeArrayFromArrow <- function(uri, naap, nasp, arraytype = "", config = NULL, tsvec = NULL) {
+    invisible(.Call(`_tiledbsoma_writeArrayFromArrow`, uri, naap, nasp, arraytype, config, tsvec))
 }
 
 #' Get nnumber of metadata items
@@ -66,12 +66,14 @@ delete_metadata <- function(uri, key, is_array, ctxxp) {
 #'
 #' @param uri The array URI
 #' @param key The array metadata key
-#' @param value The metadata value
-#' @
+#' @param valuesxp The metadata value
+#' @param type The datatype
+#' @param is_array A boolean to indicate array or group
 #' @param ctxxp An external pointer to the SOMAContext wrapper
+#' @param tsvec An optional two-element datetime vector
 #' @export
-set_metadata <- function(uri, key, valuesxp, type, is_array, ctxxp) {
-    invisible(.Call(`_tiledbsoma_set_metadata`, uri, key, valuesxp, type, is_array, ctxxp))
+set_metadata <- function(uri, key, valuesxp, type, is_array, ctxxp, tsvec = NULL) {
+    invisible(.Call(`_tiledbsoma_set_metadata`, uri, key, valuesxp, type, is_array, ctxxp, tsvec))
 }
 
 reindex_create <- function() {
@@ -87,8 +89,8 @@ reindex_lookup <- function(idx, kvec) {
 }
 
 #' @noRd
-soma_array_reader_impl <- function(uri, colnames = NULL, qc = NULL, dim_points = NULL, dim_ranges = NULL, batch_size = "auto", result_order = "auto", loglevel = "auto", config = NULL) {
-    .Call(`_tiledbsoma_soma_array_reader`, uri, colnames, qc, dim_points, dim_ranges, batch_size, result_order, loglevel, config)
+soma_array_reader_impl <- function(uri, colnames = NULL, qc = NULL, dim_points = NULL, dim_ranges = NULL, batch_size = "auto", result_order = "auto", loglevel = "auto", config = NULL, timestamprange = NULL) {
+    .Call(`_tiledbsoma_soma_array_reader`, uri, colnames, qc, dim_points, dim_ranges, batch_size, result_order, loglevel, config, timestamprange)
 }
 
 #' Set the logging level for the R package and underlying C++ library
@@ -148,8 +150,8 @@ shape <- function(uri, config = NULL) {
 #' @param result_order Optional argument for query result order, defaults to \sQuote{auto}
 #' @param loglevel Character value with the desired logging level, defaults to \sQuote{auto}
 #' which lets prior setting prevail, any other value is set as new logging level.
-#' @param timestamp_end Optional POSIXct (i.e. Datetime) type for end of interval for which
-#' data is considered.
+#' @param timestamprange Optional POSIXct (i.e. Datetime) vector with start and end of
+#' interval for which data is considered.
 #' @param sr An external pointer to a TileDB SOMAArray object
 #'
 #' @return \code{sr_setup} returns an external pointer to a SOMAArray. \code{sr_complete}
@@ -169,8 +171,8 @@ shape <- function(uri, config = NULL) {
 #' summary(rl)
 #' }
 #' @noRd
-sr_setup <- function(uri, config, colnames = NULL, qc = NULL, dim_points = NULL, dim_ranges = NULL, batch_size = "auto", result_order = "auto", timestamp_end = NULL, loglevel = "auto") {
-    .Call(`_tiledbsoma_sr_setup`, uri, config, colnames, qc, dim_points, dim_ranges, batch_size, result_order, timestamp_end, loglevel)
+sr_setup <- function(uri, config, colnames = NULL, qc = NULL, dim_points = NULL, dim_ranges = NULL, batch_size = "auto", result_order = "auto", timestamprange = NULL, loglevel = "auto") {
+    .Call(`_tiledbsoma_sr_setup`, uri, config, colnames, qc, dim_points, dim_ranges, batch_size, result_order, timestamprange, loglevel)
 }
 
 sr_complete <- function(sr) {
diff --git a/apis/r/R/SOMAArrayBase.R b/apis/r/R/SOMAArrayBase.R
index ca9f241838..237db7784f 100644
--- a/apis/r/R/SOMAArrayBase.R
+++ b/apis/r/R/SOMAArrayBase.R
@@ -29,13 +29,14 @@ SOMAArrayBase <- R6::R6Class(
       private$soma_type_cache <- self$get_metadata(SOMA_OBJECT_TYPE_METADATA_KEY)
     },
 
-    write_object_type_metadata = function() {
-      private$check_open_for_write()
+    write_object_type_metadata = function(timestamps=NULL) {
+      #private$check_open_for_write()
 
       meta <- list()
       meta[[SOMA_OBJECT_TYPE_METADATA_KEY]] <- self$class()
       meta[[SOMA_ENCODING_VERSION_METADATA_KEY]] <- SOMA_ENCODING_VERSION
-      self$set_metadata(meta)
+      spdl::debug("[SOMAArrayBase::write_object_metadata] calling set metadata")
+      self$set_metadata(meta, c(self$tiledb_timestamp,self$tiledb_timestamp))
     }
   )
 )
diff --git a/apis/r/R/SOMADataFrame.R b/apis/r/R/SOMADataFrame.R
index 3eb24aed2d..b4d81fff0c 100644
--- a/apis/r/R/SOMADataFrame.R
+++ b/apis/r/R/SOMADataFrame.R
@@ -21,10 +21,11 @@ SOMADataFrame <- R6::R6Class(
     #' index columns.  All named columns must exist in the schema, and at least
     #' one index column name is required.
     #' @template param-platform-config
+    #' @param timestamps Optional timestamp start and end range
     #' @param internal_use_only Character value to signal this is a 'permitted' call,
     #' as `create()` is considered internal and should not be called directly.
     create = function(schema, index_column_names = c("soma_joinid"),
-                      platform_config = NULL, internal_use_only = NULL) {
+                      platform_config = NULL, timestamps = NULL, internal_use_only = NULL) {
       if (is.null(internal_use_only) || internal_use_only != "allowed_use") {
         stop(paste("Use of the create() method is for internal use only. Consider using a",
                    "factory method as e.g. 'SOMADataFrameCreate()'."), call. = FALSE)
@@ -53,12 +54,15 @@ SOMADataFrame <- R6::R6Class(
       nasp <- nanoarrow::nanoarrow_allocate_schema()
       schema$export_to_c(nasp)
 
+      spdl::debug("[SOMADataFrame$create] about to create schema from arrow")
       ctxptr <- super$tiledbsoma_ctx$context()
       createSchemaFromArrow(uri = self$uri, nasp, dnaap, dnasp, TRUE, "SOMADataFrame",
-                            tiledb_create_options$to_list(FALSE), soma_context())
+                            tiledb_create_options$to_list(FALSE), soma_context(), timestamps)
+
+      spdl::debug("[SOMADataFrame$create] about to call write_object_type_metadata")
+      private$write_object_type_metadata(timestamps)
 
       self$open("WRITE", internal_use_only = "allowed_use")
-      private$write_object_type_metadata()
       self
     },
 
@@ -67,8 +71,10 @@ SOMADataFrame <- R6::R6Class(
     #' @param values An [`arrow::Table`] or [`arrow::RecordBatch`]
     #' containing all columns, including any index columns. The
     #' schema for `values` must match the schema for the `SOMADataFrame`.
+    #' @param tsrange An optional two-element Datetime vector for the
+    #' start and end of the timestamp range
     #'
-    write = function(values) {
+    write = function(values, tsrange = NULL) {
       private$check_open_for_write()
 
       # Prevent downcasting of int64 to int32 when materializing a column
@@ -97,8 +103,7 @@ SOMADataFrame <- R6::R6Class(
 
       df <- as.data.frame(values)[schema_names]
       arr <- self$object
-
-      writeArrayFromArrow(self$uri, naap, nasp, "SOMADataFrame")
+      writeArrayFromArrow(self$uri, naap, nasp, "SOMADataFrame", NULL, tsrange)
 
       invisible(self)
     },
@@ -152,14 +157,15 @@ SOMADataFrame <- R6::R6Class(
                             args = list(expr = str2lang(value_filter), ta = arr))
           value_filter <- parsed@ptr
       }
-
+      spdl::debug("[SOMADataFrame$read] calling sr_setup for {} at ({},{})", self$uri,
+                  private$tiledb_timestamp[1], private$tiledb_timestamp[2])
       cfg <- as.character(tiledb::config(self$tiledbsoma_ctx$context()))
       rl <- sr_setup(uri = self$uri,
                      config = cfg,
                      colnames = column_names,
                      qc = value_filter,
                      dim_points = coords,
-                     timestamp_end = private$tiledb_timestamp,
+                     timestamprange = self$tiledb_timestamp,  # NULL or two-elem vector
                      loglevel = log_level)
       private$ctx_ptr <- rl$ctx
       TableReadIter$new(rl$sr)
diff --git a/apis/r/R/SOMADenseNDArray.R b/apis/r/R/SOMADenseNDArray.R
index e259bb24ee..884da79b7c 100644
--- a/apis/r/R/SOMADenseNDArray.R
+++ b/apis/r/R/SOMADenseNDArray.R
@@ -32,6 +32,8 @@ SOMADenseNDArray <- R6::R6Class(
     #' read. List elements can be named when specifying a subset of dimensions.
     #' @template param-result-order
     #' @param log_level Optional logging level with default value of `"warn"`.
+    #' @param timestamprange Optional POSIXct (i.e. Datetime) vector with start and end of
+    #' interval for which data is considered.
     #' @return An [`arrow::Table`].
     read_arrow_table = function(
       coords = NULL,
@@ -52,10 +54,15 @@ SOMADenseNDArray <- R6::R6Class(
       coords <- private$.convert_coords(coords)
 
       cfg <- as.character(tiledb::config(self$tiledbsoma_ctx$context()))
+      spdl::debug("[SOMADenseNDArray$read_arrow_table] timestamp ({},{})",
+                  self$tiledb_timestamp[1], self$tiledb_timestamp[2])
+
       rl <- soma_array_reader(uri = uri,
                               dim_points = coords,
                               result_order = result_order,
-                              loglevel = log_level,       # NULL dealt with by soma_array_reader()
+                              timestamprange = if (is.null(self$tiledb_timestamp)) self$tiledb_timestamp
+                                               else c(0, self$tiledb_timestamp[2]),
+                              loglevel = log_level,
                               config = cfg)
 
       soma_array_to_arrow_table(rl)
@@ -137,7 +144,8 @@ SOMADenseNDArray <- R6::R6Class(
       nasp <- nanoarrow::nanoarrow_allocate_schema()
       arrow::as_record_batch(tbl)$export_to_c(naap, nasp)
       #arr[] <- values
-      writeArrayFromArrow(self$uri, naap, nasp, "SOMADenseNDArray")
+      writeArrayFromArrow(self$uri, naap, nasp, "SOMADenseNDArray", NULL,
+                          c(self$tiledb_timestamp[1], self$tiledb_timestamp[1]))
       spdl::debug("[SOMADenseNDArray::write] written")
 
       # tiledb-r always closes the array after a write operation so we need to
diff --git a/apis/r/R/SOMANDArrayBase.R b/apis/r/R/SOMANDArrayBase.R
index a891fc42bc..25a11ed73e 100644
--- a/apis/r/R/SOMANDArrayBase.R
+++ b/apis/r/R/SOMANDArrayBase.R
@@ -20,10 +20,12 @@ SOMANDArrayBase <- R6::R6Class(
     #' element in the array.
     #' @param shape a vector of integers defining the shape of the array.
     #' @template param-platform-config
+    #' @param timestamps Optional timestamp start and end range
     #' @param internal_use_only Character value to signal this is a 'permitted'
     #' call, as `create()` is considered internal and should not be called
     #' directly.
-    create = function(type, shape, platform_config = NULL, internal_use_only = NULL) {
+    create = function(type, shape, platform_config = NULL,
+                      timestamps = NULL, internal_use_only = NULL) {
       if (is.null(internal_use_only) || internal_use_only != "allowed_use") {
         stop(paste("Use of the create() method is for internal use only. Consider using a",
                    "factory method as e.g. 'SOMASparseNDArrayCreate()'."), call. = FALSE)
@@ -59,10 +61,10 @@ SOMANDArrayBase <- R6::R6Class(
       createSchemaFromArrow(uri = self$uri, nasp, dnaap, dnasp,
                             private$.is_sparse,
                             if (private$.is_sparse) "SOMASparseNDArray" else "SOMADenseNDArray",
-                            tiledb_create_options$to_list(FALSE), soma_context())
+                            tiledb_create_options$to_list(FALSE), soma_context(), timestamps)
+      #private$write_object_type_metadata(timestamps)  ## FIXME: temp. commented out -- can this be removed overall?
 
       self$open("WRITE", internal_use_only = "allowed_use")
-      private$write_object_type_metadata()
       self
     },
 
diff --git a/apis/r/R/SOMASparseNDArray.R b/apis/r/R/SOMASparseNDArray.R
index 84b8299c2e..a6e412c2b2 100644
--- a/apis/r/R/SOMASparseNDArray.R
+++ b/apis/r/R/SOMASparseNDArray.R
@@ -51,7 +51,8 @@ SOMASparseNDArray <- R6::R6Class(
                      config = cfg,
                      dim_points = coords,
                      result_order = result_order,
-                     timestamp_end = private$tiledb_timestamp,
+                     timestamprange = if (is.null(self$tiledb_timestamp)) self$tiledb_timestamp
+                                      else c(0, self$tiledb_timestamp[2]),
                      loglevel = log_level)
       private$ctx_ptr <- rl$ctx
       SOMASparseNDArrayRead$new(rl$sr, self, coords)
@@ -174,7 +175,10 @@ SOMASparseNDArray <- R6::R6Class(
         names(bbox_flat)[index:(index + 1L)] <- paste0(names(bbox)[i], c('_lower', '_upper'))
         index <- index + 2L
       }
-      self$set_metadata(bbox_flat)
+      self$set_metadata(bbox_flat, c(self$tiledb_timestamp[1],self$tiledb_timestamp[1]))
+      spdl::debug("[SOMASparseNDArray$write] Calling .write_coo_df ({},{})",
+                  self$tiledb_timestamp[1],self$tiledb_timestamp[2])
+
       private$.write_coo_dataframe(coo)
 
       invisible(self)
@@ -220,10 +224,11 @@ SOMASparseNDArray <- R6::R6Class(
 
       stopifnot(is.data.frame(values))
       # private$log_array_ingestion()
-      arr <- self$object
-      if (!is.null(private$tiledb_timestamp)) {
-          arr@timestamp <- private$tiledb_timestamp
-      }
+      #arr <- self$object
+      #if (!is.null(self$tiledb_timestamp)) {
+      #    # arr@timestamp <- self$tiledb_timestamp
+      #   arr@timestamp_end <- self$tiledb_timestamp
+      #}
       nms <- colnames(values)
 
       ## the 'soma_data' data type may not have been cached, and if so we need to fetch it
@@ -239,11 +244,14 @@ SOMASparseNDArray <- R6::R6Class(
                               arrow::field(nms[3], private$.type))
 
       tbl <- arrow::arrow_table(values, schema = arrsch)
-      spdl::debug("[SOMASparseNDArray::write] array created, writing to {}", self$uri)
+      spdl::debug("[SOMASparseNDArray$write] array created, writing to {} at ({},{})", self$uri,
+                  self$tiledb_timestamp[1],self$tiledb_timestamp[2])
       naap <- nanoarrow::nanoarrow_allocate_array()
       nasp <- nanoarrow::nanoarrow_allocate_schema()
       arrow::as_record_batch(tbl)$export_to_c(naap, nasp)
-      writeArrayFromArrow(self$uri, naap, nasp, "SOMASparseNDArray")
+      writeArrayFromArrow(self$uri, naap, nasp, "SOMASparseNDArray", NULL,
+                          if (is.null(self$tiledb_timestamp[1])) NULL
+                          else c(self$tiledb_timestamp[1], self$tiledb_timestamp[1]))
     },
 
     # Internal marking of one or zero based matrices for iterated reads
diff --git a/apis/r/R/SOMASparseNDArrayRead.R b/apis/r/R/SOMASparseNDArrayRead.R
index 018718e24c..e4b620f24b 100644
--- a/apis/r/R/SOMASparseNDArrayRead.R
+++ b/apis/r/R/SOMASparseNDArrayRead.R
@@ -134,7 +134,7 @@ SOMASparseNDArrayRead <- R6::R6Class(
     sparse_matrix = function(zero_based=FALSE) {
       #TODO implement zero_based argument, currently doesn't do anything
 
-        shape <- self$shape
+      shape <- self$shape
       # if (any(private$shape > .Machine$integer.max)) {
       if (any(shape > .Machine$integer.max)) {
         warning(
@@ -147,7 +147,6 @@ SOMASparseNDArrayRead <- R6::R6Class(
         # private$shape <- pmin(private$shape, .Machine$integer.max)
         shape <- pmin(shape, .Machine$integer.max)
       }
-
       SparseReadIter$new(self$sr, shape, zero_based = zero_based)
     },
 
diff --git a/apis/r/R/TileDBArray.R b/apis/r/R/TileDBArray.R
index 795a580b94..67aa8d7b4a 100644
--- a/apis/r/R/TileDBArray.R
+++ b/apis/r/R/TileDBArray.R
@@ -26,15 +26,16 @@ TileDBArray <- R6::R6Class(
       }
 
       private$.mode <- mode
-      if (is.null(private$tiledb_timestamp)) {
-        spdl::debug("[TileDBArray::open] Opening {} '{}' in {} mode", self$class(), self$uri, mode)
+      if (is.null(self$tiledb_timestamp)) {
+        spdl::debug("[TileDBArray$open] Opening {} '{}' in {} mode", self$class(), self$uri, mode)
         private$.tiledb_array <- tiledb::tiledb_array_open(self$object, type = mode)
       } else {
-        stopifnot("tiledb_timestamp not yet supported for WRITE mode" = mode == "READ")
-        spdl::debug("[TileDBArray::open] Opening {} '{}' in {} mode at {}",
-                    self$class(), self$uri, mode, private$tiledb_timestamp)
-        private$.tiledb_array <- tiledb::tiledb_array_open_at(self$object, type = mode,
-                                                              timestamp = private$tiledb_timestamp)
+        if (is.null(internal_use_only)) stopifnot("tiledb_timestamp not yet supported for WRITE mode" = mode == "READ")
+        spdl::debug("[TileDBArray$open] Opening {} '{}' in {} mode at ({},{})",
+                    self$class(), self$uri, mode, self$tiledb_timestamp[1],
+                    self$tiledb_timestamp[2])
+        #private$.tiledb_array <- tiledb::tiledb_array_open_at(self$object, type = mode,
+        #                                                      timestamp = self$tiledb_timestamp)
       }
 
       ## TODO -- cannot do here while needed for array case does not work for data frame case
@@ -47,7 +48,7 @@ TileDBArray <- R6::R6Class(
     #' @description Close the SOMA object.
     #' @return The object, invisibly
     close = function() {
-      spdl::debug("[TileDBArray::close] Closing {} '{}'", self$class(), self$uri)
+      spdl::debug("[TileDBArray$close] Closing {} '{}'", self$class(), self$uri)
       private$.mode = "CLOSED"
       tiledb::tiledb_array_close(self$object)
       invisible(self)
@@ -71,7 +72,7 @@ TileDBArray <- R6::R6Class(
       args$query_type <- self$.mode
       args$query_layout <- "UNORDERED"
       args$ctx <- self$tiledbsoma_ctx$context()
-      spdl::debug("[TileDBArray::tiledb_array] ctor uri {} mode {} layout {}", args$uri, args$query_type, args$query_layout)
+      spdl::debug("[TileDBArray$tiledb_array] ctor uri {} mode {} layout {}", args$uri, args$query_type, args$query_layout)
       do.call(tiledb::tiledb_array, args)
     },
 
@@ -81,7 +82,7 @@ TileDBArray <- R6::R6Class(
     get_metadata = function(key = NULL) {
       private$check_open_for_read_or_write()
 
-      spdl::debug("[TileDBArray::get_metadata] Retrieving metadata for {} '{}'", self$class(), self$uri)
+      spdl::debug("[TileDBArray$get_metadata] Retrieving metadata for {} '{}'", self$class(), self$uri)
       private$fill_metadata_cache_if_null()
       if (!is.null(key)) {
         private$.metadata_cache[[key]]
@@ -92,19 +93,19 @@ TileDBArray <- R6::R6Class(
 
     #' @description Add list of metadata to the specified TileDB array. (lifecycle: maturing)
     #' @param metadata Named list of metadata to add.
+    #' @param timestamps Optional two-element vector of timestamp range starts and end
     #' @return NULL
-    set_metadata = function(metadata) {
+    set_metadata = function(metadata, timestamps = NULL) {
       stopifnot(
         "Metadata must be a named list" = is_named_list(metadata)
       )
 
-      private$check_open_for_write()
+      #private$check_open_for_write()
 
       for (nm in names(metadata)) {
-          #spdl::warn("[set_metadata] key {}", nm)
           val <- metadata[[nm]]
-          spdl::debug("[set_metadata] setting key {} to {} ({})", nm, val, class(val))
-          set_metadata(self$uri, nm, val, class(val), TRUE, soma_context())
+          spdl::debug("[TileDBArray$set_metadata] setting key {} to {} ({})", nm, val, class(val))
+          set_metadata(self$uri, nm, val, class(val), TRUE, soma_context(), timestamps)
       }
 
       dev_null <- mapply(
@@ -207,9 +208,15 @@ TileDBArray <- R6::R6Class(
     non_empty_domain = function(index1 = FALSE) {
       dims <- self$dimnames()
       ned <- bit64::integer64(length = length(dims))
+      ## added during C++-ification as self$object could close
+      if (isFALSE(tiledb::tiledb_array_is_open(self$object))) {
+          arrhandle <- tiledb::tiledb_array_open(self$object, type = "READ")
+      } else {
+          arrhandle <- self$object
+      }
       for (i in seq_along(along.with = ned)) {
         dom <- max(tiledb::tiledb_array_get_non_empty_domain_from_name(
-          self$object,
+          arrhandle, # instead of:  self$object,
           name = dims[i]
         ))
         if (isTRUE(x = index1)) {
@@ -338,29 +345,29 @@ TileDBArray <- R6::R6Class(
     },
 
     update_metadata_cache = function() {
-      spdl::debug("[TileDBArray::update_metadata_cache] updating metadata cache for {} '{}' in {}", self$class(), self$uri, private$.mode)
+      spdl::debug("[TileDBArray$update_metadata_cache] updating metadata cache for {} '{}' in {}", self$class(), self$uri, private$.mode)
 
       # See notes above -- at the TileDB implementation level, we cannot read array metadata
       # while the array is open for read, but at the SOMA application level we must support
       # this. Therefore if the array is opened for write and there is no cache populated then
       # we must open a temporary handle for read, to fill the cache.
-      array_handle <- private$.tiledb_array
-      if (private$.mode == "WRITE") {
-        spdl::debug("[TileDBArray::update_metadata_cache] getting object")
-        array_object <- tiledb::tiledb_array(self$uri, ctx = private$.tiledb_ctx)
-        array_handle <- tiledb::tiledb_array_open(array_object, type = "READ")
-      }
-
-      if (isFALSE(tiledb::tiledb_array_is_open(array_handle))) {
-        spdl::debug("[TileDBArray::update_metadata_cache] reopening object")
-        array_handle <- tiledb::tiledb_array_open(array_handle, type = "READ")
-      }
+      #array_handle <- private$.tiledb_array
+      #if (private$.mode == "WRITE") {
+      #  spdl::debug("[TileDBArray::update_metadata_cache] getting object")
+      #  array_object <- tiledb::tiledb_array(self$uri, ctx = private$.tiledb_ctx)
+      #  array_handle <- tiledb::tiledb_array_open(array_object, type = "READ")
+      #}
+
+      #if (isFALSE(tiledb::tiledb_array_is_open(array_handle))) {
+      #  spdl::debug("[TileDBArray::update_metadata_cache] reopening object")
+      #  array_handle <- tiledb::tiledb_array_open(array_handle, type = "READ")
+      #}
 
       private$.metadata_cache <- get_all_metadata(self$uri, TRUE, soma_context())
-
-      if (private$.mode == "WRITE") {
-        tiledb::tiledb_array_close(array_handle)
-      }
+      #print(str(private$.metadata_cache))
+      #if (private$.mode == "WRITE") {
+      #  tiledb::tiledb_array_close(array_handle)
+      #}
 
       invisible(NULL)
     },
diff --git a/apis/r/R/TileDBGroup.R b/apis/r/R/TileDBGroup.R
index fd1f788c5d..82f411231a 100644
--- a/apis/r/R/TileDBGroup.R
+++ b/apis/r/R/TileDBGroup.R
@@ -52,12 +52,12 @@ TileDBGroup <- R6::R6Class(
                    "factory method as e.g. 'SOMACollectionOpen()'."), call. = FALSE)
       }
       private$.mode = mode
-      private$.group_open_timestamp <- if (mode == "READ" && is.null(private$tiledb_timestamp)) {
+      private$.group_open_timestamp <- if (mode == "READ" && is.null(self$tiledb_timestamp)) {
         # In READ mode, if the opener supplied no timestamp then we default to the time of
         # opening, providing a temporal snapshot of all group members.
         Sys.time()
       } else {
-        private$tiledb_timestamp
+        self$tiledb_timestamp
       }
       if (is.null(private$.group_open_timestamp)) {
         spdl::debug("Opening {} '{}' in {} mode", self$class(), self$uri, mode)
@@ -312,7 +312,7 @@ TileDBGroup <- R6::R6Class(
     .tiledb_group = NULL,
 
     # This field stores the timestamp with which we opened the group, whether we used the
-    # opener-supplied private$tiledb_timestamp, or defaulted to the time of opening, or neither
+    # opener-supplied self$tiledb_timestamp, or defaulted to the time of opening, or neither
     # (NULL). This is the timestamp to propagate to accessed members.
     .group_open_timestamp = NULL,
 
diff --git a/apis/r/R/TileDBObject.R b/apis/r/R/TileDBObject.R
index 9f94c763ba..0b80e14251 100644
--- a/apis/r/R/TileDBObject.R
+++ b/apis/r/R/TileDBObject.R
@@ -40,10 +40,11 @@ TileDBObject <- R6::R6Class(
 
       if (!is.null(tiledb_timestamp)) {
         stopifnot("'tiledb_timestamp' must be a POSIXct datetime object" = inherits(tiledb_timestamp, "POSIXct"))
-        private$tiledb_timestamp <- tiledb_timestamp
+        private$.tiledb_timestamp <- tiledb_timestamp
       }
 
-      spdl::debug("[TileDBObject] initialize {} with '{}'", self$class(), self$uri)
+      spdl::debug("[TileDBObject] initialize {} with '{}' at ({},{})", self$class(), self$uri,
+                  private$.tiledb_timestamp[1],private$.tiledb_timestamp[2])
     },
 
     #' @description Print the name of the R6 class.
@@ -82,13 +83,18 @@ TileDBObject <- R6::R6Class(
     #'  \item \dQuote{\code{READ}}
     #'  \item \dQuote{\code{WRITE}}
     #' }
+    #' @param tiledb_timestamp Optional Datetime (POSIXct) with TileDB timestamp
     #'
     #' @return Invisibly returns \code{self} opened in \code{mode}
     #'
-    reopen = function(mode) {
+    reopen = function(mode, tiledb_timestamp = NULL) {
       mode <- match.arg(mode, choices = c('READ', 'WRITE'))
+      stopifnot(
+        "'tiledb_timestamp' must be a POSIXct datetime object" = is.null(tiledb_timestamp) ||
+          inherits(tiledb_timestamp, what = "POSIXct")
+      )
       self$close()
-      private$tiledb_timestamp <- NULL
+      private$.tiledb_timestamp <- tiledb_timestamp
       self$open(mode, internal_use_only = 'allowed_use')
       return(invisible(self))
     },
@@ -130,6 +136,14 @@ TileDBObject <- R6::R6Class(
       }
       return(private$.tiledbsoma_ctx)
     },
+    #' @field tiledb_timestamp Time that object was opened at
+    #'
+    tiledb_timestamp = function(value) {
+      if (!missing(value)) {
+        private$.read_only_error("tiledb_timestamp")
+      }
+      return(private$.tiledb_timestamp)
+    },
     #' @field uri
     #' The URI of the TileDB object.
     uri = function(value) {
@@ -162,7 +176,7 @@ TileDBObject <- R6::R6Class(
 
     # Opener-supplied POSIXct timestamp, if any. TileDBArray and TileDBGroup are each responsible
     # for making this effective, since the methods differ slightly.
-    tiledb_timestamp = NULL,
+    .tiledb_timestamp = NULL,
 
     # Internal context
     .tiledbsoma_ctx = NULL,
diff --git a/apis/r/R/soma_array_reader.R b/apis/r/R/soma_array_reader.R
index c3fe8e3df0..a0cda43e0f 100644
--- a/apis/r/R/soma_array_reader.R
+++ b/apis/r/R/soma_array_reader.R
@@ -28,7 +28,7 @@
 #' @noRd
 soma_array_reader <- function(uri, colnames = NULL, qc = NULL, dim_points = NULL, dim_ranges = NULL,
                               batch_size = "auto", result_order = "auto", loglevel = "auto",
-                              config = NULL) {
+                              config = NULL, timestamprange = NULL) {
 
     stopifnot("'uri' must be character" = is.character(uri),
               "'colnames' must be character or NULL" = is_character_or_null(colnames),
@@ -53,6 +53,8 @@ soma_array_reader <- function(uri, colnames = NULL, qc = NULL, dim_points = NULL
         }
     }
 
+    spdl::debug("[soma_array_reader] calling soma_array_reader_impl ({},{}",
+                timestamprange[1], timestamprange[2])
     soma_array_reader_impl(uri, colnames, qc, dim_points, dim_ranges, batch_size, result_order,
-                           loglevel, config)
+                           loglevel, config, timestamprange)
 }
diff --git a/apis/r/R/utils.R b/apis/r/R/utils.R
index b8b451305a..2b78da7f2c 100644
--- a/apis/r/R/utils.R
+++ b/apis/r/R/utils.R
@@ -123,7 +123,11 @@ uns_hint <- function(type = c('1d', '2d')) {
 .read_soma_joinids <- function(x, ...) {
   stopifnot(inherits(x = x, what = 'TileDBArray'))
   oldmode <- x$mode()
-  on.exit(x$reopen(oldmode), add = TRUE, after = FALSE)
+  on.exit(
+    x$reopen(oldmode, tiledb_timestamp = x$tiledb_timestamp),
+    add = TRUE,
+    after = FALSE
+  )
   op <- options(arrow.int64_downcast = FALSE)
   on.exit(options(op), add = TRUE, after = FALSE)
   ids <- UseMethod(generic = '.read_soma_joinids', object = x)
@@ -138,7 +142,7 @@ uns_hint <- function(type = c('1d', '2d')) {
 #' @export
 #'
 .read_soma_joinids.SOMADataFrame <- function(x, ...) {
-  x$reopen("READ")
+  x$reopen("READ", tiledb_timestamp = x$tiledb_timestamp)
   return(x$read(column_names = "soma_joinid")$concat()$GetColumnByName("soma_joinid")$as_vector())
 }
 
@@ -159,13 +163,13 @@ uns_hint <- function(type = c('1d', '2d')) {
   if (axis < 0L || axis >= length(x$dimnames())) {
     stop("'axis' must be between 0 and ", length(x$dimnames()), call. = FALSE)
   }
-  x$reopen("READ")
+  x$reopen("READ", tiledb_timestamp = x$tiledb_timestamp)
   dimname <- x$dimnames()[axis + 1L]
   rl <- sr_setup(
     uri = x$uri,
     config = as.character(tiledb::config(x$tiledbsoma_ctx$context())),
     colnames = dimname,
-    timestamp_end = x$.__enclos_env__$private$tiledb_timestamp
+    timestamprange = c(0, x$tiledb_timestamp)
   )
   return(TableReadIter$new(rl$sr)$concat()$GetColumnByName(dimname)$as_vector())
 }
diff --git a/apis/r/R/write_soma.R b/apis/r/R/write_soma.R
index 89194b80e8..e1433458d6 100644
--- a/apis/r/R/write_soma.R
+++ b/apis/r/R/write_soma.R
@@ -466,7 +466,8 @@ write_soma.TsparseMatrix <- function(
     shape = shape %||% dim(x),
     ingest_mode = ingest_mode,
     platform_config = platform_config,
-    tiledbsoma_ctx = tiledbsoma_ctx
+    tiledbsoma_ctx = tiledbsoma_ctx,
+    tiledb_timestamp = Sys.time()
   )
   # Write values
   if (ingest_mode %in% c('resume')) {
@@ -606,13 +607,13 @@ write_soma.TsparseMatrix <- function(
   )
   xmode <- x$mode()
   if (xmode == 'CLOSED') {
-    x$reopen('READ')
+    x$reopen('READ', tiledb_timestamp = x$tiledb_timestamp)
     xmode <- x$mode()
   }
   on.exit(x$reopen(mode = xmode), add = TRUE, after = FALSE)
   oldmode <- soma_parent$mode()
   if (oldmode == 'CLOSED') {
-    soma_parent$reopen("READ")
+    soma_parent$reopen("READ", tiledb_timestamp = soma_parent$tiledb_timestamp)
     oldmode <- soma_parent$mode()
   }
   on.exit(soma_parent$reopen(oldmode), add = TRUE, after = FALSE)
diff --git a/apis/r/man/SOMADataFrame.Rd b/apis/r/man/SOMADataFrame.Rd
index 1e84308920..9c3a2d550b 100644
--- a/apis/r/man/SOMADataFrame.Rd
+++ b/apis/r/man/SOMADataFrame.Rd
@@ -63,6 +63,7 @@ Create (lifecycle: maturing)
   schema,
   index_column_names = c("soma_joinid"),
   platform_config = NULL,
+  timestamps = NULL,
   internal_use_only = NULL
 )}\if{html}{\out{</div>}}
 }
@@ -79,6 +80,8 @@ one index column name is required.}
 \item{\code{platform_config}}{A \link[tiledbsoma:PlatformConfig]{platform configuration}
 object}
 
+\item{\code{timestamps}}{Optional timestamp start and end range}
+
 \item{\code{internal_use_only}}{Character value to signal this is a 'permitted' call,
 as \code{create()} is considered internal and should not be called directly.}
 }
@@ -91,7 +94,7 @@ as \code{create()} is considered internal and should not be called directly.}
 \subsection{Method \code{write()}}{
 Write (lifecycle: maturing)
 \subsection{Usage}{
-\if{html}{\out{<div class="r">}}\preformatted{SOMADataFrame$write(values)}\if{html}{\out{</div>}}
+\if{html}{\out{<div class="r">}}\preformatted{SOMADataFrame$write(values, tsrange = NULL)}\if{html}{\out{</div>}}
 }
 
 \subsection{Arguments}{
@@ -100,6 +103,9 @@ Write (lifecycle: maturing)
 \item{\code{values}}{An \code{\link[arrow:Table-class]{arrow::Table}} or \code{\link[arrow:RecordBatch-class]{arrow::RecordBatch}}
 containing all columns, including any index columns. The
 schema for \code{values} must match the schema for the \code{SOMADataFrame}.}
+
+\item{\code{tsrange}}{An optional two-element Datetime vector for the
+start and end of the timestamp range}
 }
 \if{html}{\out{</div>}}
 }
diff --git a/apis/r/man/SOMANDArrayBase.Rd b/apis/r/man/SOMANDArrayBase.Rd
index 3e28c56708..674d0871e0 100644
--- a/apis/r/man/SOMANDArrayBase.Rd
+++ b/apis/r/man/SOMANDArrayBase.Rd
@@ -61,6 +61,7 @@ experimental)
   type,
   shape,
   platform_config = NULL,
+  timestamps = NULL,
   internal_use_only = NULL
 )}\if{html}{\out{</div>}}
 }
@@ -76,6 +77,8 @@ element in the array.}
 \item{\code{platform_config}}{A \link[tiledbsoma:PlatformConfig]{platform configuration}
 object}
 
+\item{\code{timestamps}}{Optional timestamp start and end range}
+
 \item{\code{internal_use_only}}{Character value to signal this is a 'permitted'
 call, as \code{create()} is considered internal and should not be called
 directly.}
diff --git a/apis/r/man/TileDBArray.Rd b/apis/r/man/TileDBArray.Rd
index a19cf3823a..cb4cab5b3d 100644
--- a/apis/r/man/TileDBArray.Rd
+++ b/apis/r/man/TileDBArray.Rd
@@ -148,13 +148,15 @@ A list of metadata values.
 \subsection{Method \code{set_metadata()}}{
 Add list of metadata to the specified TileDB array. (lifecycle: maturing)
 \subsection{Usage}{
-\if{html}{\out{<div class="r">}}\preformatted{TileDBArray$set_metadata(metadata)}\if{html}{\out{</div>}}
+\if{html}{\out{<div class="r">}}\preformatted{TileDBArray$set_metadata(metadata, timestamps = NULL)}\if{html}{\out{</div>}}
 }
 
 \subsection{Arguments}{
 \if{html}{\out{<div class="arguments">}}
 \describe{
 \item{\code{metadata}}{Named list of metadata to add.}
+
+\item{\code{timestamps}}{Optional two-element vector of timestamp range starts and end}
 }
 \if{html}{\out{</div>}}
 }
diff --git a/apis/r/man/TileDBObject.Rd b/apis/r/man/TileDBObject.Rd
index d17641c4a3..82c7daf2b3 100644
--- a/apis/r/man/TileDBObject.Rd
+++ b/apis/r/man/TileDBObject.Rd
@@ -14,6 +14,8 @@ TileDBGroup classes. (lifecycle: maturing)
 
 \item{\code{tiledbsoma_ctx}}{SOMATileDBContext}
 
+\item{\code{tiledb_timestamp}}{Time that object was opened at}
+
 \item{\code{uri}}{The URI of the TileDB object.}
 }
 \if{html}{\out{</div>}}
@@ -106,7 +108,7 @@ otherwise returns the mode (eg. \dQuote{\code{READ}}) of the object
 \subsection{Method \code{reopen()}}{
 Close and reopen the TileDB object in a new mode
 \subsection{Usage}{
-\if{html}{\out{<div class="r">}}\preformatted{TileDBObject$reopen(mode)}\if{html}{\out{</div>}}
+\if{html}{\out{<div class="r">}}\preformatted{TileDBObject$reopen(mode, tiledb_timestamp = NULL)}\if{html}{\out{</div>}}
 }
 
 \subsection{Arguments}{
@@ -117,6 +119,8 @@ Close and reopen the TileDB object in a new mode
 \item \dQuote{\code{READ}}
 \item \dQuote{\code{WRITE}}
 }}
+
+\item{\code{tiledb_timestamp}}{Optional Datetime (POSIXct) with TileDB timestamp}
 }
 \if{html}{\out{</div>}}
 }
diff --git a/apis/r/man/delete_metadata.Rd b/apis/r/man/delete_metadata.Rd
index 3d04e0cdad..06b7c33be8 100644
--- a/apis/r/man/delete_metadata.Rd
+++ b/apis/r/man/delete_metadata.Rd
@@ -4,7 +4,7 @@
 \alias{delete_metadata}
 \title{Delete metadata for given key}
 \usage{
-delete_metadata(uri, key, ctxxp)
+delete_metadata(uri, key, is_array, ctxxp)
 }
 \arguments{
 \item{uri}{The array URI}
diff --git a/apis/r/man/get_all_metadata.Rd b/apis/r/man/get_all_metadata.Rd
index 6b172779d6..5099ae20f6 100644
--- a/apis/r/man/get_all_metadata.Rd
+++ b/apis/r/man/get_all_metadata.Rd
@@ -2,9 +2,9 @@
 % Please edit documentation in R/RcppExports.R
 \name{get_all_metadata}
 \alias{get_all_metadata}
-\title{Read all metadata (as named character vector)}
+\title{Read all metadata (as named list)}
 \usage{
-get_all_metadata(uri, ctxxp)
+get_all_metadata(uri, is_array, ctxxp)
 }
 \arguments{
 \item{uri}{The array URI}
@@ -12,6 +12,6 @@ get_all_metadata(uri, ctxxp)
 \item{ctxxp}{An external pointer to the SOMAContext wrapper}
 }
 \description{
-This function assumes that all metadata is in fact stored as strings. It will error
-if a different datatype is encountered.
+This function currently supports metadata as either a string or an 'int64' (or 'int32').
+It will error if a different datatype is encountered.
 }
diff --git a/apis/r/man/get_metadata.Rd b/apis/r/man/get_metadata.Rd
index 8b11e1a6aa..f49299d5fb 100644
--- a/apis/r/man/get_metadata.Rd
+++ b/apis/r/man/get_metadata.Rd
@@ -4,7 +4,7 @@
 \alias{get_metadata}
 \title{Read metadata (as a string)}
 \usage{
-get_metadata(uri, key, ctxxp)
+get_metadata(uri, key, is_array, ctxxp)
 }
 \arguments{
 \item{uri}{The array URI}
diff --git a/apis/r/man/get_metadata_num.Rd b/apis/r/man/get_metadata_num.Rd
index c34c909e02..baaadb4fe4 100644
--- a/apis/r/man/get_metadata_num.Rd
+++ b/apis/r/man/get_metadata_num.Rd
@@ -4,7 +4,7 @@
 \alias{get_metadata_num}
 \title{Get nnumber of metadata items}
 \usage{
-get_metadata_num(uri, ctxxp)
+get_metadata_num(uri, is_array, ctxxp)
 }
 \arguments{
 \item{uri}{The array URI}
diff --git a/apis/r/man/has_metadata.Rd b/apis/r/man/has_metadata.Rd
index 9354b59649..8abc90e33b 100644
--- a/apis/r/man/has_metadata.Rd
+++ b/apis/r/man/has_metadata.Rd
@@ -4,7 +4,7 @@
 \alias{has_metadata}
 \title{Check for metadata given key}
 \usage{
-has_metadata(uri, key, ctxxp)
+has_metadata(uri, key, is_array, ctxxp)
 }
 \arguments{
 \item{uri}{The array URI}
diff --git a/apis/r/man/set_metadata.Rd b/apis/r/man/set_metadata.Rd
index ee3cedc3a1..d280d3587e 100644
--- a/apis/r/man/set_metadata.Rd
+++ b/apis/r/man/set_metadata.Rd
@@ -4,16 +4,22 @@
 \alias{set_metadata}
 \title{Set metadata (as a string)}
 \usage{
-set_metadata(uri, key, value, ctxxp)
+set_metadata(uri, key, valuesxp, type, is_array, ctxxp, tsvec = NULL)
 }
 \arguments{
 \item{uri}{The array URI}
 
 \item{key}{The array metadata key}
 
-\item{value}{The metadata value}
+\item{valuesxp}{The metadata value}
+
+\item{type}{The datatype}
+
+\item{is_array}{A boolean to indicate array or group}
 
 \item{ctxxp}{An external pointer to the SOMAContext wrapper}
+
+\item{tsvec}{An optional two-element datetime vector}
 }
 \description{
 Set metadata (as a string)
diff --git a/apis/r/src/RcppExports.cpp b/apis/r/src/RcppExports.cpp
index 70137b6120..4021362a23 100644
--- a/apis/r/src/RcppExports.cpp
+++ b/apis/r/src/RcppExports.cpp
@@ -23,8 +23,8 @@ BEGIN_RCPP
 END_RCPP
 }
 // createSchemaFromArrow
-void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray nadimap, naxpSchema nadimsp, bool sparse, std::string datatype, Rcpp::List pclst, Rcpp::XPtr<somactx_wrap_t> ctxxp);
-RcppExport SEXP _tiledbsoma_createSchemaFromArrow(SEXP uriSEXP, SEXP naspSEXP, SEXP nadimapSEXP, SEXP nadimspSEXP, SEXP sparseSEXP, SEXP datatypeSEXP, SEXP pclstSEXP, SEXP ctxxpSEXP) {
+void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray nadimap, naxpSchema nadimsp, bool sparse, std::string datatype, Rcpp::List pclst, Rcpp::XPtr<somactx_wrap_t> ctxxp, Rcpp::Nullable<Rcpp::DatetimeVector> tsvec);
+RcppExport SEXP _tiledbsoma_createSchemaFromArrow(SEXP uriSEXP, SEXP naspSEXP, SEXP nadimapSEXP, SEXP nadimspSEXP, SEXP sparseSEXP, SEXP datatypeSEXP, SEXP pclstSEXP, SEXP ctxxpSEXP, SEXP tsvecSEXP) {
 BEGIN_RCPP
     Rcpp::RNGScope rcpp_rngScope_gen;
     Rcpp::traits::input_parameter< const std::string& >::type uri(uriSEXP);
@@ -35,13 +35,14 @@ BEGIN_RCPP
     Rcpp::traits::input_parameter< std::string >::type datatype(datatypeSEXP);
     Rcpp::traits::input_parameter< Rcpp::List >::type pclst(pclstSEXP);
     Rcpp::traits::input_parameter< Rcpp::XPtr<somactx_wrap_t> >::type ctxxp(ctxxpSEXP);
-    createSchemaFromArrow(uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp);
+    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::DatetimeVector> >::type tsvec(tsvecSEXP);
+    createSchemaFromArrow(uri, nasp, nadimap, nadimsp, sparse, datatype, pclst, ctxxp, tsvec);
     return R_NilValue;
 END_RCPP
 }
 // writeArrayFromArrow
-void writeArrayFromArrow(const std::string& uri, naxpArray naap, naxpSchema nasp, const std::string arraytype, Rcpp::Nullable<Rcpp::CharacterVector> config);
-RcppExport SEXP _tiledbsoma_writeArrayFromArrow(SEXP uriSEXP, SEXP naapSEXP, SEXP naspSEXP, SEXP arraytypeSEXP, SEXP configSEXP) {
+void writeArrayFromArrow(const std::string& uri, naxpArray naap, naxpSchema nasp, const std::string arraytype, Rcpp::Nullable<Rcpp::CharacterVector> config, Rcpp::Nullable<Rcpp::DatetimeVector> tsvec);
+RcppExport SEXP _tiledbsoma_writeArrayFromArrow(SEXP uriSEXP, SEXP naapSEXP, SEXP naspSEXP, SEXP arraytypeSEXP, SEXP configSEXP, SEXP tsvecSEXP) {
 BEGIN_RCPP
     Rcpp::RNGScope rcpp_rngScope_gen;
     Rcpp::traits::input_parameter< const std::string& >::type uri(uriSEXP);
@@ -49,7 +50,8 @@ BEGIN_RCPP
     Rcpp::traits::input_parameter< naxpSchema >::type nasp(naspSEXP);
     Rcpp::traits::input_parameter< const std::string >::type arraytype(arraytypeSEXP);
     Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::CharacterVector> >::type config(configSEXP);
-    writeArrayFromArrow(uri, naap, nasp, arraytype, config);
+    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::DatetimeVector> >::type tsvec(tsvecSEXP);
+    writeArrayFromArrow(uri, naap, nasp, arraytype, config, tsvec);
     return R_NilValue;
 END_RCPP
 }
@@ -121,8 +123,8 @@ BEGIN_RCPP
 END_RCPP
 }
 // set_metadata
-void set_metadata(std::string& uri, std::string& key, SEXP valuesxp, std::string& type, bool is_array, Rcpp::XPtr<somactx_wrap_t> ctxxp);
-RcppExport SEXP _tiledbsoma_set_metadata(SEXP uriSEXP, SEXP keySEXP, SEXP valuesxpSEXP, SEXP typeSEXP, SEXP is_arraySEXP, SEXP ctxxpSEXP) {
+void set_metadata(std::string& uri, std::string& key, SEXP valuesxp, std::string& type, bool is_array, Rcpp::XPtr<somactx_wrap_t> ctxxp, Rcpp::Nullable<Rcpp::DatetimeVector> tsvec);
+RcppExport SEXP _tiledbsoma_set_metadata(SEXP uriSEXP, SEXP keySEXP, SEXP valuesxpSEXP, SEXP typeSEXP, SEXP is_arraySEXP, SEXP ctxxpSEXP, SEXP tsvecSEXP) {
 BEGIN_RCPP
     Rcpp::RNGScope rcpp_rngScope_gen;
     Rcpp::traits::input_parameter< std::string& >::type uri(uriSEXP);
@@ -131,7 +133,8 @@ BEGIN_RCPP
     Rcpp::traits::input_parameter< std::string& >::type type(typeSEXP);
     Rcpp::traits::input_parameter< bool >::type is_array(is_arraySEXP);
     Rcpp::traits::input_parameter< Rcpp::XPtr<somactx_wrap_t> >::type ctxxp(ctxxpSEXP);
-    set_metadata(uri, key, valuesxp, type, is_array, ctxxp);
+    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::DatetimeVector> >::type tsvec(tsvecSEXP);
+    set_metadata(uri, key, valuesxp, type, is_array, ctxxp, tsvec);
     return R_NilValue;
 END_RCPP
 }
@@ -170,8 +173,8 @@ BEGIN_RCPP
 END_RCPP
 }
 // soma_array_reader
-SEXP soma_array_reader(const std::string& uri, Rcpp::Nullable<Rcpp::CharacterVector> colnames, Rcpp::Nullable<Rcpp::XPtr<tiledb::QueryCondition>> qc, Rcpp::Nullable<Rcpp::List> dim_points, Rcpp::Nullable<Rcpp::List> dim_ranges, std::string batch_size, std::string result_order, const std::string& loglevel, Rcpp::Nullable<Rcpp::CharacterVector> config);
-RcppExport SEXP _tiledbsoma_soma_array_reader(SEXP uriSEXP, SEXP colnamesSEXP, SEXP qcSEXP, SEXP dim_pointsSEXP, SEXP dim_rangesSEXP, SEXP batch_sizeSEXP, SEXP result_orderSEXP, SEXP loglevelSEXP, SEXP configSEXP) {
+SEXP soma_array_reader(const std::string& uri, Rcpp::Nullable<Rcpp::CharacterVector> colnames, Rcpp::Nullable<Rcpp::XPtr<tiledb::QueryCondition>> qc, Rcpp::Nullable<Rcpp::List> dim_points, Rcpp::Nullable<Rcpp::List> dim_ranges, std::string batch_size, std::string result_order, const std::string& loglevel, Rcpp::Nullable<Rcpp::CharacterVector> config, Rcpp::Nullable<Rcpp::DatetimeVector> timestamprange);
+RcppExport SEXP _tiledbsoma_soma_array_reader(SEXP uriSEXP, SEXP colnamesSEXP, SEXP qcSEXP, SEXP dim_pointsSEXP, SEXP dim_rangesSEXP, SEXP batch_sizeSEXP, SEXP result_orderSEXP, SEXP loglevelSEXP, SEXP configSEXP, SEXP timestamprangeSEXP) {
 BEGIN_RCPP
     Rcpp::RObject rcpp_result_gen;
     Rcpp::RNGScope rcpp_rngScope_gen;
@@ -184,7 +187,8 @@ BEGIN_RCPP
     Rcpp::traits::input_parameter< std::string >::type result_order(result_orderSEXP);
     Rcpp::traits::input_parameter< const std::string& >::type loglevel(loglevelSEXP);
     Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::CharacterVector> >::type config(configSEXP);
-    rcpp_result_gen = Rcpp::wrap(soma_array_reader(uri, colnames, qc, dim_points, dim_ranges, batch_size, result_order, loglevel, config));
+    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::DatetimeVector> >::type timestamprange(timestamprangeSEXP);
+    rcpp_result_gen = Rcpp::wrap(soma_array_reader(uri, colnames, qc, dim_points, dim_ranges, batch_size, result_order, loglevel, config, timestamprange));
     return rcpp_result_gen;
 END_RCPP
 }
@@ -257,8 +261,8 @@ BEGIN_RCPP
 END_RCPP
 }
 // sr_setup
-Rcpp::List sr_setup(const std::string& uri, Rcpp::CharacterVector config, Rcpp::Nullable<Rcpp::CharacterVector> colnames, Rcpp::Nullable<Rcpp::XPtr<tiledb::QueryCondition>> qc, Rcpp::Nullable<Rcpp::List> dim_points, Rcpp::Nullable<Rcpp::List> dim_ranges, std::string batch_size, std::string result_order, Rcpp::Nullable<Rcpp::Datetime> timestamp_end, const std::string& loglevel);
-RcppExport SEXP _tiledbsoma_sr_setup(SEXP uriSEXP, SEXP configSEXP, SEXP colnamesSEXP, SEXP qcSEXP, SEXP dim_pointsSEXP, SEXP dim_rangesSEXP, SEXP batch_sizeSEXP, SEXP result_orderSEXP, SEXP timestamp_endSEXP, SEXP loglevelSEXP) {
+Rcpp::List sr_setup(const std::string& uri, Rcpp::CharacterVector config, Rcpp::Nullable<Rcpp::CharacterVector> colnames, Rcpp::Nullable<Rcpp::XPtr<tiledb::QueryCondition>> qc, Rcpp::Nullable<Rcpp::List> dim_points, Rcpp::Nullable<Rcpp::List> dim_ranges, std::string batch_size, std::string result_order, Rcpp::Nullable<Rcpp::DatetimeVector> timestamprange, const std::string& loglevel);
+RcppExport SEXP _tiledbsoma_sr_setup(SEXP uriSEXP, SEXP configSEXP, SEXP colnamesSEXP, SEXP qcSEXP, SEXP dim_pointsSEXP, SEXP dim_rangesSEXP, SEXP batch_sizeSEXP, SEXP result_orderSEXP, SEXP timestamprangeSEXP, SEXP loglevelSEXP) {
 BEGIN_RCPP
     Rcpp::RObject rcpp_result_gen;
     Rcpp::RNGScope rcpp_rngScope_gen;
@@ -270,9 +274,9 @@ BEGIN_RCPP
     Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::List> >::type dim_ranges(dim_rangesSEXP);
     Rcpp::traits::input_parameter< std::string >::type batch_size(batch_sizeSEXP);
     Rcpp::traits::input_parameter< std::string >::type result_order(result_orderSEXP);
-    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::Datetime> >::type timestamp_end(timestamp_endSEXP);
+    Rcpp::traits::input_parameter< Rcpp::Nullable<Rcpp::DatetimeVector> >::type timestamprange(timestamprangeSEXP);
     Rcpp::traits::input_parameter< const std::string& >::type loglevel(loglevelSEXP);
-    rcpp_result_gen = Rcpp::wrap(sr_setup(uri, config, colnames, qc, dim_points, dim_ranges, batch_size, result_order, timestamp_end, loglevel));
+    rcpp_result_gen = Rcpp::wrap(sr_setup(uri, config, colnames, qc, dim_points, dim_ranges, batch_size, result_order, timestamprange, loglevel));
     return rcpp_result_gen;
 END_RCPP
 }
@@ -427,18 +431,18 @@ END_RCPP
 
 static const R_CallMethodDef CallEntries[] = {
     {"_tiledbsoma_createSOMAContext", (DL_FUNC) &_tiledbsoma_createSOMAContext, 1},
-    {"_tiledbsoma_createSchemaFromArrow", (DL_FUNC) &_tiledbsoma_createSchemaFromArrow, 8},
-    {"_tiledbsoma_writeArrayFromArrow", (DL_FUNC) &_tiledbsoma_writeArrayFromArrow, 5},
+    {"_tiledbsoma_createSchemaFromArrow", (DL_FUNC) &_tiledbsoma_createSchemaFromArrow, 9},
+    {"_tiledbsoma_writeArrayFromArrow", (DL_FUNC) &_tiledbsoma_writeArrayFromArrow, 6},
     {"_tiledbsoma_get_metadata_num", (DL_FUNC) &_tiledbsoma_get_metadata_num, 3},
     {"_tiledbsoma_get_all_metadata", (DL_FUNC) &_tiledbsoma_get_all_metadata, 3},
     {"_tiledbsoma_get_metadata", (DL_FUNC) &_tiledbsoma_get_metadata, 4},
     {"_tiledbsoma_has_metadata", (DL_FUNC) &_tiledbsoma_has_metadata, 4},
     {"_tiledbsoma_delete_metadata", (DL_FUNC) &_tiledbsoma_delete_metadata, 4},
-    {"_tiledbsoma_set_metadata", (DL_FUNC) &_tiledbsoma_set_metadata, 6},
+    {"_tiledbsoma_set_metadata", (DL_FUNC) &_tiledbsoma_set_metadata, 7},
     {"_tiledbsoma_reindex_create", (DL_FUNC) &_tiledbsoma_reindex_create, 0},
     {"_tiledbsoma_reindex_map", (DL_FUNC) &_tiledbsoma_reindex_map, 2},
     {"_tiledbsoma_reindex_lookup", (DL_FUNC) &_tiledbsoma_reindex_lookup, 2},
-    {"_tiledbsoma_soma_array_reader", (DL_FUNC) &_tiledbsoma_soma_array_reader, 9},
+    {"_tiledbsoma_soma_array_reader", (DL_FUNC) &_tiledbsoma_soma_array_reader, 10},
     {"_tiledbsoma_set_log_level", (DL_FUNC) &_tiledbsoma_set_log_level, 1},
     {"_tiledbsoma_get_column_types", (DL_FUNC) &_tiledbsoma_get_column_types, 2},
     {"_tiledbsoma_nnz", (DL_FUNC) &_tiledbsoma_nnz, 2},
diff --git a/apis/r/src/arrow.cpp b/apis/r/src/arrow.cpp
index ee3707a763..b7fed2e4be 100644
--- a/apis/r/src/arrow.cpp
+++ b/apis/r/src/arrow.cpp
@@ -58,8 +58,8 @@ Rcpp::XPtr<somactx_wrap_t> createSOMAContext(Rcpp::Nullable<Rcpp::CharacterVecto
 
 // [[Rcpp::export]]
 void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray nadimap, naxpSchema nadimsp,
-                           bool sparse, std::string datatype, Rcpp::List pclst, Rcpp::XPtr<somactx_wrap_t> ctxxp) {
-
+                           bool sparse, std::string datatype, Rcpp::List pclst, Rcpp::XPtr<somactx_wrap_t> ctxxp,
+                           Rcpp::Nullable<Rcpp::DatetimeVector> tsvec = R_NilValue) {
 
     //struct ArrowArray* ap = (struct ArrowArray*) R_ExternalPtrAddr(naap);
     //struct ArrowSchema* sp = (struct ArrowSchema*) R_ExternalPtrAddr(nasp);
@@ -104,6 +104,9 @@ void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray na
     // shared pointer to TileDB Context from SOMAContext
     std::shared_ptr<tiledb::Context> ctx = sctx->tiledb_ctx();
 
+    // optional timestamp range
+    std::optional<tdbs::TimestampRange> tsrng = makeTimestampRange(tsvec);
+
     bool exists = false;
     if (datatype == "SOMADataFrame") {
         exists = tdbs::SOMADataFrame::exists(uri, sctx);
@@ -122,19 +125,19 @@ void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray na
     if (datatype == "SOMADataFrame") {
         tdbs::SOMADataFrame::create(uri, std::move(schema),
                                     std::pair(std::move(dimarr), std::move(dimsch)),
-                                    sctx, pltcfg);
+                                    sctx, pltcfg, tsrng);
     } else if (datatype == "SOMASparseNDArray") {
         // for arrays n_children will be three as we have two dims and a data col
         std::string datacoltype = sp->children[sp->n_children-1]->format;
         tdbs::SOMASparseNDArray::create(uri, datacoltype,
                                         std::pair(std::move(dimarr), std::move(dimsch)),
-                                        sctx, pltcfg);
+                                        sctx, pltcfg, tsrng);
     } else if (datatype == "SOMADenseNDArray") {
         // for arrays n_children will be three as we have two dims and a data col
         std::string datacoltype = sp->children[sp->n_children-1]->format;
         tdbs::SOMADenseNDArray::create(uri, datacoltype,
                                        std::pair(std::move(dimarr), std::move(dimsch)),
-                                       sctx, pltcfg);
+                                       sctx, pltcfg, tsrng);
     } else {
         Rcpp::stop(tfm::format("Error: Invalid SOMA type_argument '%s'", datatype));
     }
@@ -145,7 +148,8 @@ void createSchemaFromArrow(const std::string& uri, naxpSchema nasp, naxpArray na
 // [[Rcpp::export]]
 void writeArrayFromArrow(const std::string& uri, naxpArray naap, naxpSchema nasp,
                          const std::string arraytype = "",
-                         Rcpp::Nullable<Rcpp::CharacterVector> config = R_NilValue) {
+                         Rcpp::Nullable<Rcpp::CharacterVector> config = R_NilValue,
+                         Rcpp::Nullable<Rcpp::DatetimeVector> tsvec = R_NilValue) {
 
     //struct ArrowArray* ap = (struct ArrowArray*) R_ExternalPtrAddr(naap);
     //struct ArrowSchema* sp = (struct ArrowSchema*) R_ExternalPtrAddr(nasp);
@@ -181,14 +185,19 @@ void writeArrayFromArrow(const std::string& uri, naxpArray naap, naxpSchema nasp
         somactx = std::make_shared<tdbs::SOMAContext>();
     }
 
+    // optional timestamp range
+    std::optional<tdbs::TimestampRange> tsrng = makeTimestampRange(tsvec);
+
     std::shared_ptr<tdbs::SOMAArray> arrup;
     if (arraytype == "SOMADataFrame") {
-        arrup = tdbs::SOMADataFrame::open(OpenMode::write, uri, somactx);
+        arrup = tdbs::SOMADataFrame::open(OpenMode::write, uri, somactx, "unnamed", {},
+                                          "auto", ResultOrder::automatic, tsrng);
     } else if (arraytype == "SOMADenseNDArray") {
-        arrup = tdbs::SOMADenseNDArray::open(OpenMode::write, uri, somactx,
-                                             "unnamed", {}, "auto", ResultOrder::colmajor);
+        arrup = tdbs::SOMADenseNDArray::open(OpenMode::write, uri, somactx, "unnamed", {},
+                                             "auto", ResultOrder::colmajor, tsrng);
     } else if (arraytype == "SOMASparseNDArray") {
-        arrup = tdbs::SOMASparseNDArray::open(OpenMode::write, uri, somactx);
+        arrup = tdbs::SOMASparseNDArray::open(OpenMode::write, uri, somactx, "unnamed", {},
+                                              "auto", ResultOrder::automatic, tsrng);
     } else {  // not reached
         Rcpp::stop(tfm::format("Unexpected array type '%s'", arraytype));
     }
diff --git a/apis/r/src/metadata.cpp b/apis/r/src/metadata.cpp
index 780d023209..53c6c54d5a 100644
--- a/apis/r/src/metadata.cpp
+++ b/apis/r/src/metadata.cpp
@@ -13,11 +13,17 @@ namespace tdbs = tiledbsoma;
 
 std::unique_ptr<tdbs::SOMAObject> getObjectUniquePointer(bool is_array, OpenMode mode,
                                                          std::string& uri,
-                                                         std::shared_ptr<tdbs::SOMAContext> ctx) {
+                                                         std::shared_ptr<tdbs::SOMAContext> ctx,
+                                                         Rcpp::Nullable<Rcpp::DatetimeVector> tsvec = R_NilValue) {
+
+    // optional timestamp range
+    std::optional<tdbs::TimestampRange> tsrng = makeTimestampRange(tsvec);
+
     if (is_array) {
-        return tdbs::SOMAArray::open(mode, uri, ctx);
+        return tdbs::SOMAArray::open(mode, uri, ctx, "unnamed", {}, "auto",
+                                     ResultOrder::automatic, tsrng);
     } else {
-        return tdbs::SOMAGroup::open(mode, uri, ctx);
+        return tdbs::SOMAGroup::open(mode, uri, ctx, "unnamed", tsrng);
     }
 }
 
@@ -149,17 +155,20 @@ void delete_metadata(std::string& uri, std::string& key, bool is_array,
 //'
 //' @param uri The array URI
 //' @param key The array metadata key
-//' @param value The metadata value
-//' @
+//' @param valuesxp The metadata value
+//' @param type The datatype
+//' @param is_array A boolean to indicate array or group
 //' @param ctxxp An external pointer to the SOMAContext wrapper
+//' @param tsvec An optional two-element datetime vector
 //' @export
 // [[Rcpp::export]]
 void set_metadata(std::string& uri, std::string& key, SEXP valuesxp, std::string& type,
-                  bool is_array, Rcpp::XPtr<somactx_wrap_t> ctxxp) {
+                  bool is_array, Rcpp::XPtr<somactx_wrap_t> ctxxp,
+                  Rcpp::Nullable<Rcpp::DatetimeVector> tsvec = R_NilValue) {
     // shared pointer to SOMAContext from external pointer wrapper
     std::shared_ptr<tdbs::SOMAContext> sctx = ctxxp->ctxptr;
     // SOMA Object unique pointer (aka soup)
-    auto soup = getObjectUniquePointer(is_array, OpenMode::write, uri, sctx);
+    auto soup = getObjectUniquePointer(is_array, OpenMode::write, uri, sctx, tsvec);
 
     if (type == "character") {
         const tiledb_datatype_t value_type = TILEDB_STRING_UTF8;
diff --git a/apis/r/src/rinterface.cpp b/apis/r/src/rinterface.cpp
index df107980c8..d5a919c7cc 100644
--- a/apis/r/src/rinterface.cpp
+++ b/apis/r/src/rinterface.cpp
@@ -54,7 +54,8 @@ SEXP soma_array_reader(const std::string& uri,
                        std::string batch_size = "auto",
                        std::string result_order = "auto",
                        const std::string& loglevel = "auto",
-                       Rcpp::Nullable<Rcpp::CharacterVector> config = R_NilValue) {
+                       Rcpp::Nullable<Rcpp::CharacterVector> config = R_NilValue,
+                       Rcpp::Nullable<Rcpp::DatetimeVector> timestamprange = R_NilValue) {
 
     if (loglevel != "auto") {
         spdl::set_level(loglevel);
@@ -75,6 +76,13 @@ SEXP soma_array_reader(const std::string& uri,
 
     auto tdb_result_order = get_tdb_result_order(result_order);
 
+    // optional timestamp range
+    std::optional<tdbs::TimestampRange> tsrng = makeTimestampRange(timestamprange);
+    if (timestamprange.isNotNull()) {
+        Rcpp::DatetimeVector vec(timestamprange);
+        spdl::debug("[soma_array_reader] timestamprange ({},{})", vec[0], vec[1]);
+    }
+
     // Read selected columns from the uri (return is unique_ptr<SOMAArray>)
     auto sr = tdbs::SOMAArray::open(OpenMode::read,
                                     uri,
@@ -82,7 +90,8 @@ SEXP soma_array_reader(const std::string& uri,
                                     platform_config,
                                     column_names,
                                     batch_size,
-                                    tdb_result_order);
+                                    tdb_result_order,
+                                    tsrng);
 
     std::unordered_map<std::string, std::shared_ptr<tiledb::Dimension>> name2dim;
     std::shared_ptr<tiledb::ArraySchema> schema = sr->tiledb_schema();
@@ -97,7 +106,7 @@ SEXP soma_array_reader(const std::string& uri,
 
     // If we have a query condition, apply it
     if (!qc.isNull()) {
-        spdl::info("[soma_array_reader] Applying query condition");
+        spdl::info("[soma_array_reader_impl] Applying query condition");
         Rcpp::XPtr<tiledb::QueryCondition> qcxp(qc);
         sr->set_condition(*qcxp);
     }
@@ -142,9 +151,8 @@ SEXP soma_array_reader(const std::string& uri,
     exitIfError(ArrowArrayAllocateChildren(arr, ncol), "Bad array children alloc");
 
     arr->length = 0;             // initial value
-
     for (size_t i=0; i<ncol; i++) {
-        spdl::info("[soma_array_reader] Accessing {} at {}", names[i], i);
+        spdl::info("[soma_array_reader] Accessing '{}' at pos {}", names[i], i);
 
         // now buf is a shared_ptr to ColumnBuffer
         auto buf = sr_data->get()->at(names[i]);
@@ -179,7 +187,7 @@ SEXP soma_array_reader(const std::string& uri,
 //' @export
 // [[Rcpp::export]]
 void set_log_level(const std::string& level) {
-    spdl::set_level(level);
+    spdl::setup("R", level);
     tdbs::LOG_SET_LEVEL(level);
 }
 
diff --git a/apis/r/src/riterator.cpp b/apis/r/src/riterator.cpp
index 40a1d8b6ce..130730774b 100644
--- a/apis/r/src/riterator.cpp
+++ b/apis/r/src/riterator.cpp
@@ -44,8 +44,8 @@ namespace tdbs = tiledbsoma;
 //' @param result_order Optional argument for query result order, defaults to \sQuote{auto}
 //' @param loglevel Character value with the desired logging level, defaults to \sQuote{auto}
 //' which lets prior setting prevail, any other value is set as new logging level.
-//' @param timestamp_end Optional POSIXct (i.e. Datetime) type for end of interval for which
-//' data is considered.
+//' @param timestamprange Optional POSIXct (i.e. Datetime) vector with start and end of
+//' interval for which data is considered.
 //' @param sr An external pointer to a TileDB SOMAArray object
 //'
 //' @return \code{sr_setup} returns an external pointer to a SOMAArray. \code{sr_complete}
@@ -74,7 +74,7 @@ Rcpp::List sr_setup(const std::string& uri,
                     Rcpp::Nullable<Rcpp::List> dim_ranges = R_NilValue,
                     std::string batch_size = "auto",
                     std::string result_order = "auto",
-                    Rcpp::Nullable<Rcpp::Datetime> timestamp_end = R_NilValue,
+                    Rcpp::Nullable<Rcpp::DatetimeVector> timestamprange = R_NilValue,
                     const std::string& loglevel = "auto") {
 
     if (loglevel != "auto") {
@@ -87,7 +87,6 @@ Rcpp::List sr_setup(const std::string& uri,
     std::string_view name = "unnamed";
     std::vector<std::string> column_names = {};
 
-
     std::map<std::string, std::string> platform_config = config_vector_to_map(Rcpp::wrap(config));
     tiledb::Config cfg(platform_config);
     spdl::debug("[sr_setup] creating ctx object with supplied config");
@@ -100,18 +99,14 @@ Rcpp::List sr_setup(const std::string& uri,
         column_names = Rcpp::as<std::vector<std::string>>(colnames);
     }
 
-    std::uint64_t ts_start = 0; 		// beginning of time aka the epoch (force double signature)
-    std::uint64_t ts_end = std::numeric_limits<uint64_t>::max(); 	// max if unset
-    if (!timestamp_end.isNull()) {
-        ts_end = Rcpp::as<Rcpp::Datetime>(timestamp_end).getFractionalTimestamp() * 1e3; // in msec
-        spdl::info(tfm::format("[sr_setup] ts_end set to %ld", ts_end));
-    }
+    // optional timestamp range
+    std::optional<tdbs::TimestampRange> tsrng = makeTimestampRange(timestamprange);
 
     auto tdb_result_order = get_tdb_result_order(result_order);
 
     auto ptr = new tdbs::SOMAArray(OpenMode::read, uri, name, platform_config,
                                    column_names, batch_size,
-                                   tdb_result_order, std::make_pair(ts_start, ts_end));
+                                   tdb_result_order, tsrng);
 
     std::unordered_map<std::string, std::shared_ptr<tiledb::Dimension>> name2dim;
     std::shared_ptr<tiledb::ArraySchema> schema = ptr->tiledb_schema();
diff --git a/apis/r/src/rutilities.cpp b/apis/r/src/rutilities.cpp
index df9e560b9a..ee2dfc205e 100644
--- a/apis/r/src/rutilities.cpp
+++ b/apis/r/src/rutilities.cpp
@@ -292,3 +292,26 @@ size_t tiledb_datatype_max_value(const std::string& datatype) {
     else if (datatype == "UINT64") return std::numeric_limits<uint64_t>::max();
     else Rcpp::stop("currently unsupported datatype (%s)", datatype);
 }
+
+// Make (optional) TimestampRange from (nullable, two-element) Rcpp::DatetimeVector
+std::optional<tdbs::TimestampRange> makeTimestampRange(Rcpp::Nullable<Rcpp::DatetimeVector> tsvec) {
+
+    // optional timestamp, defaults to 'none' aka std::nullopt
+    std::optional<tdbs::TimestampRange> tsrng = std::nullopt;
+
+    if (tsvec.isNotNull()) {
+        // an Rcpp 'Nullable' is a decent compromise between adhering to SEXP semantics
+        // and having 'optional' behaviour -- but when there is a value we need to be explicit
+        Rcpp::DatetimeVector vec(tsvec); // vector of Rcpp::Datetime ie POSIXct w/ (fract.) secs since epoch
+        if (vec.size() == 1) {
+            tsrng = std::make_pair<uint64_t>( 0, static_cast<uint64_t>(Rcpp::Datetime(vec[0]).getFractionalTimestamp() * 1000) );
+        } else if (vec.size() == 2) {
+        tsrng = std::make_pair<uint64_t>( static_cast<uint64_t>(Rcpp::Datetime(vec[0]).getFractionalTimestamp() * 1000),
+                                          static_cast<uint64_t>(Rcpp::Datetime(vec[1]).getFractionalTimestamp() * 1000) );
+        } else {
+            Rcpp::stop("TimestampRange must be a one or two-element vector");
+        }
+    }
+
+    return tsrng;
+}
diff --git a/apis/r/src/rutilities.h b/apis/r/src/rutilities.h
index 664421fd60..ee1483e0eb 100644
--- a/apis/r/src/rutilities.h
+++ b/apis/r/src/rutilities.h
@@ -65,3 +65,6 @@ inline void exitIfError(const ArrowErrorCode ec, const std::string& msg) {
 inline void array_xptr_set_schema(SEXP array_xptr, SEXP schema_xptr) {
     R_SetExternalPtrTag(array_xptr, schema_xptr);
 }
+
+// make a TimestampRange from a DatetimeVector (of size two)
+std::optional<tdbs::TimestampRange> makeTimestampRange(Rcpp::Nullable<Rcpp::DatetimeVector> tsvec);
diff --git a/libtiledbsoma/src/soma/soma_dense_ndarray.cc b/libtiledbsoma/src/soma/soma_dense_ndarray.cc
index ebc72214df..63a16e31d4 100644
--- a/libtiledbsoma/src/soma/soma_dense_ndarray.cc
+++ b/libtiledbsoma/src/soma/soma_dense_ndarray.cc
@@ -73,7 +73,7 @@ void SOMADenseNDArray::create(
     attr->format = strdup(std::string(format).c_str());
     attr->name = strdup("soma_data");
     attr->n_children = 0;
-    attr->flags = 0;
+    attr->flags = 0;  // or ARROW_FLAG_NULLABLE;
     attr->dictionary = nullptr;
     attr->release = &ArrowAdapter::release_schema;
 
diff --git a/libtiledbsoma/src/soma/soma_sparse_ndarray.h b/libtiledbsoma/src/soma/soma_sparse_ndarray.h
index 7017d359cf..d51d83c1f3 100644
--- a/libtiledbsoma/src/soma/soma_sparse_ndarray.h
+++ b/libtiledbsoma/src/soma/soma_sparse_ndarray.h
@@ -5,7 +5,7 @@
  *
  * The MIT License
  *
- * @copyright Copyright (c) 2023 TileDB, Inc.
+ * @copyright Copyright (c) 2023-2024 TileDB, Inc.
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal


Test patch:
diff --git a/apis/r/tests/testthat/test-SOMACollection.R b/apis/r/tests/testthat/test-SOMACollection.R
index 1076ae08ad..08ba221b2e 100644
--- a/apis/r/tests/testthat/test-SOMACollection.R
+++ b/apis/r/tests/testthat/test-SOMACollection.R
@@ -98,7 +98,7 @@ test_that("SOMACollection timestamped ops", {
   collection <- SOMACollectionOpen(uri, tiledb_timestamp = t1)
   expect_true("A" %in% collection$names())
   a <- collection$get("A")$read()$sparse_matrix()$concat()
-  expect_equal(sum(a), 1)
+  ## FIXME(once groups done via C++)  expect_equal(sum(a), 1)
   collection$close()
 
   # open collection @ t0 => A should not even be there
diff --git a/apis/r/tests/testthat/test-SOMASparseNDArray.R b/apis/r/tests/testthat/test-SOMASparseNDArray.R
index 90f307abee..c2b64b494b 100644
--- a/apis/r/tests/testthat/test-SOMASparseNDArray.R
+++ b/apis/r/tests/testthat/test-SOMASparseNDArray.R
@@ -324,15 +324,14 @@ test_that("SOMASparseNDArray timestamped ops", {
   uri <- tempfile(pattern="soma-sparse-nd-array-timestamps")
 
   # t=10: create 2x2 array and write 1 into top-left entry
-  t10 <- Sys.time()
-  snda <- SOMASparseNDArrayCreate(uri=uri, type=arrow::int16(), shape=c(2,2))
+  t10 <- as.POSIXct(10, tz="UTC")
+  snda <- SOMASparseNDArrayCreate(uri=uri, type=arrow::int16(), shape=c(2,2), tiledb_timestamp=t10)
   snda$write(Matrix::sparseMatrix(i = 1, j = 1, x = 1, dims = c(2, 2)))
   snda$close()
-  Sys.sleep(1.0)
 
   # t=20: write 1 into bottom-right entry
-  t20 <- Sys.time()
-  snda <- SOMASparseNDArrayOpen(uri=uri, mode="WRITE")
+  t20 <- as.POSIXct(20, tz="UTC")
+  snda <- SOMASparseNDArrayOpen(uri=uri, mode="WRITE", tiledb_timestamp=t20)
   snda$write(Matrix::sparseMatrix(i = 2, j = 2, x = 1, dims = c(2, 2)))
   snda$close()
 
@@ -342,7 +341,7 @@ test_that("SOMASparseNDArray timestamped ops", {
   snda$close()
 
   # read @ t=15 and see only the first write
-  snda <- SOMASparseNDArrayOpen(uri=uri, tiledb_timestamp = t10 + 0.5*as.numeric(t20 - t10))
+  snda <- SOMASparseNDArrayOpen(uri=uri, tiledb_timestamp = rep(t10 + 0.5*as.numeric(t20 - t10),2))
   expect_equal(sum(snda$read()$sparse_matrix()$concat()), 1)
   snda$close()
 })
diff --git a/apis/r/tests/testthat/test-TileDBArray.R b/apis/r/tests/testthat/test-TileDBArray.R
index ba68f66e26..1ad04efb88 100644
--- a/apis/r/tests/testthat/test-TileDBArray.R
+++ b/apis/r/tests/testthat/test-TileDBArray.R
@@ -5,7 +5,7 @@ test_that("TileDBArray helper functions", {
 
   # Check errors on non-existent array
   expect_error(tdba$object, "Array does not exist.")
-  expect_error(tdba$set_metadata(list(foo = "bar")), "Item must be open for write.")
+  ## inactive under new implementation  expect_error(tdba$set_metadata(list(foo = "bar")), "Item must be open for write.")
 
   # Create an array
   index_cols <- c("Dept", "Gender")
diff --git a/apis/r/tests/testthat/test-Timestamps.R b/apis/r/tests/testthat/test-Timestamps.R
new file mode 100644
index 0000000000..4d29a6df50
--- /dev/null
+++ b/apis/r/tests/testthat/test-Timestamps.R
@@ -0,0 +1,108 @@
+test_that("SOMADataFrame", {
+    uri <- tempfile()
+
+    sch <- arrow::schema(arrow::field("soma_joinid", arrow::int64()),
+                         arrow::field("int", arrow::int32()),
+                         arrow::field("str", arrow::dictionary(index_type = arrow::int8(),
+                                                               value_type = arrow::utf8())))
+
+    ## create at t = 1
+    ts1 <- rep(as.POSIXct(1, tz="UTC"), 2)
+    sdf <- tiledbsoma::SOMADataFrameCreate(uri, sch, tiledb_timestamp=ts1[1])
+
+    ## write part1 at t = 2
+    dat2 <- arrow::arrow_table(soma_joinid = bit64::as.integer64(1L:5L),
+                               int = 101:105L,
+                               str = factor(c('a','b','b','a','b')))
+    ts2 <- rep(as.POSIXct(2, tz="UTC"), 2)
+    expect_silent(sdf$write(dat2, ts2))
+
+    ## write part2 at t = 3
+    dat3 <- arrow::arrow_table(soma_joinid = bit64::as.integer64(6L:10L),
+                               int = 106:110L,
+                               str = factor(c('c','b','c','c','b')))
+    ts3 <- rep(as.POSIXct(3, tz="UTC"), 2)
+    expect_silent(sdf$write(dat3, ts3))
+    sdf$close()
+
+    ## read all
+    sdf <- tiledbsoma::SOMADataFrameOpen(uri)
+    res <- tibble::as_tibble(sdf$read()$concat())
+    expect_equal(dim(res), c(10, 3)) 					# two writes lead to 10x3 data
+    expect_equal(levels(res$str), c("a", "b", "c"))     # string variable has three values
+
+    ## read before data is written
+    sdf <- tiledbsoma::SOMADataFrameOpen(uri, tiledb_timestamp=ts1)
+    res <- sdf$read()$concat()
+    expect_equal(dim(res), c(0, 3))
+
+    ## read at ts2
+    sdf <- tiledbsoma::SOMADataFrameOpen(uri, tiledb_timestamp=ts2)
+    res <- tibble::as_tibble(sdf$read()$concat())
+    expect_equal(dim(res), c(5, 3))
+    expect_equal(max(res$int), 105L)
+    expect_equal(range(res$int), c(101L,105L))
+
+    ## read at ts3
+    sdf <- tiledbsoma::SOMADataFrameOpen(uri, tiledb_timestamp=ts3)
+    res <- tibble::as_tibble(sdf$read()$concat())
+    expect_equal(dim(res), c(5, 3))
+    expect_equal(max(res$int), 110L)
+    expect_equal(range(res$int), c(106L,110L))
+
+    ## read after ts3
+    sdf <- tiledbsoma::SOMADataFrameOpen(uri, tiledb_timestamp=ts3 + c(1,1))
+    res <- tibble::as_tibble(sdf$read()$concat())
+    res <- sdf$read()$concat()
+    expect_equal(dim(res), c(0, 3))
+})
+
+test_that("SOMANDSparseArray", {
+    uri <- tempfile()
+
+    ## create at t = 2, also writes at t = 2 as timestamp is cached
+    ts1 <- rep(as.POSIXct(2, tz="UTC"), 2)
+    snda <- SOMASparseNDArrayCreate(uri, arrow::int32(), shape = c(10, 10), tiledb_timestamp=ts1[1])
+    mat <- create_sparse_matrix_with_int_dims(10, 10)
+    snda$write(mat) 		# write happens at create time
+
+    ## read at t = 3, expect all rows as read is from (0, 3)
+    ts2 <- rep(as.POSIXct(3, tz="UTC"), 2)
+    snda <- tiledbsoma::SOMASparseNDArrayOpen(uri, tiledb_timestamp=ts2)
+    res <- snda$read()$tables()$concat()
+    expect_equal(dim(res), c(60,3))
+
+    ## read at t = 1, expect zero rows as read is from (0, 1)
+    ts3 <- rep(as.POSIXct(1, tz="UTC"), 2)
+    snda <- tiledbsoma::SOMASparseNDArrayOpen(uri, tiledb_timestamp=ts3)
+    res <- snda$read()$tables()$concat()
+    expect_equal(dim(res), c(0,3))
+
+})
+
+test_that("SOMANDDenseArray", {
+    uri <- tempfile()
+
+    ## create at t = 2, also writes at t = 2 as timestamp is cached
+    ts1 <- rep(as.POSIXct(2, tz="UTC"), 2)
+    dnda <- SOMADenseNDArrayCreate(uri, arrow::int32(), shape = c(5, 2), tiledb_timestamp=ts1[1])
+    mat <- create_dense_matrix_with_int_dims(5, 2)
+    expect_silent(dnda$write(mat)) 		# write happens at create time
+
+    ## read at t = 3, expect all rows as read is from (0, 3)
+    ts2 <- rep(as.POSIXct(3, tz="UTC"), 2)
+    dnda <- tiledbsoma::SOMADenseNDArrayOpen(uri, tiledb_timestamp=ts2)
+    res <- dnda$read_arrow_table()
+    expect_equal(dim(res), c(10,1))
+
+    ## read at t = 1, expect zero rows as read is from (0, 1)
+    ## NB that this requires a) nullable arrow schema and b) na.omit() on result
+    ## It also works without a) as the fill value is also NA
+    ts3 <- rep(as.POSIXct(1, tz="UTC"), 2)
+    dnda <- tiledbsoma::SOMADenseNDArrayOpen(uri, tiledb_timestamp=ts3)
+    res <- dnda$read_arrow_table()
+    #print(res$soma_data)
+    vec <- tibble::as_tibble(res)
+    expect_equal(dim(na.omit(vec)), c(0,1))
+
+})