Skip to content

Commit 30341af

Browse files
bakeyclaude
andauthored
refactor(sources): share the UDTF string-argument extractor (#169)
* refactor(sources): share the UDTF string-argument extractor Nine per-provider Expr-to-string extractors (lance knn/fts, sqlite, seekdb, pg fts/knn, mongo, open_connector) had drifted in both error wording ("sqlite: 'x'", "lance_knn: x", "mongo_fts: 'x' ... got {:?}") and NULL handling — some accepted NULL as the pipeline schema-inference placeholder, others rejected it, with the difference nowhere stated. One shared module (providers/udtf_args.rs) now carries the three deliberate semantics, named at each call site: - string_arg: NULL is the pipeline {param} placeholder, yields "" (the prior behavior of lance_fts / sqlite / seekdb / pg_fts / mongo); - optional_string_arg: NULL means "argument not provided" (lance_fts's or-null variant); - strict_string_arg: NULL rejected, for arguments that determine the planned schema where a placeholder cannot produce a plan (lance_knn, pg_knn, and the open_connector UDTFs keep their prior strictness). Error wording is uniform ("{fn}: '{arg}' must be a string literal"); each provider keeps its exact prior NULL semantics, so this changes no behavior beyond message text. Addresses the non-blocking cleanup note from the #165 review. The ColumnarValue-based extractors in model/ (scalar-UDF context, different signature) are out of scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(sources): share the UDTF JSON-object argument parser open_connector_query's resource_json and open_connector_scan's input_json duplicated the serde_json::from_str + is_object() validation with already-diverging messages ("of resource inputs" vs "of action inputs"). One parse_json_object(fn_name, arg, raw) helper keeps the two diagnostics in lockstep; tests pin the non-object rejection for both functions. Addresses the second non-blocking cleanup note from the #165 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sources): drop the doc comment orphaned by the extract_string removal Deleting sqlite's extract_string left its two doc-comment lines attached to the unrelated quote_sqlite_table below it. The other providers' section headers were checked and still head real content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sources): keep the per-argument context in the shared JSON-object error Sharing parse_json_object flattened "of resource inputs" / "of action inputs" into a generic "of input fields". Thread the caller's noun through so the shared implementation and the specific diagnostic coexist; the non-object tests now pin each function's full wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sources): stop pg_knn swallowing malformed inline filters The 6th argument was extracted with `.ok()` (pre-existing on main), so any non-string filter was silently dropped and the query returned unfiltered rows. Use optional_string_arg: NULL still means "no filter" (the pipeline placeholder), everything else that isn't a string literal now fails planning with the targeted message — matching lance_knn, which always propagated. Tests pin both paths without needing a live Postgres. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(sources): use the plan_err macro pair throughout parse_json_object The parse-failure branch built DataFusionError::Plan(format!(...)) by hand while the non-object branch used plan_err! — two construction styles in one helper. Use DataFusion's pair as intended: plan_datafusion_err! (constructs the error, for map_err) alongside plan_err! (returns it). Note the review's literal suggestion, .map_err(|e| plan_err!(...))??, does not compile — plan_err! expands to a full Err(...) Result, which is why the *_datafusion_err! twin exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(sources): pin the optional happy path and typed-null boundaries Per review: null_semantics_differ_by_variant now asserts optional_string_arg returns Some for a real string, and a new test locks Utf8(None)/LargeUtf8(None) — typed NULLs, e.g. from CAST(NULL AS VARCHAR) — as rejections across all three variants: they must be neither a valid empty string nor the untyped-Null placeholder, matching every provider's pre-refactor behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(sources): cover NULL arguments to open_connector_query at planning Per review: the argument-rejection test covered non-string literals but not SQL NULL. Add NULL for gateway, table_id, and resource_json, asserting the strict_string_arg "must be a string literal, not NULL" diagnostic — still inside the same test, whose trailing assertion proves every rejection fires before any HTTP execute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4ea7d9f commit 30341af

14 files changed

Lines changed: 299 additions & 165 deletions

File tree

crates/skardi/src/sources/providers/lance/fts_table_function.rs

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use std::sync::Arc;
3939

4040
use super::fts_exec::LanceFtsExec;
4141
use super::utils::expr_to_lance_sql;
42+
use crate::sources::providers::udtf_args::{optional_string_arg, string_arg};
4243
use crate::sources::providers::{DatasetEntry, DatasetRegistry};
4344

4445
/// Table function that creates full-text search on Lance tables
@@ -62,9 +63,9 @@ impl TableFunctionImpl for LanceFtsTableFunction {
6263
);
6364
}
6465

65-
let table_name = extract_string(&exprs[0], "table_name")?;
66-
let text_column = extract_string(&exprs[1], "text_column")?;
67-
let query = extract_string_or_null(&exprs[2], "query")?;
66+
let table_name = string_arg(&exprs[0], "lance_fts", "table_name")?;
67+
let text_column = string_arg(&exprs[1], "lance_fts", "text_column")?;
68+
let query = optional_string_arg(&exprs[2], "lance_fts", "query")?;
6869
let limit = extract_int(&exprs[3], "limit")?;
6970

7071
// Get dataset from registry
@@ -309,24 +310,6 @@ impl TableProvider for LanceFtsProvider {
309310

310311
// Helper functions for argument extraction
311312

312-
fn extract_string(expr: &Expr, name: &str) -> DFResult<String> {
313-
match expr {
314-
Expr::Literal(ScalarValue::Utf8(Some(s)), _) => Ok(s.clone()),
315-
Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => Ok(s.clone()),
316-
// Accept NULL as placeholder during pipeline validation/schema inference.
317-
// The inferencer replaces {param} with NULL before plan creation.
318-
Expr::Literal(ScalarValue::Null, _) => Ok(String::new()),
319-
_ => plan_err!("lance_fts: {} must be a string literal", name),
320-
}
321-
}
322-
323-
fn extract_string_or_null(expr: &Expr, name: &str) -> DFResult<Option<String>> {
324-
match expr {
325-
Expr::Literal(ScalarValue::Null, _) => Ok(None),
326-
other => extract_string(other, name).map(Some),
327-
}
328-
}
329-
330313
fn extract_int(expr: &Expr, name: &str) -> DFResult<usize> {
331314
match expr {
332315
Expr::Literal(ScalarValue::Int64(Some(n)), _) => Ok(*n as usize),

crates/skardi/src/sources/providers/lance/knn_table_function.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::sync::Arc;
3030
use super::knn_exec::LanceKnnExec;
3131
use super::utils::expr_to_lance_sql;
3232
use crate::sources::providers::knn_utils::extract_k;
33+
use crate::sources::providers::udtf_args::strict_string_arg;
3334
use crate::sources::providers::{DatasetEntry, DatasetRegistry};
3435

3536
/// Table function that creates KNN search on Lance tables
@@ -54,11 +55,11 @@ impl TableFunctionImpl for LanceKnnTableFunction {
5455
}
5556

5657
// Extract string arguments
57-
let table_name = extract_string(&exprs[0], "table_name")?;
58-
let vector_column = extract_string(&exprs[1], "vector_column")?;
58+
let table_name = strict_string_arg(&exprs[0], "lance_knn", "table_name")?;
59+
let vector_column = strict_string_arg(&exprs[1], "lance_knn", "vector_column")?;
5960
let k = extract_k(&exprs[3], "lance_knn")?;
6061
let filter = if exprs.len() == 5 {
61-
Some(extract_string(&exprs[4], "filter")?)
62+
Some(strict_string_arg(&exprs[4], "lance_knn", "filter")?)
6263
} else {
6364
None
6465
};
@@ -230,14 +231,6 @@ impl TableProvider for LanceKnnProvider {
230231

231232
// Helper functions for argument extraction
232233

233-
fn extract_string(expr: &Expr, name: &str) -> DFResult<String> {
234-
match expr {
235-
Expr::Literal(ScalarValue::Utf8(Some(s)), _) => Ok(s.clone()),
236-
Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => Ok(s.clone()),
237-
_ => plan_err!("lance_knn: {} must be a string literal", name),
238-
}
239-
}
240-
241234
fn try_extract_vector(expr: &Expr) -> DFResult<Option<ArrayRef>> {
242235
match expr {
243236
Expr::Literal(ScalarValue::List(arr), _) => {

crates/skardi/src/sources/providers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod redis;
1414
pub mod seekdb;
1515
pub mod sqlite;
1616
pub mod sqlx;
17+
pub(crate) mod udtf_args;
1718

1819
use ::lance::dataset::Dataset;
1920
use datafusion::datasource::TableProvider;

crates/skardi/src/sources/providers/mongo/fts_table_function.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::sync::Arc;
2929

3030
use super::fts_exec::MongoFtsExec;
3131
use super::{binary_expr_to_mongo, is_pushable_binary_filter};
32+
use crate::sources::providers::udtf_args::string_arg;
3233
use crate::sources::providers::{DatasetEntry, DatasetRegistry};
3334

3435
/// Maximum allowed FTS result limit (matches MAX_KNN_K).
@@ -65,8 +66,8 @@ impl TableFunctionImpl for MongoFtsTableFunction {
6566
);
6667
}
6768

68-
let collection_name = extract_string(&exprs[0], "collection")?;
69-
let query = extract_string(&exprs[1], "query")?;
69+
let collection_name = string_arg(&exprs[0], "mongo_fts", "collection")?;
70+
let query = string_arg(&exprs[1], "mongo_fts", "query")?;
7071
let limit = extract_int(&exprs[2], "limit")?;
7172

7273
// The inferencer replaces {param} with NULL, yielding empty string for
@@ -236,21 +237,6 @@ fn expr_to_mongo_filter_entry(expr: &Expr, primary_key: &str) -> Option<Document
236237

237238
// ─── Argument extraction helpers ─────────────────────────────────────────────
238239

239-
fn extract_string(expr: &Expr, name: &str) -> DFResult<String> {
240-
match expr {
241-
Expr::Literal(ScalarValue::Utf8(Some(s)), _)
242-
| Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => Ok(s.clone()),
243-
// Accept NULL as placeholder during pipeline validation/schema inference.
244-
// The inferencer replaces {param} with NULL before plan creation.
245-
Expr::Literal(ScalarValue::Null, _) => Ok(String::new()),
246-
_ => plan_err!(
247-
"mongo_fts: '{}' must be a string literal, got {:?}",
248-
name,
249-
expr
250-
),
251-
}
252-
}
253-
254240
fn extract_int(expr: &Expr, name: &str) -> DFResult<usize> {
255241
match expr {
256242
Expr::Literal(ScalarValue::Int64(Some(v)), _) => Ok(*v as usize),

crates/skardi/src/sources/providers/open_connector/table_functions.rs

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use std::time::Duration;
3737
use arrow::datatypes::SchemaRef;
3838
use async_trait::async_trait;
3939
use datafusion::catalog::{Session, TableFunctionImpl, TableProvider};
40-
use datafusion::common::{ScalarValue, plan_err};
40+
use datafusion::common::{plan_datafusion_err, plan_err};
4141
use datafusion::datasource::TableType;
4242
use datafusion::error::{DataFusionError, Result as DFResult};
4343
use datafusion::logical_expr::Expr;
@@ -57,6 +57,7 @@ use super::raw_schema::derive_raw_columns;
5757
use super::row_path::RowPath;
5858
use super::source_pack::SourcePackRegistry;
5959
use super::table::OpenConnectorTableProvider;
60+
use crate::sources::providers::udtf_args::strict_string_arg;
6061

6162
/// Planning-time state of one registered gateway, captured by
6263
/// `register_open_connector_tables` and shared with both UDTFs.
@@ -143,12 +144,12 @@ impl TableFunctionImpl for OpenConnectorQueryFunction {
143144
exprs.len()
144145
);
145146
}
146-
let gateway = literal_string("open_connector_query", &exprs[0], "gateway")?;
147-
let table_id = literal_string("open_connector_query", &exprs[1], "table_id")?;
148-
let resource_json = literal_string("open_connector_query", &exprs[2], "resource_json")?;
147+
let gateway = strict_string_arg(&exprs[0], "open_connector_query", "gateway")?;
148+
let table_id = strict_string_arg(&exprs[1], "open_connector_query", "table_id")?;
149+
let resource_json = strict_string_arg(&exprs[2], "open_connector_query", "resource_json")?;
149150
let alias = exprs
150151
.get(3)
151-
.map(|expr| literal_string("open_connector_query", expr, "connection_alias"))
152+
.map(|expr| strict_string_arg(expr, "open_connector_query", "connection_alias"))
152153
.transpose()?;
153154

154155
let handle = lookup_gateway(&self.gateways, &gateway)?;
@@ -165,17 +166,12 @@ impl TableFunctionImpl for OpenConnectorQueryFunction {
165166
let pack = self.packs.require(pack_name).map_err(plan_error)?;
166167
let table = self.packs.table(pack, table_name).map_err(plan_error)?;
167168

168-
let resource: Value = serde_json::from_str(&resource_json).map_err(|e| {
169-
DataFusionError::Plan(format!(
170-
"open_connector_query: resource_json is not valid JSON: {e}"
171-
))
172-
})?;
173-
if !resource.is_object() {
174-
return plan_err!(
175-
"open_connector_query: resource_json must be a JSON object of resource \
176-
inputs, e.g. '{{\"owner\":\"SkardiLabs\",\"repo\":\"skardi\"}}'"
177-
);
178-
}
169+
let resource = parse_json_object(
170+
"open_connector_query",
171+
"resource_json",
172+
&resource_json,
173+
"resource inputs",
174+
)?;
179175
for key in table.required_resources {
180176
if resource.get(*key).is_none() {
181177
return Err(plan_error(OpenConnectorError::MissingResourceInput {
@@ -242,13 +238,13 @@ impl TableFunctionImpl for OpenConnectorScanFunction {
242238
exprs.len()
243239
);
244240
}
245-
let gateway = literal_string("open_connector_scan", &exprs[0], "gateway")?;
246-
let action_id = literal_string("open_connector_scan", &exprs[1], "action_id")?;
247-
let input_json = literal_string("open_connector_scan", &exprs[2], "input_json")?;
248-
let row_path = literal_string("open_connector_scan", &exprs[3], "row_path")?;
241+
let gateway = strict_string_arg(&exprs[0], "open_connector_scan", "gateway")?;
242+
let action_id = strict_string_arg(&exprs[1], "open_connector_scan", "action_id")?;
243+
let input_json = strict_string_arg(&exprs[2], "open_connector_scan", "input_json")?;
244+
let row_path = strict_string_arg(&exprs[3], "open_connector_scan", "row_path")?;
249245
let alias = exprs
250246
.get(4)
251-
.map(|expr| literal_string("open_connector_scan", expr, "connection_alias"))
247+
.map(|expr| strict_string_arg(expr, "open_connector_scan", "connection_alias"))
252248
.transpose()?;
253249

254250
let handle = lookup_gateway(&self.gateways, &gateway)?;
@@ -279,17 +275,12 @@ impl TableFunctionImpl for OpenConnectorScanFunction {
279275
}
280276
}
281277

282-
let input: Value = serde_json::from_str(&input_json).map_err(|e| {
283-
DataFusionError::Plan(format!(
284-
"open_connector_scan: input_json is not valid JSON: {e}"
285-
))
286-
})?;
287-
if !input.is_object() {
288-
return plan_err!(
289-
"open_connector_scan: input_json must be a JSON object of action inputs, \
290-
e.g. '{{\"owner\":\"SkardiLabs\",\"repo\":\"skardi\"}}'"
291-
);
292-
}
278+
let input = parse_json_object(
279+
"open_connector_scan",
280+
"input_json",
281+
&input_json,
282+
"action inputs",
283+
)?;
293284

294285
let row_path = RowPath::parse(&row_path).map_err(plan_error)?;
295286
// Deterministic row type or planning error — derived purely from the
@@ -423,13 +414,20 @@ fn plan_error(e: OpenConnectorError) -> DataFusionError {
423414
DataFusionError::Plan(e.to_string())
424415
}
425416

426-
/// Extract one string-literal argument.
427-
fn literal_string(function: &str, expr: &Expr, name: &str) -> DFResult<String> {
428-
match expr {
429-
Expr::Literal(ScalarValue::Utf8(Some(s)), _)
430-
| Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => Ok(s.clone()),
431-
_ => plan_err!("{function}: {name} must be a string literal"),
417+
/// Parse a UDTF argument that must carry a JSON object, shared by both
418+
/// functions so the two diagnostics stay in lockstep. `noun` names what the
419+
/// object holds in the caller's vocabulary ("resource inputs" / "action
420+
/// inputs"), so sharing the implementation doesn't flatten the context.
421+
fn parse_json_object(fn_name: &str, arg: &str, raw: &str, noun: &str) -> DFResult<Value> {
422+
let value: Value = serde_json::from_str(raw)
423+
.map_err(|e| plan_datafusion_err!("{fn_name}: {arg} is not valid JSON: {e}"))?;
424+
if !value.is_object() {
425+
return plan_err!(
426+
"{fn_name}: {arg} must be a JSON object of {noun}, \
427+
e.g. '{{\"owner\":\"SkardiLabs\",\"repo\":\"skardi\"}}'"
428+
);
432429
}
430+
Ok(value)
433431
}
434432

435433
#[cfg(test)]
@@ -746,6 +744,12 @@ raw_action_allowlist:
746744
"resource_json is not valid JSON",
747745
)
748746
.await;
747+
expect_plan_error(
748+
&ctx,
749+
"SELECT * FROM open_connector_query('saas', 'mock.items', '[1, 2]')",
750+
"resource_json must be a JSON object of resource inputs",
751+
)
752+
.await;
749753
expect_plan_error(
750754
&ctx,
751755
"SELECT * FROM open_connector_query('saas', 'mock.items')",
@@ -755,7 +759,27 @@ raw_action_allowlist:
755759
expect_plan_error(
756760
&ctx,
757761
"SELECT * FROM open_connector_query(1, 'mock.items', '{}')",
758-
"gateway must be a string literal",
762+
"'gateway' must be a string literal",
763+
)
764+
.await;
765+
// NULL is rejected outright for schema-determining arguments — a
766+
// placeholder cannot produce a plan (strict_string_arg semantics).
767+
expect_plan_error(
768+
&ctx,
769+
"SELECT * FROM open_connector_query(NULL, 'mock.items', '{}')",
770+
"'gateway' must be a string literal, not NULL",
771+
)
772+
.await;
773+
expect_plan_error(
774+
&ctx,
775+
"SELECT * FROM open_connector_query('saas', NULL, '{}')",
776+
"'table_id' must be a string literal, not NULL",
777+
)
778+
.await;
779+
expect_plan_error(
780+
&ctx,
781+
"SELECT * FROM open_connector_query('saas', 'mock.items', NULL)",
782+
"'resource_json' must be a string literal, not NULL",
759783
)
760784
.await;
761785

@@ -998,6 +1022,13 @@ raw_action_allowlist:
9981022
"must start with '$.'",
9991023
)
10001024
.await;
1025+
expect_plan_error(
1026+
&ctx,
1027+
r#"SELECT * FROM open_connector_scan('saas', 'mock.list_items',
1028+
'[1, 2]', '$.items')"#,
1029+
"input_json must be a JSON object of action inputs",
1030+
)
1031+
.await;
10011032
assert!(execute_requests(&gateway).is_empty(), "rejected pre-HTTP");
10021033
}
10031034

crates/skardi/src/sources/providers/seekdb/fts_table_function.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ use datafusion_table_providers::sql::db_connection_pool::mysqlpool::MySQLConnect
3636
use std::any::Any;
3737
use std::sync::Arc;
3838

39+
use super::expr_to_seekdb_sql;
3940
use super::fts_exec::SeekDbFtsExec;
40-
use super::{expr_to_seekdb_sql, extract_string_arg};
41+
use crate::sources::providers::udtf_args::string_arg;
4142
use crate::sources::providers::{DatasetEntry, DatasetRegistry};
4243

4344
/// Maximum allowed FTS result limit.
@@ -66,9 +67,9 @@ impl TableFunctionImpl for SeekDbFtsTableFunction {
6667
);
6768
}
6869

69-
let table_name = extract_string_arg(&exprs[0], "seekdb_fts", "table")?;
70-
let text_col = extract_string_arg(&exprs[1], "seekdb_fts", "text_col")?;
71-
let query = extract_string_arg(&exprs[2], "seekdb_fts", "query")?;
70+
let table_name = string_arg(&exprs[0], "seekdb_fts", "table")?;
71+
let text_col = string_arg(&exprs[1], "seekdb_fts", "text_col")?;
72+
let query = string_arg(&exprs[2], "seekdb_fts", "query")?;
7273
let limit = match extract_int(&exprs[3], "limit")? {
7374
None => 1,
7475
Some(v) if v > MAX_FTS_LIMIT => {

crates/skardi/src/sources/providers/seekdb/knn_table_function.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ use datafusion_table_providers::sql::db_connection_pool::mysqlpool::MySQLConnect
5252
use std::any::Any;
5353
use std::sync::Arc;
5454

55+
use super::expr_to_seekdb_sql;
5556
use super::knn_exec::{DistanceMetric, SeekDbKnnExec};
56-
use super::{expr_to_seekdb_sql, extract_string_arg};
5757
use crate::sources::providers::knn_utils::{extract_k, extract_literal_vector};
58+
use crate::sources::providers::udtf_args::string_arg;
5859
use crate::sources::providers::{DatasetEntry, DatasetRegistry};
5960

6061
/// Entry stored in the registry for each registered SeekDB table.
@@ -92,10 +93,10 @@ impl TableFunctionImpl for SeekDbKnnTableFunction {
9293
);
9394
}
9495

95-
let table_name = extract_string_arg(&exprs[0], "seekdb_knn", "table")?;
96-
let vector_col = extract_string_arg(&exprs[1], "seekdb_knn", "vector_col")?;
96+
let table_name = string_arg(&exprs[0], "seekdb_knn", "table")?;
97+
let vector_col = string_arg(&exprs[1], "seekdb_knn", "vector_col")?;
9798

98-
let metric_str = extract_string_arg(&exprs[3], "seekdb_knn", "metric")?;
99+
let metric_str = string_arg(&exprs[3], "seekdb_knn", "metric")?;
99100
let metric = if metric_str.is_empty() {
100101
// NULL placeholder during schema inference — default to L2.
101102
DistanceMetric::default()

crates/skardi/src/sources/providers/seekdb/mod.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use arrow::array::{RecordBatch, UInt64Array};
3939
use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
4040
use async_trait::async_trait;
4141
use datafusion::catalog::Session;
42-
use datafusion::common::{Constraints, ScalarValue, plan_err};
42+
use datafusion::common::Constraints;
4343
use datafusion::datasource::{TableProvider, TableType};
4444
use datafusion::error::{DataFusionError, Result as DataFusionResult};
4545
use datafusion::execution::{SendableRecordBatchStream, TaskContext};
@@ -793,23 +793,6 @@ pub(crate) fn expr_to_seekdb_sql(expr: &Expr) -> Option<String> {
793793
unparser.expr_to_sql(expr).ok().map(|ast| ast.to_string())
794794
}
795795

796-
/// Extract a string-literal argument from a table-function `Expr`. Shared by
797-
/// `seekdb_fts` and `seekdb_knn` so they can emit consistent error messages
798-
/// prefixed with the caller's UDTF name (`fn_name`). NULL is accepted as a
799-
/// placeholder during pipeline schema inference and returns the empty string.
800-
pub(crate) fn extract_string_arg(
801-
expr: &Expr,
802-
fn_name: &str,
803-
arg: &str,
804-
) -> DataFusionResult<String> {
805-
match expr {
806-
Expr::Literal(ScalarValue::Utf8(Some(s)), _)
807-
| Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => Ok(s.clone()),
808-
Expr::Literal(ScalarValue::Null, _) => Ok(String::new()),
809-
_ => plan_err!("{fn_name}: '{arg}' must be a string literal"),
810-
}
811-
}
812-
813796
// ─── Execution plan for DELETE / UPDATE results ─────────────────────────────
814797

815798
/// A leaf [`ExecutionPlan`] that executes a pre-built SeekDB DML statement

0 commit comments

Comments
 (0)