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

Issue:
[r] Port the query-condition logic from TileDB-R to TileDB-SOMA-R
Currently tiledbsoma-r uses query-condition logic from tiledb-r. We need to duplicate this within tiledbsoma-r. We've already done so for tiledbsoma-py and tiledb-py.

#2406 

[[sc-55672]](https://app.shortcut.com/tiledb-inc/story/55672/r-duplicate-query-condition-logic)

Do not close until this is complete: https://github.com/single-cell-data/TileDB-SOMA/pull/3162#discussion_r1801606040
[r] Query conditions for timestamps [WIP]
**Issue and/or context:** #3051 [[sc-55672]](https://app.shortcut.com/tiledb-inc/story/55672/r-port-the-query-condition-logic-from-tiledb-r-to-tiledb-soma-r). Follow-on to #3162.

**Changes:**

Needs a write-up about the issues.

**Notes for Reviewer:**

Needs a write-up about the issues.

This PR is not ready for review.


Discussion / hints:

Fix patch:
diff --git a/apis/r/DESCRIPTION b/apis/r/DESCRIPTION
index 9da4a5efe1..e9f63cce8c 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.15.99.4
+Version: 1.15.99.5
 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 9a66841637..665e4fe24a 100644
--- a/apis/r/NEWS.md
+++ b/apis/r/NEWS.md
@@ -10,7 +10,8 @@
 * Handle `numeric` coords properly when reading arrays
 * Remove two more `tiledb::schema` callsites [#3160](https://github.com/single-cell-data/TileDB-SOMA/pull/3160)
 * Add new Arrow-to-R type mapper
-* Add transitiona/non-exported `parse_query_condition_new` [#3162](https://github.com/single-cell-data/TileDB-SOMA/pull/3162)
+* Add transitional/non-exported `parse_query_condition_new` [#3162](https://github.com/single-cell-data/TileDB-SOMA/pull/3162)
+* Apply new `parse_query_condition` [#3174](https://github.com/single-cell-data/TileDB-SOMA/pull/3174)
 
 # tiledbsoma 1.14.1
 
diff --git a/apis/r/R/QueryCondition.R b/apis/r/R/QueryCondition.R
index 4bfd01bf5f..cffb5bfffb 100644
--- a/apis/r/R/QueryCondition.R
+++ b/apis/r/R/QueryCondition.R
@@ -34,7 +34,7 @@
 #' Expressions, in the R language syntax, are parsed locally by this function.
 #'
 #' @param expr An expression that is understood by the TileDB grammar for
-#' query conditions.
+#' query conditions, as a character string.
 #'
 #' @param schema The Arrow schema for the array for which a query
 #' condition is being prepared. This is necessary to obtain type information
@@ -57,7 +57,11 @@ parse_query_condition_new <- function(
   somactx
   ) {
 
+    spdl::debug("[parseqc] ENTER [{}]", expr)
+
   stopifnot(
+      "The expr argument must be a single character string" =
+        is(expr, "character") && length(expr) == 1,
       "The schema argument must be an Arrow Schema" =
           is(schema, "ArrowObject") &&
           is(schema, "Schema"),
@@ -164,17 +168,66 @@ parse_query_condition_new <- function(
                 arrow_type_name <- "utf8"
             }
 
+            if (arrow_type_name == "timestamp") {
+              unit <- arrow_field$type$unit()
+              if (unit == 0) {
+                arrow_type_name <- "timestamp_s"
+              } else if (unit == 1) {
+                arrow_type_name <- "timestamp_ms"
+              } else if (unit == 2) {
+                arrow_type_name <- "timestamp_us"
+              } else if (unit == 3) {
+                arrow_type_name <- "timestamp_ns"
+              } else {
+                .error_function(
+                  "Attribute '", attr_name, "' has unknown unit ",
+                  arrow_field$type$unit, call. = FALSE)
+              }
+            }
+
+            value = switch(
+                arrow_type_name,
+                ascii = rhs_text,
+                string = rhs_text,
+                utf8 = rhs_text,
+                large_utf8 = rhs_text,
+                bool = as.logical(rhs_text),
+                # Problem:
+
+                # > t <-as.POSIXct('1970-01-01 01:00:05 UTC')
+                # > as.numeric(t)
+                # [1] 21605
+                # > ?as.POSIXct
+                # > t <-as.POSIXct('1970-01-01 01:00:05 EST')
+                # > as.numeric(t)
+                # [1] 21605
+                # > t <-as.POSIXct('1970-01-01 01:00:05 UTC', tz="EST")
+                # > as.numeric(t)
+                # [1] 21605
+                # > t <-as.POSIXct('1970-01-01 01:00:05 UTC', tz="UTC")
+                # > as.numeric(t)
+                # [1] 3605
+
+                # It's not respecting the timezone given in the first argument string.
+                # Not good.
+
+                timestamp_s  = as.numeric(as.POSIXct(rhs_text, tz="UTC")), # THIS NEEDS THOUGHT
+                timestamp_ms = as.numeric(as.POSIXct(rhs_text, tz="UTC")), # THIS NEEDS THOUGHT
+                timestamp_ns = as.numeric(as.POSIXct(rhs_text, tz="UTC")), # THIS NEEDS THOUGHT
+                timestamp_us = as.numeric(as.POSIXct(rhs_text, tz="UTC")), # THIS NEEDS THOUGHT
+                date32 = as.Date(rhs_text),
+                as.numeric(rhs_text))
+
+            spdl::debug("[parseqc] triple name:[{}] value:[{}] type:[{}] op:[{}]",
+                attr_name,
+                value,
+                arrow_type_name,
+                op_name);
+
             # General case of extracting appropriate value given type info
             return(tiledbsoma_query_condition_from_triple(
                 attr_name = attr_name,
-                value = switch(
-                    arrow_type_name,
-                    ascii = rhs_text,
-                    utf8 = rhs_text,
-                    bool = as.logical(rhs_text),
-                    date32 = as.POSIXct(rhs_text),
-                    timestamp = as.Date(rhs_text),
-                    as.numeric(rhs_text)),
+                value = value,
                 arrow_type_name = arrow_type_name,
                 op_name = .map_op_to_character(op_name),
                 qc = tiledbsoma_empty_query_condition(somactx)))
@@ -184,8 +237,11 @@ parse_query_condition_new <- function(
         }
     }
 
+    # Convert expr from string to language
+    aslang <- str2lang(expr)
+
     # Use base-r `substitute` to map the user-provided expression to a parse tree
-    parse_tree <- substitute(expr)
+    parse_tree <- substitute(aslang)
 
     # Map the parse tree to TileDB core QueryCondition
     return(.parse_tree_to_qc(parse_tree, debug))
diff --git a/apis/r/R/SOMADataFrame.R b/apis/r/R/SOMADataFrame.R
index 5ff770de4b..88372e3891 100644
--- a/apis/r/R/SOMADataFrame.R
+++ b/apis/r/R/SOMADataFrame.R
@@ -178,8 +178,6 @@ SOMADataFrame <- R6::R6Class(
       private$check_open_for_read()
 
       result_order <- match_query_layout(result_order)
-      uri <- self$uri
-      arr <- self$object                 # need array (schema) to properly parse query condition
 
       ## if unnamed set names
       if (!is.null(coords)) {
@@ -199,8 +197,9 @@ SOMADataFrame <- R6::R6Class(
 
       if (!is.null(value_filter)) {
           value_filter <- validate_read_value_filter(value_filter)
-          parsed <- do.call(what = tiledb::parse_query_condition,
-                            args = list(expr = str2lang(value_filter), ta = arr))
+          parsed <- do.call(
+              what = parse_query_condition,
+              args = list(expr = value_filter, schema = self$schema(), somactx = private$.soma_context))
           value_filter <- parsed@ptr
       }
       spdl::debug("[SOMADataFrame$read] calling sr_setup for {} at ({},{})", self$uri,
diff --git a/apis/r/src/query_condition.cpp b/apis/r/src/query_condition.cpp
index 45aa7a6edd..785a2d214f 100644
--- a/apis/r/src/query_condition.cpp
+++ b/apis/r/src/query_condition.cpp
@@ -130,7 +130,9 @@ void libtiledbsoma_query_condition_from_triple(
         uint64_t cond_val_size = sizeof(float);
         query_cond->init(attr_name, (void*)&v, cond_val_size, op);
 
-    } else if (arrow_type_name == "utf8" || arrow_type_name == "large_utf8") {
+    } else if (
+        arrow_type_name == "string" || arrow_type_name == "ascii" ||
+        arrow_type_name == "utf8" || arrow_type_name == "large_utf8") {
         std::string v = Rcpp::as<std::string>(condition_value);
         query_cond->init(attr_name, v, op);
 
@@ -139,13 +141,32 @@ void libtiledbsoma_query_condition_from_triple(
         uint64_t cond_val_size = sizeof(bool);
         query_cond->init(attr_name, (void*)&v, cond_val_size, op);
 
-    } else if (arrow_type_name == "timestamp") {
-        // Arrow timestamp TileDB DATETIME_MS
+    } else if (arrow_type_name == "timestamp_s") {
+        int64_t v = static_cast<int64_t>(
+            Rcpp::as<double>(condition_value));
+      spdl::debug("ts3 {}", v);
+        uint64_t cond_val_size = sizeof(int64_t);
+        query_cond->init(attr_name, (void*)&v, cond_val_size, op);
+
+    } else if (arrow_type_name == "timestamp_ms") {
         int64_t v = static_cast<int64_t>(
             Rcpp::as<double>(condition_value) * 1000);
         uint64_t cond_val_size = sizeof(int64_t);
         query_cond->init(attr_name, (void*)&v, cond_val_size, op);
 
+    } else if (arrow_type_name == "timestamp_us") {
+        int64_t v = static_cast<int64_t>(
+            Rcpp::as<double>(condition_value) * 1e6);
+        uint64_t cond_val_size = sizeof(int64_t);
+        query_cond->init(attr_name, (void*)&v, cond_val_size, op);
+
+    } else if (arrow_type_name == "timestamp_ns") {
+        // XXX nanotime ...
+        int64_t v = static_cast<int64_t>(
+            Rcpp::as<double>(condition_value) * 1e9);
+        uint64_t cond_val_size = sizeof(int64_t);
+        query_cond->init(attr_name, (void*)&v, cond_val_size, op);
+
     } else if (arrow_type_name == "date32") {
         // Arrow date32 TileDB DATETIME_DAY
         int64_t v = static_cast<int64_t>(Rcpp::as<double>(condition_value));


Test patch:
diff --git a/apis/r/tests/testthat/test-SCEOutgest.R b/apis/r/tests/testthat/test-SCEOutgest.R
index 2fcc2f4d86..6402497c32 100644
--- a/apis/r/tests/testthat/test-SCEOutgest.R
+++ b/apis/r/tests/testthat/test-SCEOutgest.R
@@ -335,6 +335,7 @@ test_that("Load SCE object from indexed ExperimentQuery", {
   skip_if(!extended_tests() || covr_tests())
   skip_if_not_installed('SingleCellExperiment', .MINIMUM_SCE_VERSION('c'))
   uri <- tempfile(pattern="sce-experiment-query-value-filters")
+
   n_obs <- 1001L
   n_var <- 99L
   n_pcs <- 50L
@@ -371,6 +372,7 @@ test_that("Load SCE object from indexed ExperimentQuery", {
   n_var_select <- length(var_label_values)
   n_obs_select <- length(obs_label_values)
   expect_no_condition(obj <- query$to_single_cell_experiment())
+
   expect_s4_class(obj, 'SingleCellExperiment')
   expect_identical(dim(obj), c(n_var_select, n_obs_select))
   expect_identical(
diff --git a/apis/r/tests/testthat/test-SOMAArrayReader-Arrow.R b/apis/r/tests/testthat/test-SOMAArrayReader-Arrow.R
index 2135f557a6..8753730a51 100644
--- a/apis/r/tests/testthat/test-SOMAArrayReader-Arrow.R
+++ b/apis/r/tests/testthat/test-SOMAArrayReader-Arrow.R
@@ -15,33 +15,24 @@ test_that("Arrow Interface from SOMAArrayReader", {
     tb1 <- soma_array_to_arrow_table(soma_array_reader(uri, columns))
     expect_equal(tb1$num_rows, 2638)
 
-    arr <- tiledb_array(uri)         # need array for schema access to qc parser
-    qc <- parse_query_condition(n_counts < 1000 && n_genes >= 400, ta = arr)
-    tb2 <- soma_array_to_arrow_table(soma_array_reader(uri, columns, qc@ptr))
-
-    expect_equal(tb2$num_rows, 47)
-    expect_true(all(tb2$n_counts < 1000))
-    expect_true(all(tb2$n_genes >= 400))
-
-
     # read everything
-    tb3 <- soma_array_to_arrow_table(soma_array_reader(uri))
+    tb2 <- soma_array_to_arrow_table(soma_array_reader(uri))
 
-    expect_equal(tb3$num_rows, 2638)
-    expect_equal(tb3$num_columns, 6)
+    expect_equal(tb2$num_rows, 2638)
+    expect_equal(tb2$num_columns, 6)
 
     # read a subset of rows and columns
-    tb4 <- soma_array_to_arrow_table(soma_array_reader(uri = uri,
+    tb3 <- soma_array_to_arrow_table(soma_array_reader(uri = uri,
                 colnames = c("obs_id", "percent_mito", "n_counts", "louvain"),
                 dim_ranges = list(soma_joinid = rbind(bit64::as.integer64(c(1000, 1004)),
                                                   bit64::as.integer64(c(2000, 2004)))),
                 dim_points=list(soma_joinid = bit64::as.integer64(seq(0, 100, by = 20)))))
 
 
-    expect_equal(tb4$num_rows, 16)
-    expect_equal(tb4$num_columns, 4)
+    expect_equal(tb3$num_rows, 16)
+    expect_equal(tb3$num_columns, 4)
 
-    rm(z, tb, rb, tb1, arr, tb2, tb3, tb4)
+    rm(z, tb, rb, tb1, tb2, tb3)
     gc()
 })
 
diff --git a/apis/r/tests/testthat/test-query-condition.R b/apis/r/tests/testthat/test-query-condition.R
index 1f89191427..6c65c1800b 100644
--- a/apis/r/tests/testthat/test-query-condition.R
+++ b/apis/r/tests/testthat/test-query-condition.R
@@ -15,6 +15,8 @@ test_that("DataFrame Factory", {
       arrow::field("uint32", arrow::uint32()),
       arrow::field("uint64", arrow::uint64()),
       arrow::field("string", arrow::string()),
+      # Unlike in pyarrow there is no arrow::large_string
+      arrow::field("utf8", arrow::utf8()),
       arrow::field("large_utf8", arrow::large_utf8()),
       arrow::field("enum",
           arrow::dictionary(
@@ -22,12 +24,12 @@ test_that("DataFrame Factory", {
               value_type = arrow::utf8(),
               ordered = TRUE)),
       arrow::field("float32", arrow::float32()),
-      arrow::field("float64", arrow::float64())
+      arrow::field("float64", arrow::float64()),
       # TODO: for a follow-up PR
-      # arrow::field("timestamp_s", arrow::timestamp(unit="s")),
-      # arrow::field("timestamp_ms", arrow::timestamp(unit="ms")),
-      # arrow::field("timestamp_us", arrow::timestamp(unit="us")),
-      # arrow::field("timestamp_ns", arrow::timestamp(unit="ns"))
+      arrow::field("timestamp_s", arrow::timestamp(unit="s")),
+      arrow::field("timestamp_ms", arrow::timestamp(unit="ms")),
+      arrow::field("timestamp_us", arrow::timestamp(unit="us")),
+      arrow::field("timestamp_ns", arrow::timestamp(unit="ns"))
       # Not supported in libtiledbsoma
       # arrow::field("datetime_day", arrow::date32())
     )
@@ -47,6 +49,7 @@ test_that("DataFrame Factory", {
         uint32 =  301L:310L,
         uint64 =  401L:410L,
         string = c("apple", "ball", "cat", "dog", "egg", "fig", "goose", "hay", "ice", "jam"),
+        utf8 = c("apple", "ball", "cat", "dog", "egg", "fig", "goose", "hay", "ice", "jam"),
         large_utf8 = c("APPLE", "BALL", "CAT", "DOG", "EGG", "FIG", "GOOSE", "HAY", "ICE", "JAM"),
         enum = factor(
             c("red", "yellow", "green", "red", "red", "red", "yellow", "green", "red", "green"),
@@ -54,10 +57,10 @@ test_that("DataFrame Factory", {
         float32 = 1.5:10.5,
         float64 = 11.5:20.5,
         # TODO: for a follow-up PR
-        # timestamp_s  = as.POSIXct(as.numeric(3600 + 1:10), tz="GMT"),
-        # timestamp_ms = as.POSIXct(as.numeric(3600*1000 + 1:10), tz="GMT"),
-        # timestamp_us = as.POSIXct(as.numeric(3600*1000*1000 + 1:10), tz="GMT"),
-        # timestamp_ns = as.POSIXct(as.numeric(3600*1000*1000*1000 + 1:10), tz="GMT"),
+        timestamp_s  = as.POSIXct(as.numeric(1*3600 + 1:10), tz="UTC"),
+        timestamp_ms = as.POSIXct(as.numeric(2*3600 + 1:10), tz="UTC"),
+        timestamp_us = as.POSIXct(as.numeric(3*3600 + 1:10), tz="UTC"),
+        timestamp_ns = as.POSIXct(as.numeric(4*3600 + 1:10), tz="UTC"),
         schema = sch)
     sdf$write(tbl)
     sdf$close()
@@ -118,6 +121,15 @@ test_that("DataFrame Factory", {
       'string == "dog"' = function(df) {
           expect_equal(df$soma_joinid, c(4))
       },
+      'string == "cat" || string == "dog"' = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "string == 'cat' || string == 'dog'" = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "string == 'cat' || string == 'yak'" = function(df) {
+          expect_equal(df$soma_joinid, c(3))
+      },
       'string %in% c("fig", "dog")' = function(df) {
           expect_equal(df$soma_joinid, c(4, 6))
       },
@@ -125,6 +137,44 @@ test_that("DataFrame Factory", {
           expect_equal(df$soma_joinid, c(1, 2, 3, 5, 7, 8, 9, 10))
       },
 
+      'utf8 == "dog"' = function(df) {
+          expect_equal(df$soma_joinid, c(4))
+      },
+      'utf8 == "cat" || utf8 == "dog"' = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "utf8 == 'cat' || utf8 == 'dog'" = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "utf8 == 'cat' || utf8 == 'yak'" = function(df) {
+          expect_equal(df$soma_joinid, c(3))
+      },
+      'utf8 %in% c("fig", "dog")' = function(df) {
+          expect_equal(df$soma_joinid, c(4, 6))
+      },
+      'utf8 %nin% c("fig", "dog")' = function(df) {
+          expect_equal(df$soma_joinid, c(1, 2, 3, 5, 7, 8, 9, 10))
+      },
+
+      'large_utf8 == "DOG"' = function(df) {
+          expect_equal(df$soma_joinid, c(4))
+      },
+      'large_utf8 == "CAT" || large_utf8 == "DOG"' = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "large_utf8 == 'CAT' || large_utf8 == 'DOG'" = function(df) {
+          expect_equal(df$soma_joinid, c(3, 4))
+      },
+      "large_utf8 == 'CAT' || large_utf8 == 'YAK'" = function(df) {
+          expect_equal(df$soma_joinid, c(3))
+      },
+      'large_utf8 %in% c("FIG", "DOG")' = function(df) {
+          expect_equal(df$soma_joinid, c(4, 6))
+      },
+      'large_utf8 %nin% c("FIG", "DOG")' = function(df) {
+          expect_equal(df$soma_joinid, c(1, 2, 3, 5, 7, 8, 9, 10))
+      },
+
       'enum == "red"' = function(df) {
           expect_equal(df$soma_joinid, c(1, 4, 5, 6, 9))
       },
@@ -154,33 +204,41 @@ test_that("DataFrame Factory", {
       },
       'enum %nin% c("orange", "purple")' = function(df) {
           expect_equal(df$soma_joinid, 1:10)
-      }
+      },
 
       # TODO: for a follow-up PR
-      # 'timestamp_s < "1969-12-31 20:01:04 EST"' = function(df) {
-      #     expect_equal(df$soma_joinid, 1:3)
-      # },
-      # 'timestamp_ms != "1970-02-11 11:00:05 EST"' = function(df) {
-      #     expect_equal(df$soma_joinid, 1:10)
-      # },
-      # 'timestamp_us > "1970-01-01 00:00:01 GMT"' = function(df) {
-      #     expect_equal(df$soma_joinid, 1:10)
-      # },
-      # 'timestamp_ns > "1970-01-01 00:00:01 GMT"' = function(df) {
-      #     expect_equal(df$soma_joinid, 1:10)
-      # }
+      'timestamp_s < "1970-01-01 01:00:05 UTC"' = function(df) {
+          expect_equal(df$soma_joinid, 1:4)
+      },
+
+      'timestamp_ms < "1970-01-01 02:00:05 UTC"' = function(df) {
+          expect_equal(df$soma_joinid, 1:4)
+      },
+
+      'timestamp_us < "1970-01-01 03:00:05 UTC"' = function(df) {
+          expect_equal(df$soma_joinid, 1:4)
+      },
+
+      'timestamp_ns < "1970-01-01 04:00:05 UTC"' = function(df) {
+          expect_equal(df$soma_joinid, 1:4)
+      }
+
+      #         timestamp_s        timestamp_ms        timestamp_us        timestamp_ns
+      # 1970-01-01 01:00:01 1970-01-01 02:00:01 1970-01-01 03:00:01 1970-01-01 04:00:01
+      # 1970-01-01 01:00:02 1970-01-01 02:00:02 1970-01-01 03:00:02 1970-01-01 04:00:02
+      # 1970-01-01 01:00:03 1970-01-01 02:00:03 1970-01-01 03:00:03 1970-01-01 04:00:03
+      # 1970-01-01 01:00:04 1970-01-01 02:00:04 1970-01-01 03:00:04 1970-01-01 04:00:04
+      # 1970-01-01 01:00:05 1970-01-01 02:00:05 1970-01-01 03:00:05 1970-01-01 04:00:05
+      # 1970-01-01 01:00:06 1970-01-01 02:00:06 1970-01-01 03:00:06 1970-01-01 04:00:06
+      # 1970-01-01 01:00:07 1970-01-01 02:00:07 1970-01-01 03:00:07 1970-01-01 04:00:07
+      # 1970-01-01 01:00:08 1970-01-01 02:00:08 1970-01-01 03:00:08 1970-01-01 04:00:08
+      # 1970-01-01 01:00:09 1970-01-01 02:00:09 1970-01-01 03:00:09 1970-01-01 04:00:09
+      # 1970-01-01 01:00:10 1970-01-01 02:00:10 1970-01-01 03:00:10 1970-01-01 04:00:10
+
     )
 
     for (query_string in names(good_cases)) {
-        parsed <- do.call(
-            what = tiledbsoma:::parse_query_condition_new,
-            args = list(expr=str2lang(query_string), schema=sch, somactx=ctx))
-        clib_value_filter <- parsed@ptr
-
-        sr <- sr_setup(uri = sdf$uri, ctx, qc=clib_value_filter)
-        iter <- TableReadIter$new(sr)
-        tbl <- iter$read_next()
-        expect_true(iter$read_complete())
+        tbl <- sdf$read(value_filter = query_string)$concat()
         df <- as.data.frame(tbl)
         # Call the validator
         good_cases[[query_string]](df)
@@ -196,10 +254,7 @@ test_that("DataFrame Factory", {
     )
 
     for (query_string in names(bad_cases)) {
-        expect_error(
-            do.call(
-                what = tiledbsoma:::parse_query_condition_new,
-                args = list(expr=str2lang(query_string), schema=sch, somactx=ctx)))
+        expect_error(sdf$read(value_filter = query_string)$concat())
     }
 
     sdf$close()