1use crate::extended_sql_functions::{json_table_batches_from_text, JsonTableMode};
2use crate::{arrow_conv, MongrelQueryError, Result};
3use arrow::array::{ArrayRef, Int64Array, StringArray};
4use arrow::datatypes::{DataType, Field, Schema as ArrowSchema, SchemaRef};
5use arrow::record_batch::{RecordBatch, RecordBatchOptions};
6use datafusion::catalog::{Session, TableProvider};
7use datafusion::common::{DataFusionError, Result as DFResult, ScalarValue};
8use datafusion::execution::TaskContext;
9use datafusion::logical_expr::{
10 BinaryExpr, Expr, Operator, TableProviderFilterPushDown, TableType,
11};
12use datafusion::physical_expr::EquivalenceProperties;
13use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
14use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
15use datafusion::physical_plan::{
16 DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
17 SendableRecordBatchStream,
18};
19use mongreldb_core::catalog::TableState;
20use mongreldb_core::database::{TABLES_DIR, VTAB_DIR};
21use mongreldb_core::schema::{
22 ColumnDef as CoreColumnDef, ColumnFlags, Schema as CoreSchema, TypeId,
23};
24use mongreldb_core::{Database, ExternalTableEntry, ModuleArg, ModuleCapabilities, Value};
25use serde::{Deserialize, Serialize};
26use std::any::Any;
27use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
28use std::fs;
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31
32use crate::query_registry::{RegisteredSqlQuery, SqlTaskContext};
33
34const EXTERNAL_STATE_BYTES_LIMIT: usize = 64 * 1024 * 1024;
35const EXTERNAL_ROWS_LIMIT: usize = 1_000_000;
36const EXTERNAL_ROWS_BYTES_LIMIT: usize = 64 * 1024 * 1024;
37const EXTERNAL_BASE_WRITES_LIMIT: usize = 1_000_000;
38const EXTERNAL_BASE_WRITE_BYTES_LIMIT: usize = 64 * 1024 * 1024;
39
40fn external_resource_limit(
41 resource: &'static str,
42 requested: usize,
43 limit: usize,
44) -> MongrelQueryError {
45 MongrelQueryError::Core(mongreldb_core::MongrelError::ResourceLimitExceeded {
46 resource,
47 requested,
48 limit,
49 })
50}
51
52fn external_value_bytes(value: &Value) -> usize {
53 match value {
54 Value::Null => 0,
55 Value::Bool(_) => 1,
56 Value::Int64(_) | Value::Float64(_) => 8,
57 Value::Bytes(value) | Value::Json(value) => value.len(),
58 Value::Embedding(value) => value.len().saturating_mul(std::mem::size_of::<f32>()),
59 Value::Decimal(_) | Value::Uuid(_) => 16,
60 Value::Interval { .. } => 20,
61 }
62}
63
64fn external_row_bytes(row: &HashMap<u16, Value>) -> usize {
65 let bucket_bytes =
66 std::mem::size_of::<(u16, Value)>().saturating_add(2 * std::mem::size_of::<usize>());
67 row.capacity().saturating_mul(bucket_bytes).saturating_add(
68 row.values()
69 .map(external_value_bytes)
70 .fold(0_usize, usize::saturating_add),
71 )
72}
73
74pub(crate) fn enforce_external_rows_limit(
75 rows: &[HashMap<u16, Value>],
76 query: Option<&RegisteredSqlQuery>,
77) -> Result<()> {
78 if rows.len() > EXTERNAL_ROWS_LIMIT {
79 return Err(external_resource_limit(
80 "external table rows",
81 rows.len(),
82 EXTERNAL_ROWS_LIMIT,
83 ));
84 }
85 let mut bytes = 0_usize;
86 for (index, row) in rows.iter().enumerate() {
87 if index % 256 == 0 {
88 query.map(RegisteredSqlQuery::checkpoint).transpose()?;
89 }
90 bytes = bytes.saturating_add(external_row_bytes(row));
91 if bytes > EXTERNAL_ROWS_BYTES_LIMIT {
92 return Err(external_resource_limit(
93 "external table row bytes",
94 bytes,
95 EXTERNAL_ROWS_BYTES_LIMIT,
96 ));
97 }
98 }
99 Ok(())
100}
101
102pub(crate) fn enforce_external_state_limit(state: &[u8]) -> Result<()> {
103 if state.len() > EXTERNAL_STATE_BYTES_LIMIT {
104 return Err(external_resource_limit(
105 "external table state bytes",
106 state.len(),
107 EXTERNAL_STATE_BYTES_LIMIT,
108 ));
109 }
110 Ok(())
111}
112
113pub trait ExternalTableModule: Send + Sync {
114 fn name(&self) -> &str;
115 fn descriptor(&self) -> ExternalModuleDescriptor;
116 fn indexes(&self, _entry: &ExternalTableEntry) -> Result<Vec<ExternalModuleIndex>> {
117 Ok(Vec::new())
118 }
119 fn connect(
120 &self,
121 ctx: &ModuleConnectCtx<'_>,
122 entry: &ExternalTableEntry,
123 ) -> Result<Arc<dyn ExternalTable>>;
124 fn read_rows(
125 &self,
126 _ctx: &ModuleConnectCtx<'_>,
127 entry: &ExternalTableEntry,
128 ) -> Result<Vec<HashMap<u16, Value>>> {
129 Err(MongrelQueryError::Schema(format!(
130 "external table {:?} using module {:?} is not row-writable",
131 entry.name,
132 self.name()
133 )))
134 }
135 fn prepare_rows(
136 &self,
137 _ctx: &ModuleConnectCtx<'_>,
138 entry: &ExternalTableEntry,
139 _rows: Vec<HashMap<u16, Value>>,
140 ) -> Result<Vec<u8>> {
141 Err(MongrelQueryError::Schema(format!(
142 "external table {:?} using module {:?} is not row-writable",
143 entry.name,
144 self.name()
145 )))
146 }
147 fn rows_from_state(&self, state: &[u8]) -> Result<Vec<HashMap<u16, Value>>> {
148 if state.is_empty() {
149 Ok(Vec::new())
150 } else {
151 decode_state_rows(state)
152 }
153 }
154 fn write(
155 &self,
156 ctx: &ModuleConnectCtx<'_>,
157 entry: &ExternalTableEntry,
158 op: ExternalWriteOp,
159 txn: &mut ExternalTxn,
160 ) -> Result<ExternalWriteResult> {
161 let (rows, changes) = match op {
162 ExternalWriteOp::Insert { rows: inserted } => {
163 let changes = inserted.len() as u64;
164 let mut rows = txn.read_rows()?;
165 rows.extend(inserted);
166 (rows, changes)
167 }
168 ExternalWriteOp::ReplaceRows { rows, changes } => (rows, changes),
169 };
170 txn.replace_state(self.prepare_rows(ctx, entry, rows)?);
171 Ok(ExternalWriteResult { changes })
172 }
173 fn destroy(&self, _ctx: &ModuleConnectCtx<'_>, _entry: &ExternalTableEntry) -> Result<()> {
174 Ok(())
175 }
176}
177
178pub trait ExternalTable: std::fmt::Debug + Send + Sync {
179 fn schema(&self) -> SchemaRef;
180 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan>;
181 fn scan_with_control(
182 &self,
183 request: &ExternalPlanRequest<'_>,
184 control: &mongreldb_core::ExecutionControl,
185 ) -> DFResult<ExternalScan>;
186}
187
188#[derive(Clone)]
189pub struct ExternalModuleDescriptor {
190 pub schema: CoreSchema,
191 pub hidden_columns: Vec<String>,
192 pub capabilities: ModuleCapabilities,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct ExternalModuleIndex {
197 pub name: String,
198 pub column_ids: Vec<u16>,
199 pub unique: bool,
200 pub partial: bool,
201}
202
203impl ExternalModuleIndex {
204 pub fn new(name: impl Into<String>, column_ids: Vec<u16>) -> Self {
205 Self {
206 name: name.into(),
207 column_ids,
208 unique: false,
209 partial: false,
210 }
211 }
212}
213
214pub struct ModuleConnectCtx<'a> {
215 pub db: &'a Arc<Database>,
216}
217
218impl ModuleConnectCtx<'_> {
219 pub fn raw_state(&self, entry: &ExternalTableEntry) -> Result<Vec<u8>> {
220 external_table_state_bytes(self.db, entry)
221 }
222
223 pub fn read_state(&self, entry: &ExternalTableEntry, key: &[u8]) -> Result<Option<Vec<u8>>> {
224 ExternalTxn::read_state_from_bytes(&self.raw_state(entry)?, key)
225 }
226}
227
228pub struct ExternalPlanRequest<'a> {
229 pub projection: Option<Vec<usize>>,
230 pub filters: Vec<ExternalFilter>,
231 pub raw_filters: Vec<&'a Expr>,
232 pub order_by: Vec<ExternalOrder>,
233 pub limit: Option<usize>,
234 pub offset: Option<usize>,
235}
236
237pub struct ExternalScan {
238 pub schema: SchemaRef,
239 pub batches: Vec<RecordBatch>,
240}
241
242#[derive(Clone)]
243pub struct ExternalPlan {
244 pub filter_pushdown: Vec<TableProviderFilterPushDown>,
245 pub accepted_filters: Vec<AcceptedFilter>,
246 pub residual_filters_required: bool,
247 pub estimated_rows: Option<u64>,
248 pub estimated_cost: f64,
249 pub order_satisfied: bool,
250 pub opaque: Arc<dyn Any + Send + Sync>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Hash)]
254pub struct ExternalOrder {
255 pub column_index: usize,
256 pub descending: bool,
257 pub nulls_first: Option<bool>,
258}
259
260#[derive(Debug, Clone, PartialEq)]
261pub enum ExternalFilter {
262 And(Vec<ExternalFilter>),
263 Compare {
264 column_index: usize,
265 op: ExternalFilterOp,
266 value: ScalarValue,
267 },
268 Unsupported {
269 expr: String,
270 },
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274pub enum ExternalFilterOp {
275 Eq,
276 NotEq,
277 Gt,
278 GtEq,
279 Lt,
280 LtEq,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct AcceptedFilter {
285 pub filter_index: usize,
286 pub pushdown: TableProviderFilterPushDown,
287}
288
289impl ExternalPlan {
290 pub fn new(
291 filter_pushdown: Vec<TableProviderFilterPushDown>,
292 estimated_rows: Option<u64>,
293 estimated_cost: f64,
294 order_satisfied: bool,
295 ) -> Self {
296 let accepted_filters = filter_pushdown
297 .iter()
298 .enumerate()
299 .filter_map(|(filter_index, pushdown)| match pushdown {
300 TableProviderFilterPushDown::Exact | TableProviderFilterPushDown::Inexact => {
301 Some(AcceptedFilter {
302 filter_index,
303 pushdown: pushdown.clone(),
304 })
305 }
306 TableProviderFilterPushDown::Unsupported => None,
307 })
308 .collect();
309 let residual_filters_required = filter_pushdown.iter().any(|pushdown| {
310 matches!(
311 pushdown,
312 TableProviderFilterPushDown::Inexact | TableProviderFilterPushDown::Unsupported
313 )
314 });
315 Self {
316 filter_pushdown,
317 accepted_filters,
318 residual_filters_required,
319 estimated_rows,
320 estimated_cost,
321 order_satisfied,
322 opaque: Arc::new(()),
323 }
324 }
325
326 pub fn with_opaque(mut self, opaque: Arc<dyn Any + Send + Sync>) -> Self {
327 self.opaque = opaque;
328 self
329 }
330}
331
332fn external_filter_from_expr(expr: &Expr, schema: &ArrowSchema) -> ExternalFilter {
333 match expr {
334 Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::And => {
335 ExternalFilter::And(vec![
336 external_filter_from_expr(left, schema),
337 external_filter_from_expr(right, schema),
338 ])
339 }
340 Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
341 let Some(op) = external_filter_op(*op) else {
342 return ExternalFilter::Unsupported {
343 expr: expr.to_string(),
344 };
345 };
346 match (left.as_ref(), right.as_ref()) {
347 (Expr::Column(column), Expr::Literal(value, _)) => schema
348 .index_of(&column.name)
349 .map(|column_index| ExternalFilter::Compare {
350 column_index,
351 op,
352 value: value.clone(),
353 })
354 .unwrap_or_else(|_| ExternalFilter::Unsupported {
355 expr: expr.to_string(),
356 }),
357 (Expr::Literal(value, _), Expr::Column(column)) => schema
358 .index_of(&column.name)
359 .map(|column_index| ExternalFilter::Compare {
360 column_index,
361 op: op.flipped(),
362 value: value.clone(),
363 })
364 .unwrap_or_else(|_| ExternalFilter::Unsupported {
365 expr: expr.to_string(),
366 }),
367 _ => ExternalFilter::Unsupported {
368 expr: expr.to_string(),
369 },
370 }
371 }
372 _ => ExternalFilter::Unsupported {
373 expr: expr.to_string(),
374 },
375 }
376}
377
378fn external_filter_op(op: Operator) -> Option<ExternalFilterOp> {
379 match op {
380 Operator::Eq => Some(ExternalFilterOp::Eq),
381 Operator::NotEq => Some(ExternalFilterOp::NotEq),
382 Operator::Gt => Some(ExternalFilterOp::Gt),
383 Operator::GtEq => Some(ExternalFilterOp::GtEq),
384 Operator::Lt => Some(ExternalFilterOp::Lt),
385 Operator::LtEq => Some(ExternalFilterOp::LtEq),
386 _ => None,
387 }
388}
389
390impl ExternalFilterOp {
391 fn flipped(self) -> Self {
392 match self {
393 ExternalFilterOp::Eq => ExternalFilterOp::Eq,
394 ExternalFilterOp::NotEq => ExternalFilterOp::NotEq,
395 ExternalFilterOp::Gt => ExternalFilterOp::Lt,
396 ExternalFilterOp::GtEq => ExternalFilterOp::LtEq,
397 ExternalFilterOp::Lt => ExternalFilterOp::Gt,
398 ExternalFilterOp::LtEq => ExternalFilterOp::GtEq,
399 }
400 }
401}
402
403#[derive(Clone)]
404pub enum ExternalWriteOp {
405 Insert {
406 rows: Vec<HashMap<u16, Value>>,
407 },
408 ReplaceRows {
409 rows: Vec<HashMap<u16, Value>>,
410 changes: u64,
411 },
412}
413
414#[derive(Debug, Clone, PartialEq)]
415pub enum ExternalBaseWrite {
416 Put {
417 table: String,
418 cells: Vec<(u16, Value)>,
419 },
420 Delete {
421 table: String,
422 row_id: u64,
423 },
424}
425
426fn external_base_write_bytes(op: &ExternalBaseWrite) -> usize {
427 match op {
428 ExternalBaseWrite::Put { table, cells } => table.len().saturating_add(
429 cells
430 .iter()
431 .map(|(_, value)| {
432 std::mem::size_of::<(u16, Value)>().saturating_add(external_value_bytes(value))
433 })
434 .fold(0_usize, usize::saturating_add),
435 ),
436 ExternalBaseWrite::Delete { table, .. } => {
437 table.len().saturating_add(std::mem::size_of::<u64>())
438 }
439 }
440}
441
442#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
443pub struct ExternalWriteResult {
444 pub changes: u64,
445}
446
447impl ExternalWriteResult {
448 pub fn new(changes: u64) -> Self {
449 Self { changes }
450 }
451}
452
453pub struct ExternalTxn {
454 state: Vec<u8>,
455 base_writes: Vec<ExternalBaseWrite>,
456 base_write_bytes: usize,
457}
458
459impl ExternalTxn {
460 pub fn new(state: Vec<u8>) -> Self {
461 Self {
462 state,
463 base_writes: Vec::new(),
464 base_write_bytes: 0,
465 }
466 }
467
468 pub fn read_state_from_bytes(state: &[u8], key: &[u8]) -> Result<Option<Vec<u8>>> {
469 enforce_external_state_limit(state)?;
470 Ok(decode_kv_state(state)?.remove(key))
471 }
472
473 pub fn read_state(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
474 Self::read_state_from_bytes(&self.state, key)
475 }
476
477 pub fn put_state(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
478 enforce_external_state_limit(&self.state)?;
479 if key.len().saturating_add(value.len()) > EXTERNAL_STATE_BYTES_LIMIT {
480 return Err(external_resource_limit(
481 "external transaction key/value bytes",
482 key.len().saturating_add(value.len()),
483 EXTERNAL_STATE_BYTES_LIMIT,
484 ));
485 }
486 let mut state = decode_kv_state(&self.state)?;
487 state.insert(key.to_vec(), value.to_vec());
488 self.state = encode_kv_state(&state)?;
489 Ok(())
490 }
491
492 pub fn delete_state(&mut self, key: &[u8]) -> Result<()> {
493 enforce_external_state_limit(&self.state)?;
494 let mut state = decode_kv_state(&self.state)?;
495 state.remove(key);
496 self.state = encode_kv_state(&state)?;
497 Ok(())
498 }
499
500 pub fn replace_state(&mut self, state: Vec<u8>) {
501 self.state = state;
502 }
503
504 pub fn read_rows(&self) -> Result<Vec<HashMap<u16, Value>>> {
505 enforce_external_state_limit(&self.state)?;
506 if self.state.is_empty() {
507 Ok(Vec::new())
508 } else {
509 decode_state_rows(&self.state)
510 }
511 }
512
513 pub fn replace_rows(
514 &mut self,
515 schema: &CoreSchema,
516 rows: Vec<HashMap<u16, Value>>,
517 ) -> Result<()> {
518 enforce_external_rows_limit(&rows, None)?;
519 validate_external_rows(schema, &rows)?;
520 self.state = encode_state_rows(&rows)?;
521 Ok(())
522 }
523
524 pub fn emit_base_write(&mut self, op: ExternalBaseWrite) -> Result<()> {
525 if self.base_writes.len() >= EXTERNAL_BASE_WRITES_LIMIT {
526 return Err(external_resource_limit(
527 "external base write count",
528 self.base_writes.len().saturating_add(1),
529 EXTERNAL_BASE_WRITES_LIMIT,
530 ));
531 }
532 let bytes = external_base_write_bytes(&op);
533 let requested = self.base_write_bytes.saturating_add(bytes);
534 if requested > EXTERNAL_BASE_WRITE_BYTES_LIMIT {
535 return Err(external_resource_limit(
536 "external base write bytes",
537 requested,
538 EXTERNAL_BASE_WRITE_BYTES_LIMIT,
539 ));
540 }
541 self.base_write_bytes = requested;
542 self.base_writes.push(op);
543 Ok(())
544 }
545
546 fn into_parts(self) -> (Vec<u8>, Vec<ExternalBaseWrite>) {
547 (self.state, self.base_writes)
548 }
549}
550
551pub struct ExternalModuleRegistry {
552 modules: parking_lot::RwLock<HashMap<String, Arc<dyn ExternalTableModule>>>,
553}
554
555impl Default for ExternalModuleRegistry {
556 fn default() -> Self {
557 let registry = Self {
558 modules: parking_lot::RwLock::new(HashMap::new()),
559 };
560 for module in builtin_modules() {
561 registry.register(module).expect("valid built-in module");
562 }
563 registry
564 }
565}
566
567impl ExternalModuleRegistry {
568 pub fn register(&self, module: Arc<dyn ExternalTableModule>) -> Result<()> {
569 let name = normalize_module_name(module.name())?;
570 self.modules.write().insert(name, module);
571 Ok(())
572 }
573
574 pub fn names(&self) -> Vec<String> {
575 let mut names = self.modules.read().keys().cloned().collect::<Vec<_>>();
576 names.sort();
577 names
578 }
579
580 pub(crate) fn contains(&self, name: &str) -> bool {
581 normalize_module_name(name)
582 .ok()
583 .is_some_and(|name| self.modules.read().contains_key(&name))
584 }
585
586 pub(crate) fn descriptor(&self, name: &str) -> Result<ExternalModuleDescriptor> {
587 Ok(self.module(name)?.descriptor())
588 }
589
590 pub(crate) fn external_table_provider(
591 &self,
592 db: &Arc<Database>,
593 entry: &ExternalTableEntry,
594 ) -> Result<Arc<dyn TableProvider>> {
595 let module = self.module(&entry.module)?;
596 let table = module.connect(&ModuleConnectCtx { db }, entry)?;
597 Ok(Arc::new(ExternalTableProvider::new(
598 Arc::clone(db),
599 table,
600 entry.capabilities,
601 )))
602 }
603
604 pub(crate) fn external_table_indexes(
605 &self,
606 entry: &ExternalTableEntry,
607 ) -> Result<Vec<ExternalModuleIndex>> {
608 let module = self.module(&entry.module)?;
609 module.indexes(entry)
610 }
611
612 pub(crate) fn external_table_rows(
613 &self,
614 db: &Arc<Database>,
615 entry: &ExternalTableEntry,
616 ) -> Result<Vec<HashMap<u16, Value>>> {
617 let module = self.module(&entry.module)?;
618 let rows = module.read_rows(&ModuleConnectCtx { db }, entry)?;
619 enforce_external_rows_limit(&rows, None)?;
620 Ok(rows)
621 }
622
623 pub(crate) fn external_table_rows_from_state(
624 &self,
625 entry: &ExternalTableEntry,
626 state: &[u8],
627 ) -> Result<Vec<HashMap<u16, Value>>> {
628 enforce_external_state_limit(state)?;
629 let module = self.module(&entry.module)?;
630 let rows = module.rows_from_state(state)?;
631 enforce_external_rows_limit(&rows, None)?;
632 Ok(rows)
633 }
634
635 pub(crate) fn external_table_write(
636 &self,
637 db: &Arc<Database>,
638 entry: &ExternalTableEntry,
639 base_state: Vec<u8>,
640 op: ExternalWriteOp,
641 ) -> Result<(Vec<u8>, ExternalWriteResult, Vec<ExternalBaseWrite>)> {
642 enforce_external_state_limit(&base_state)?;
643 match &op {
644 ExternalWriteOp::Insert { rows } | ExternalWriteOp::ReplaceRows { rows, .. } => {
645 enforce_external_rows_limit(rows, None)?;
646 }
647 }
648 let module = self.module(&entry.module)?;
649 let mut txn = ExternalTxn::new(base_state);
650 let result = module.write(&ModuleConnectCtx { db }, entry, op, &mut txn)?;
651 let (state, base_writes) = txn.into_parts();
652 enforce_external_state_limit(&state)?;
653 Ok((state, result, base_writes))
654 }
655
656 pub(crate) fn destroy_external_table(
657 &self,
658 db: &Arc<Database>,
659 entry: &ExternalTableEntry,
660 ) -> Result<()> {
661 let module = self.module(&entry.module)?;
662 module.destroy(&ModuleConnectCtx { db }, entry)
663 }
664
665 fn module(&self, name: &str) -> Result<Arc<dyn ExternalTableModule>> {
666 let name = normalize_module_name(name)?;
667 self.modules.read().get(&name).cloned().ok_or_else(|| {
668 MongrelQueryError::Schema(format!("external table module {name:?} is not registered"))
669 })
670 }
671}
672
673fn normalize_module_name(name: &str) -> Result<String> {
674 let name = name.trim().to_ascii_lowercase();
675 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
676 return Err(MongrelQueryError::Schema(format!(
677 "invalid external table module name {name:?}"
678 )));
679 }
680 Ok(name)
681}
682
683fn builtin_modules() -> Vec<Arc<dyn ExternalTableModule>> {
684 vec![
685 Arc::new(JsonModule {
686 name: "json_each",
687 mode: JsonTableMode::Each,
688 }),
689 Arc::new(JsonModule {
690 name: "json_tree",
691 mode: JsonTableMode::Tree,
692 }),
693 Arc::new(JsonModule {
694 name: "jsonb_each",
695 mode: JsonTableMode::Each,
696 }),
697 Arc::new(JsonModule {
698 name: "jsonb_tree",
699 mode: JsonTableMode::Tree,
700 }),
701 Arc::new(SchemaTablesModule),
702 Arc::new(DbStatModule),
703 Arc::new(FtsDocsModule),
704 Arc::new(KvStoreModule),
705 Arc::new(RTreeRectsModule),
706 Arc::new(SeriesModule),
707 ]
708}
709
710struct ExternalTableProvider {
711 db: Arc<Database>,
712 table: Arc<dyn ExternalTable>,
713 cache_plans: bool,
714 plan_cache: parking_lot::Mutex<HashMap<ExternalPlanCacheKey, ExternalPlan>>,
715}
716
717struct ExternalTableExec {
718 props: Arc<PlanProperties>,
719 schema: SchemaRef,
720 db: Arc<Database>,
721 table: Arc<dyn ExternalTable>,
722 projection: Option<Vec<usize>>,
723 filters: Vec<Expr>,
724 limit: Option<usize>,
725}
726
727impl std::fmt::Debug for ExternalTableExec {
728 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729 f.debug_struct("ExternalTableExec").finish_non_exhaustive()
730 }
731}
732
733impl DisplayAs for ExternalTableExec {
734 fn fmt_as(
735 &self,
736 _format: DisplayFormatType,
737 f: &mut std::fmt::Formatter<'_>,
738 ) -> std::fmt::Result {
739 write!(f, "ExternalTableExec")
740 }
741}
742
743impl ExecutionPlan for ExternalTableExec {
744 fn name(&self) -> &str {
745 "ExternalTableExec"
746 }
747
748 fn properties(&self) -> &Arc<PlanProperties> {
749 &self.props
750 }
751
752 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
753 Vec::new()
754 }
755
756 fn with_new_children(
757 self: Arc<Self>,
758 children: Vec<Arc<dyn ExecutionPlan>>,
759 ) -> DFResult<Arc<dyn ExecutionPlan>> {
760 if children.is_empty() {
761 Ok(self)
762 } else {
763 Err(DataFusionError::Internal(
764 "ExternalTableExec is a leaf node and has no children".into(),
765 ))
766 }
767 }
768
769 fn execute(
770 &self,
771 partition: usize,
772 task_context: Arc<TaskContext>,
773 ) -> DFResult<SendableRecordBatchStream> {
774 use futures::TryStreamExt;
775
776 self.db
777 .ensure_consistent_read()
778 .map_err(|error| DataFusionError::Execution(error.to_string()))?;
779 if partition != 0 {
780 return Err(DataFusionError::Internal(format!(
781 "ExternalTableExec is single-partition; invalid partition {partition}"
782 )));
783 }
784 let query = task_context
785 .session_config()
786 .get_extension::<SqlTaskContext>()
787 .map(|context| context.query().clone());
788 let control = query
789 .as_ref()
790 .map(|query| query.control().child_with_deadline(None))
791 .unwrap_or_else(|| mongreldb_core::ExecutionControl::new(None));
792 control
793 .checkpoint()
794 .map_err(|error| external_control_error(query.as_ref(), error))?;
795 let table = Arc::clone(&self.table);
796 let projection = self.projection.clone();
797 let filters = self.filters.clone();
798 let limit = self.limit;
799 let worker_control = control.clone();
800 let future = async move {
801 let mut task = tokio::task::spawn_blocking(move || {
802 let schema = table.schema();
803 let request = ExternalPlanRequest {
804 projection,
805 filters: filters
806 .iter()
807 .map(|filter| external_filter_from_expr(filter, &schema))
808 .collect(),
809 raw_filters: filters.iter().collect(),
810 order_by: Vec::new(),
811 limit,
812 offset: None,
813 };
814 table.scan_with_control(&request, &worker_control)
815 });
816 let scan = tokio::select! {
817 result = &mut task => result.map_err(|error| {
818 DataFusionError::Execution(format!("external table worker failed: {error}"))
819 })??,
820 _ = control.cancelled() => {
821 if tokio::time::timeout(std::time::Duration::from_millis(100), &mut task)
822 .await
823 .is_err()
824 {
825 eprintln!("external table worker exceeded cancellation grace");
826 }
827 return Err(external_control_error_from_control(query.as_ref(), &control));
828 }
829 };
830 control
831 .checkpoint()
832 .map_err(|error| external_control_error(query.as_ref(), error))?;
833 Ok(futures::stream::iter(scan.batches.into_iter().map(Ok)))
834 };
835 let stream = futures::stream::once(future).try_flatten();
836 Ok(Box::pin(RecordBatchStreamAdapter::new(
837 Arc::clone(&self.schema),
838 stream,
839 )))
840 }
841}
842
843fn external_control_error(
844 query: Option<&crate::RegisteredSqlQuery>,
845 error: mongreldb_core::MongrelError,
846) -> DataFusionError {
847 if let Some(error) = query.and_then(|query| query.checkpoint().err()) {
848 DataFusionError::External(Box::new(error))
849 } else {
850 DataFusionError::Execution(error.to_string())
851 }
852}
853
854fn external_control_error_from_control(
855 query: Option<&crate::RegisteredSqlQuery>,
856 control: &mongreldb_core::ExecutionControl,
857) -> DataFusionError {
858 external_control_error(
859 query,
860 control
861 .checkpoint()
862 .expect_err("cancelled external control must fail its checkpoint"),
863 )
864}
865
866#[derive(Debug, Clone, PartialEq, Eq, Hash)]
867struct ExternalPlanCacheKey {
868 projection: Option<Vec<usize>>,
869 filters: Vec<String>,
870 order_by: Vec<ExternalOrder>,
871 limit: Option<usize>,
872 offset: Option<usize>,
873}
874
875impl ExternalPlanCacheKey {
876 fn from_request(request: &ExternalPlanRequest<'_>) -> Self {
877 Self {
878 projection: request.projection.clone(),
879 filters: request
880 .filters
881 .iter()
882 .map(|filter| format!("{filter:?}"))
883 .collect(),
884 order_by: request.order_by.clone(),
885 limit: request.limit,
886 offset: request.offset,
887 }
888 }
889}
890
891impl ExternalTableProvider {
892 fn new(
893 db: Arc<Database>,
894 table: Arc<dyn ExternalTable>,
895 capabilities: ModuleCapabilities,
896 ) -> Self {
897 Self {
898 db,
899 table,
900 cache_plans: capabilities.read_only && capabilities.deterministic,
901 plan_cache: parking_lot::Mutex::new(HashMap::new()),
902 }
903 }
904
905 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
906 if !self.cache_plans {
907 return self.table.plan(request);
908 }
909 let key = ExternalPlanCacheKey::from_request(request);
910 if let Some(plan) = self.plan_cache.lock().get(&key).cloned() {
911 return Ok(plan);
912 }
913 let plan = self.table.plan(request)?;
914 self.plan_cache.lock().insert(key, plan.clone());
915 Ok(plan)
916 }
917}
918
919impl std::fmt::Debug for ExternalTableProvider {
920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
921 f.debug_struct("ExternalTableProvider")
922 .field("table", &self.table)
923 .finish()
924 }
925}
926
927#[async_trait::async_trait]
928impl TableProvider for ExternalTableProvider {
929 fn schema(&self) -> SchemaRef {
930 self.table.schema()
931 }
932
933 fn table_type(&self) -> TableType {
934 TableType::Base
935 }
936
937 fn supports_filters_pushdown(
938 &self,
939 filters: &[&Expr],
940 ) -> DFResult<Vec<TableProviderFilterPushDown>> {
941 let schema = self.table.schema();
942 let request = ExternalPlanRequest {
943 projection: None,
944 filters: filters
945 .iter()
946 .map(|filter| external_filter_from_expr(filter, &schema))
947 .collect(),
948 raw_filters: filters.to_vec(),
949 order_by: Vec::new(),
950 limit: None,
951 offset: None,
952 };
953 let ExternalPlan {
954 filter_pushdown,
955 accepted_filters: _accepted_filters,
956 residual_filters_required: _residual_filters_required,
957 estimated_rows: _estimated_rows,
958 estimated_cost: _estimated_cost,
959 order_satisfied: _order_satisfied,
960 opaque: _opaque,
961 } = self.plan(&request)?;
962 Ok(filter_pushdown)
963 }
964
965 async fn scan(
966 &self,
967 _state: &dyn Session,
968 projection: Option<&Vec<usize>>,
969 filters: &[Expr],
970 limit: Option<usize>,
971 ) -> DFResult<Arc<dyn ExecutionPlan>> {
972 mongreldb_core::trace::QueryTrace::record(|t| {
973 t.scan_mode = mongreldb_core::trace::ScanMode::ExternalModule;
974 });
975 let schema = self.table.schema();
976 let request = ExternalPlanRequest {
977 projection: projection.cloned(),
978 filters: filters
979 .iter()
980 .map(|filter| external_filter_from_expr(filter, &schema))
981 .collect(),
982 raw_filters: filters.iter().collect(),
983 order_by: Vec::new(),
984 limit,
985 offset: None,
986 };
987 let _plan = self.plan(&request)?;
988 let output_schema = match projection {
989 Some(projection) => Arc::new(schema.project(projection)?),
990 None => schema,
991 };
992 let props = Arc::new(PlanProperties::new(
993 EquivalenceProperties::new(Arc::clone(&output_schema)),
994 Partitioning::UnknownPartitioning(1),
995 EmissionType::Incremental,
996 Boundedness::Bounded,
997 ));
998 Ok(Arc::new(ExternalTableExec {
999 props,
1000 schema: output_schema,
1001 db: Arc::clone(&self.db),
1002 table: Arc::clone(&self.table),
1003 projection: projection.cloned(),
1004 filters: filters.to_vec(),
1005 limit,
1006 }))
1007 }
1008}
1009
1010struct JsonModule {
1011 name: &'static str,
1012 mode: JsonTableMode,
1013}
1014
1015impl ExternalTableModule for JsonModule {
1016 fn name(&self) -> &str {
1017 self.name
1018 }
1019
1020 fn descriptor(&self) -> ExternalModuleDescriptor {
1021 json_descriptor()
1022 }
1023
1024 fn connect(
1025 &self,
1026 _ctx: &ModuleConnectCtx<'_>,
1027 entry: &ExternalTableEntry,
1028 ) -> Result<Arc<dyn ExternalTable>> {
1029 let (json, root) = json_args(entry, self.name)?;
1030 let (schema, batches) =
1031 json_table_batches_from_text(self.name, &json, root.as_deref(), self.mode)
1032 .map_err(|e| MongrelQueryError::DataFusion(e.to_string()))?;
1033 Ok(Arc::new(JsonExternalTable {
1034 module: self.name,
1035 schema,
1036 batches,
1037 }))
1038 }
1039}
1040
1041#[derive(Clone)]
1042struct JsonExternalTable {
1043 module: &'static str,
1044 schema: SchemaRef,
1045 batches: Vec<RecordBatch>,
1046}
1047
1048impl std::fmt::Debug for JsonExternalTable {
1049 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1050 f.debug_struct("JsonExternalTable")
1051 .field("module", &self.module)
1052 .field("schema", &self.schema)
1053 .finish_non_exhaustive()
1054 }
1055}
1056
1057impl ExternalTable for JsonExternalTable {
1058 fn schema(&self) -> SchemaRef {
1059 self.schema.clone()
1060 }
1061
1062 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1063 Ok(ExternalPlan::new(
1064 request
1065 .filters
1066 .iter()
1067 .map(|_| TableProviderFilterPushDown::Unsupported)
1068 .collect(),
1069 None,
1070 1.0,
1071 false,
1072 ))
1073 }
1074
1075 fn scan_with_control(
1076 &self,
1077 request: &ExternalPlanRequest<'_>,
1078 control: &mongreldb_core::ExecutionControl,
1079 ) -> DFResult<ExternalScan> {
1080 project_scan_with_control(
1081 self.schema.clone(),
1082 self.batches.clone(),
1083 request.projection.as_deref(),
1084 request.limit,
1085 Some(control),
1086 )
1087 }
1088}
1089
1090fn json_descriptor() -> ExternalModuleDescriptor {
1091 ExternalModuleDescriptor {
1092 schema: CoreSchema {
1093 schema_id: 0,
1094 columns: vec![
1095 json_column(1, "key", TypeId::Bytes, true),
1096 json_column(2, "value", TypeId::Bytes, true),
1097 json_column(3, "type", TypeId::Bytes, true),
1098 json_column(4, "atom", TypeId::Bytes, true),
1099 json_column(5, "id", TypeId::Int64, false),
1100 json_column(6, "parent", TypeId::Int64, true),
1101 json_column(7, "fullkey", TypeId::Bytes, true),
1102 json_column(8, "path", TypeId::Bytes, true),
1103 json_column(9, "json", TypeId::Bytes, true),
1104 json_column(10, "root", TypeId::Bytes, true),
1105 ],
1106 indexes: Vec::new(),
1107 colocation: Vec::new(),
1108 constraints: Default::default(),
1109 clustered: false,
1110 },
1111 hidden_columns: vec!["json".to_string(), "root".to_string()],
1112 capabilities: ModuleCapabilities {
1113 read_only: true,
1114 deterministic: true,
1115 trigger_safe: true,
1116 ..ModuleCapabilities::default()
1117 },
1118 }
1119}
1120
1121fn json_column(id: u16, name: &str, ty: TypeId, nullable: bool) -> CoreColumnDef {
1122 CoreColumnDef {
1123 id,
1124 name: name.to_string(),
1125 ty,
1126 flags: if nullable {
1127 ColumnFlags::empty().with(ColumnFlags::NULLABLE)
1128 } else {
1129 ColumnFlags::empty()
1130 },
1131 default_value: None,
1132 }
1133}
1134
1135fn json_args(entry: &ExternalTableEntry, module: &str) -> Result<(String, Option<String>)> {
1136 match entry.args.as_slice() {
1137 [json] => Ok((module_arg_string(json).to_string(), None)),
1138 [json, root] => Ok((
1139 module_arg_string(json).to_string(),
1140 Some(module_arg_string(root).to_string()),
1141 )),
1142 _ => Err(MongrelQueryError::Schema(format!(
1143 "{module} external table requires one JSON argument and an optional root path"
1144 ))),
1145 }
1146}
1147
1148fn module_arg_string(arg: &ModuleArg) -> &str {
1149 match arg {
1150 ModuleArg::Ident(value) | ModuleArg::String(value) | ModuleArg::Number(value) => value,
1151 }
1152}
1153
1154struct SchemaTablesModule;
1155
1156impl ExternalTableModule for SchemaTablesModule {
1157 fn name(&self) -> &str {
1158 "schema_tables"
1159 }
1160
1161 fn descriptor(&self) -> ExternalModuleDescriptor {
1162 ExternalModuleDescriptor {
1163 schema: CoreSchema {
1164 schema_id: 0,
1165 columns: vec![
1166 catalog_column(1, "schema_name", TypeId::Bytes, false),
1167 catalog_column(2, "name", TypeId::Bytes, false),
1168 catalog_column(3, "type", TypeId::Bytes, false),
1169 catalog_column(4, "ncol", TypeId::Int64, false),
1170 catalog_column(5, "module", TypeId::Bytes, true),
1171 catalog_column(6, "created_epoch", TypeId::Int64, false),
1172 ],
1173 indexes: Vec::new(),
1174 colocation: Vec::new(),
1175 constraints: Default::default(),
1176 clustered: false,
1177 },
1178 hidden_columns: Vec::new(),
1179 capabilities: catalog_capabilities(),
1180 }
1181 }
1182
1183 fn connect(
1184 &self,
1185 ctx: &ModuleConnectCtx<'_>,
1186 entry: &ExternalTableEntry,
1187 ) -> Result<Arc<dyn ExternalTable>> {
1188 ensure_no_args(entry, self.name())?;
1189 Ok(Arc::new(SchemaTablesExternalTable {
1190 db: Arc::clone(ctx.db),
1191 schema: schema_tables_arrow_schema(),
1192 }))
1193 }
1194}
1195
1196#[derive(Clone)]
1197struct SchemaTablesExternalTable {
1198 db: Arc<Database>,
1199 schema: SchemaRef,
1200}
1201
1202impl std::fmt::Debug for SchemaTablesExternalTable {
1203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1204 f.debug_struct("SchemaTablesExternalTable")
1205 .field("schema", &self.schema)
1206 .finish_non_exhaustive()
1207 }
1208}
1209
1210impl ExternalTable for SchemaTablesExternalTable {
1211 fn schema(&self) -> SchemaRef {
1212 self.schema.clone()
1213 }
1214
1215 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1216 unsupported_plan(request)
1217 }
1218
1219 fn scan_with_control(
1220 &self,
1221 request: &ExternalPlanRequest<'_>,
1222 control: &mongreldb_core::ExecutionControl,
1223 ) -> DFResult<ExternalScan> {
1224 external_checkpoint(Some(control), 0)?;
1225 let batches = schema_tables_batches(&self.db, self.schema.clone(), Some(control))?;
1226 project_scan_with_control(
1227 self.schema.clone(),
1228 batches,
1229 request.projection.as_deref(),
1230 request.limit,
1231 Some(control),
1232 )
1233 }
1234}
1235
1236struct DbStatModule;
1237
1238impl ExternalTableModule for DbStatModule {
1239 fn name(&self) -> &str {
1240 "dbstat"
1241 }
1242
1243 fn descriptor(&self) -> ExternalModuleDescriptor {
1244 ExternalModuleDescriptor {
1245 schema: CoreSchema {
1246 schema_id: 0,
1247 columns: vec![
1248 catalog_column(1, "name", TypeId::Bytes, false),
1249 catalog_column(2, "type", TypeId::Bytes, false),
1250 catalog_column(3, "rows", TypeId::Int64, true),
1251 catalog_column(4, "runs", TypeId::Int64, true),
1252 catalog_column(5, "memtable_rows", TypeId::Int64, true),
1253 catalog_column(6, "columns", TypeId::Int64, false),
1254 catalog_column(7, "storage_bytes", TypeId::Int64, false),
1255 ],
1256 indexes: Vec::new(),
1257 colocation: Vec::new(),
1258 constraints: Default::default(),
1259 clustered: false,
1260 },
1261 hidden_columns: Vec::new(),
1262 capabilities: catalog_capabilities(),
1263 }
1264 }
1265
1266 fn connect(
1267 &self,
1268 ctx: &ModuleConnectCtx<'_>,
1269 entry: &ExternalTableEntry,
1270 ) -> Result<Arc<dyn ExternalTable>> {
1271 ensure_no_args(entry, self.name())?;
1272 Ok(Arc::new(DbStatExternalTable {
1273 db: Arc::clone(ctx.db),
1274 schema: dbstat_arrow_schema(),
1275 }))
1276 }
1277}
1278
1279#[derive(Clone)]
1280struct DbStatExternalTable {
1281 db: Arc<Database>,
1282 schema: SchemaRef,
1283}
1284
1285impl std::fmt::Debug for DbStatExternalTable {
1286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1287 f.debug_struct("DbStatExternalTable")
1288 .field("schema", &self.schema)
1289 .finish_non_exhaustive()
1290 }
1291}
1292
1293impl ExternalTable for DbStatExternalTable {
1294 fn schema(&self) -> SchemaRef {
1295 self.schema.clone()
1296 }
1297
1298 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1299 unsupported_plan(request)
1300 }
1301
1302 fn scan_with_control(
1303 &self,
1304 request: &ExternalPlanRequest<'_>,
1305 control: &mongreldb_core::ExecutionControl,
1306 ) -> DFResult<ExternalScan> {
1307 external_checkpoint(Some(control), 0)?;
1308 let batches = dbstat_batches(&self.db, self.schema.clone(), Some(control))?;
1309 project_scan_with_control(
1310 self.schema.clone(),
1311 batches,
1312 request.projection.as_deref(),
1313 request.limit,
1314 Some(control),
1315 )
1316 }
1317}
1318
1319fn catalog_column(id: u16, name: &str, ty: TypeId, nullable: bool) -> CoreColumnDef {
1320 CoreColumnDef {
1321 id,
1322 name: name.to_string(),
1323 ty,
1324 flags: if nullable {
1325 ColumnFlags::empty().with(ColumnFlags::NULLABLE)
1326 } else {
1327 ColumnFlags::empty()
1328 },
1329 default_value: None,
1330 }
1331}
1332
1333fn catalog_capabilities() -> ModuleCapabilities {
1334 ModuleCapabilities {
1335 read_only: true,
1336 trigger_safe: true,
1337 ..ModuleCapabilities::default()
1338 }
1339}
1340
1341fn ensure_no_args(entry: &ExternalTableEntry, module: &str) -> Result<()> {
1342 if entry.args.is_empty() {
1343 Ok(())
1344 } else {
1345 Err(MongrelQueryError::Schema(format!(
1346 "{module} external table does not accept arguments"
1347 )))
1348 }
1349}
1350
1351fn schema_tables_arrow_schema() -> SchemaRef {
1352 Arc::new(ArrowSchema::new(vec![
1353 Field::new("schema_name", DataType::Utf8, false),
1354 Field::new("name", DataType::Utf8, false),
1355 Field::new("type", DataType::Utf8, false),
1356 Field::new("ncol", DataType::Int64, false),
1357 Field::new("module", DataType::Utf8, true),
1358 Field::new("created_epoch", DataType::Int64, false),
1359 ]))
1360}
1361
1362fn schema_tables_batches(
1363 db: &Arc<Database>,
1364 schema: SchemaRef,
1365 control: Option<&mongreldb_core::ExecutionControl>,
1366) -> DFResult<Vec<RecordBatch>> {
1367 struct Row {
1368 name: String,
1369 ty: String,
1370 ncol: i64,
1371 module: Option<String>,
1372 created_epoch: i64,
1373 }
1374
1375 let catalog = db.catalog_snapshot();
1376 let mut rows = Vec::new();
1377 for (index, table) in catalog
1378 .tables
1379 .into_iter()
1380 .filter(|table| matches!(table.state, TableState::Live))
1381 .enumerate()
1382 {
1383 external_checkpoint(control, index)?;
1384 rows.push(Row {
1385 name: table.name,
1386 ty: "table".to_string(),
1387 ncol: saturating_i64(table.schema.columns.len() as u64),
1388 module: None,
1389 created_epoch: saturating_i64(table.created_epoch),
1390 });
1391 }
1392 for (index, table) in catalog.external_tables.into_iter().enumerate() {
1393 external_checkpoint(control, index)?;
1394 rows.push(Row {
1395 name: table.name,
1396 ty: "external".to_string(),
1397 ncol: saturating_i64(table.declared_schema.columns.len() as u64),
1398 module: Some(table.module),
1399 created_epoch: saturating_i64(table.created_epoch),
1400 });
1401 }
1402 rows.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.ty.cmp(&b.ty)));
1403
1404 let schema_names = vec!["main".to_string(); rows.len()];
1405 let names = rows.iter().map(|row| row.name.clone()).collect::<Vec<_>>();
1406 let types = rows.iter().map(|row| row.ty.clone()).collect::<Vec<_>>();
1407 let ncols = rows.iter().map(|row| row.ncol).collect::<Vec<_>>();
1408 let modules = rows
1409 .iter()
1410 .map(|row| row.module.clone())
1411 .collect::<Vec<_>>();
1412 let created_epochs = rows.iter().map(|row| row.created_epoch).collect::<Vec<_>>();
1413
1414 Ok(vec![RecordBatch::try_new(
1415 schema,
1416 vec![
1417 Arc::new(StringArray::from(schema_names)) as ArrayRef,
1418 Arc::new(StringArray::from(names)),
1419 Arc::new(StringArray::from(types)),
1420 Arc::new(Int64Array::from(ncols)),
1421 Arc::new(StringArray::from(modules)),
1422 Arc::new(Int64Array::from(created_epochs)),
1423 ],
1424 )?])
1425}
1426
1427fn dbstat_arrow_schema() -> SchemaRef {
1428 Arc::new(ArrowSchema::new(vec![
1429 Field::new("name", DataType::Utf8, false),
1430 Field::new("type", DataType::Utf8, false),
1431 Field::new("rows", DataType::Int64, true),
1432 Field::new("runs", DataType::Int64, true),
1433 Field::new("memtable_rows", DataType::Int64, true),
1434 Field::new("columns", DataType::Int64, false),
1435 Field::new("storage_bytes", DataType::Int64, false),
1436 ]))
1437}
1438
1439fn dbstat_batches(
1440 db: &Arc<Database>,
1441 schema: SchemaRef,
1442 control: Option<&mongreldb_core::ExecutionControl>,
1443) -> DFResult<Vec<RecordBatch>> {
1444 struct Row {
1445 name: String,
1446 ty: String,
1447 rows: Option<i64>,
1448 runs: Option<i64>,
1449 memtable_rows: Option<i64>,
1450 columns: i64,
1451 storage_bytes: i64,
1452 }
1453
1454 let catalog = db.catalog_snapshot();
1455 let mut rows = Vec::new();
1456 for (index, table) in catalog
1457 .tables
1458 .into_iter()
1459 .filter(|table| matches!(table.state, TableState::Live))
1460 .enumerate()
1461 {
1462 external_checkpoint(control, index)?;
1463 let handle = db
1464 .table(&table.name)
1465 .map_err(|e| DataFusionError::Execution(e.to_string()))?;
1466 let table_guard = handle.lock();
1467 let table_dir = db.root().join(TABLES_DIR).join(table.table_id.to_string());
1468 rows.push(Row {
1469 name: table.name,
1470 ty: "table".to_string(),
1471 rows: Some(saturating_i64(table_guard.count())),
1472 runs: Some(saturating_i64(table_guard.run_count() as u64)),
1473 memtable_rows: Some(saturating_i64(table_guard.memtable_len() as u64)),
1474 columns: saturating_i64(table.schema.columns.len() as u64),
1475 storage_bytes: saturating_i64(dir_size(&table_dir, control)?),
1476 });
1477 }
1478 for (index, table) in catalog.external_tables.into_iter().enumerate() {
1479 external_checkpoint(control, index)?;
1480 let state_dir = db.root().join(VTAB_DIR).join(&table.name);
1481 rows.push(Row {
1482 name: table.name,
1483 ty: "external".to_string(),
1484 rows: None,
1485 runs: None,
1486 memtable_rows: None,
1487 columns: saturating_i64(table.declared_schema.columns.len() as u64),
1488 storage_bytes: saturating_i64(dir_size(&state_dir, control)?),
1489 });
1490 }
1491 rows.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.ty.cmp(&b.ty)));
1492
1493 let names = rows.iter().map(|row| row.name.clone()).collect::<Vec<_>>();
1494 let types = rows.iter().map(|row| row.ty.clone()).collect::<Vec<_>>();
1495 let row_counts = rows.iter().map(|row| row.rows).collect::<Vec<_>>();
1496 let runs = rows.iter().map(|row| row.runs).collect::<Vec<_>>();
1497 let memtable_rows = rows.iter().map(|row| row.memtable_rows).collect::<Vec<_>>();
1498 let columns = rows.iter().map(|row| row.columns).collect::<Vec<_>>();
1499 let storage_bytes = rows.iter().map(|row| row.storage_bytes).collect::<Vec<_>>();
1500
1501 Ok(vec![RecordBatch::try_new(
1502 schema,
1503 vec![
1504 Arc::new(StringArray::from(names)) as ArrayRef,
1505 Arc::new(StringArray::from(types)),
1506 Arc::new(Int64Array::from(row_counts)),
1507 Arc::new(Int64Array::from(runs)),
1508 Arc::new(Int64Array::from(memtable_rows)),
1509 Arc::new(Int64Array::from(columns)),
1510 Arc::new(Int64Array::from(storage_bytes)),
1511 ],
1512 )?])
1513}
1514
1515fn dir_size(path: &Path, control: Option<&mongreldb_core::ExecutionControl>) -> DFResult<u64> {
1516 external_checkpoint(control, 0)?;
1517 if !path.exists() {
1518 return Ok(0);
1519 }
1520 let metadata = std::fs::symlink_metadata(path)
1521 .map_err(|e| DataFusionError::Execution(format!("stat {:?}: {e}", path)))?;
1522 if metadata.is_file() {
1523 return Ok(metadata.len());
1524 }
1525 if !metadata.is_dir() {
1526 return Ok(0);
1527 }
1528
1529 let mut total = 0_u64;
1530 for (index, entry) in std::fs::read_dir(path)
1531 .map_err(|e| DataFusionError::Execution(format!("{e}")))?
1532 .enumerate()
1533 {
1534 external_checkpoint(control, index)?;
1535 let entry = entry.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
1536 total = total.saturating_add(dir_size(&entry.path(), control)?);
1537 }
1538 Ok(total)
1539}
1540
1541fn saturating_i64(value: u64) -> i64 {
1542 i64::try_from(value).unwrap_or(i64::MAX)
1543}
1544
1545fn unsupported_plan(request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1546 Ok(ExternalPlan::new(
1547 request
1548 .filters
1549 .iter()
1550 .map(|_| TableProviderFilterPushDown::Unsupported)
1551 .collect(),
1552 None,
1553 1.0,
1554 false,
1555 ))
1556}
1557
1558#[cfg(test)]
1559fn project_scan(
1560 full_schema: SchemaRef,
1561 batches: Vec<RecordBatch>,
1562 projection: Option<&[usize]>,
1563 limit: Option<usize>,
1564) -> DFResult<ExternalScan> {
1565 project_scan_with_control(full_schema, batches, projection, limit, None)
1566}
1567
1568fn project_scan_with_control(
1569 full_schema: SchemaRef,
1570 batches: Vec<RecordBatch>,
1571 projection: Option<&[usize]>,
1572 limit: Option<usize>,
1573 control: Option<&mongreldb_core::ExecutionControl>,
1574) -> DFResult<ExternalScan> {
1575 external_checkpoint(control, 0)?;
1576 let Some(projection) = projection else {
1577 return Ok(ExternalScan {
1578 schema: full_schema,
1579 batches: limit_batches(batches, limit, control)?,
1580 });
1581 };
1582 let schema = Arc::new(ArrowSchema::new(
1583 projection
1584 .iter()
1585 .map(|idx| full_schema.field(*idx).clone())
1586 .collect::<Vec<_>>(),
1587 ));
1588 let projected = batches
1589 .into_iter()
1590 .enumerate()
1591 .map(|(index, batch)| {
1592 external_checkpoint(control, index)?;
1593 let columns = projection
1594 .iter()
1595 .map(|idx| batch.column(*idx).clone())
1596 .collect::<Vec<_>>();
1597 let batch = if columns.is_empty() {
1598 RecordBatch::try_new_with_options(
1599 schema.clone(),
1600 columns,
1601 &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
1602 )
1603 } else {
1604 RecordBatch::try_new(schema.clone(), columns)
1605 };
1606 batch.map_err(DataFusionError::from)
1607 })
1608 .collect::<DFResult<Vec<_>>>()?;
1609 Ok(ExternalScan {
1610 schema,
1611 batches: limit_batches(projected, limit, control)?,
1612 })
1613}
1614
1615fn limit_batches(
1616 batches: Vec<RecordBatch>,
1617 limit: Option<usize>,
1618 control: Option<&mongreldb_core::ExecutionControl>,
1619) -> DFResult<Vec<RecordBatch>> {
1620 let Some(mut remaining) = limit else {
1621 return Ok(batches);
1622 };
1623 let mut limited = Vec::new();
1624 for (index, batch) in batches.into_iter().enumerate() {
1625 external_checkpoint(control, index)?;
1626 if remaining == 0 {
1627 break;
1628 }
1629 let take = remaining.min(batch.num_rows());
1630 limited.push(batch.slice(0, take));
1631 remaining -= take;
1632 }
1633 Ok(limited)
1634}
1635
1636#[inline]
1637fn external_checkpoint(
1638 control: Option<&mongreldb_core::ExecutionControl>,
1639 index: usize,
1640) -> DFResult<()> {
1641 if index.is_multiple_of(256) {
1642 control
1643 .map(mongreldb_core::ExecutionControl::checkpoint)
1644 .transpose()
1645 .map_err(|error| DataFusionError::Execution(error.to_string()))?;
1646 }
1647 Ok(())
1648}
1649
1650#[cfg(test)]
1651mod tests {
1652 use super::*;
1653
1654 #[test]
1655 fn project_scan_applies_limit_after_projection() {
1656 let schema = Arc::new(ArrowSchema::new(vec![
1657 Field::new("id", DataType::Int64, false),
1658 Field::new("value", DataType::Int64, false),
1659 ]));
1660 let batch = RecordBatch::try_new(
1661 schema.clone(),
1662 vec![
1663 Arc::new(Int64Array::from(vec![1, 2, 3, 4])) as ArrayRef,
1664 Arc::new(Int64Array::from(vec![10, 11, 12, 13])) as ArrayRef,
1665 ],
1666 )
1667 .unwrap();
1668
1669 let scan = project_scan(schema, vec![batch], Some(&[1]), Some(2)).unwrap();
1670
1671 assert_eq!(scan.schema.fields().len(), 1);
1672 assert_eq!(scan.schema.field(0).name(), "value");
1673 assert_eq!(scan.batches.len(), 1);
1674 assert_eq!(scan.batches[0].num_columns(), 1);
1675 assert_eq!(scan.batches[0].num_rows(), 2);
1676 let values = scan.batches[0]
1677 .column(0)
1678 .as_any()
1679 .downcast_ref::<Int64Array>()
1680 .unwrap();
1681 assert_eq!(values.value(0), 10);
1682 assert_eq!(values.value(1), 11);
1683 }
1684
1685 #[test]
1686 fn external_plan_derives_accepted_and_residual_filter_metadata() {
1687 let plan = ExternalPlan::new(
1688 vec![
1689 TableProviderFilterPushDown::Exact,
1690 TableProviderFilterPushDown::Inexact,
1691 TableProviderFilterPushDown::Unsupported,
1692 ],
1693 Some(42),
1694 3.5,
1695 true,
1696 );
1697
1698 assert_eq!(
1699 plan.accepted_filters,
1700 vec![
1701 AcceptedFilter {
1702 filter_index: 0,
1703 pushdown: TableProviderFilterPushDown::Exact,
1704 },
1705 AcceptedFilter {
1706 filter_index: 1,
1707 pushdown: TableProviderFilterPushDown::Inexact,
1708 },
1709 ]
1710 );
1711 assert!(plan.residual_filters_required);
1712 assert_eq!(plan.estimated_rows, Some(42));
1713 assert_eq!(plan.estimated_cost, 3.5);
1714 assert!(plan.order_satisfied);
1715 }
1716
1717 #[test]
1718 fn external_state_writer_stops_before_over_budget_allocation() {
1719 use std::io::Write as _;
1720
1721 let mut writer = LimitedStateWriter::new(&[]);
1722 writer.requested = EXTERNAL_STATE_BYTES_LIMIT;
1723 assert!(writer.write_all(b"x").is_err());
1724 assert!(writer.bytes.is_empty());
1725 }
1726
1727 #[test]
1728 fn external_base_write_budget_rejects_before_push() {
1729 let mut txn = ExternalTxn::new(Vec::new());
1730 txn.base_write_bytes = EXTERNAL_BASE_WRITE_BYTES_LIMIT;
1731 let error = txn
1732 .emit_base_write(ExternalBaseWrite::Delete {
1733 table: "t".into(),
1734 row_id: 1,
1735 })
1736 .unwrap_err();
1737 assert!(matches!(
1738 error,
1739 MongrelQueryError::Core(mongreldb_core::MongrelError::ResourceLimitExceeded { .. })
1740 ));
1741 assert!(txn.base_writes.is_empty());
1742 }
1743}
1744
1745struct KvStoreModule;
1746
1747impl ExternalTableModule for KvStoreModule {
1748 fn name(&self) -> &str {
1749 "kv_store"
1750 }
1751
1752 fn descriptor(&self) -> ExternalModuleDescriptor {
1753 ExternalModuleDescriptor {
1754 schema: kv_store_schema(),
1755 hidden_columns: Vec::new(),
1756 capabilities: ModuleCapabilities {
1757 writable: true,
1758 deterministic: true,
1759 trigger_safe: true,
1760 transaction_safe: true,
1761 ..ModuleCapabilities::default()
1762 },
1763 }
1764 }
1765
1766 fn connect(
1767 &self,
1768 ctx: &ModuleConnectCtx<'_>,
1769 entry: &ExternalTableEntry,
1770 ) -> Result<Arc<dyn ExternalTable>> {
1771 ensure_no_args(entry, self.name())?;
1772 let rows = read_state_rows(ctx.db, entry)?;
1773 let schema = arrow_conv::arrow_schema(&entry.declared_schema)?;
1774 let batches = core_rows_to_batches(&entry.declared_schema, rows)?;
1775 Ok(Arc::new(KvStoreExternalTable { schema, batches }))
1776 }
1777
1778 fn read_rows(
1779 &self,
1780 ctx: &ModuleConnectCtx<'_>,
1781 entry: &ExternalTableEntry,
1782 ) -> Result<Vec<HashMap<u16, Value>>> {
1783 ensure_no_args(entry, self.name())?;
1784 read_state_rows(ctx.db, entry)
1785 }
1786
1787 fn prepare_rows(
1788 &self,
1789 _ctx: &ModuleConnectCtx<'_>,
1790 entry: &ExternalTableEntry,
1791 rows: Vec<HashMap<u16, Value>>,
1792 ) -> Result<Vec<u8>> {
1793 ensure_no_args(entry, self.name())?;
1794 validate_external_rows(&entry.declared_schema, &rows)?;
1795 encode_state_rows(&rows)
1796 }
1797}
1798
1799#[derive(Clone)]
1800struct KvStoreExternalTable {
1801 schema: SchemaRef,
1802 batches: Vec<RecordBatch>,
1803}
1804
1805impl std::fmt::Debug for KvStoreExternalTable {
1806 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1807 f.debug_struct("KvStoreExternalTable")
1808 .field("schema", &self.schema)
1809 .finish_non_exhaustive()
1810 }
1811}
1812
1813impl ExternalTable for KvStoreExternalTable {
1814 fn schema(&self) -> SchemaRef {
1815 self.schema.clone()
1816 }
1817
1818 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1819 unsupported_plan(request)
1820 }
1821
1822 fn scan_with_control(
1823 &self,
1824 request: &ExternalPlanRequest<'_>,
1825 control: &mongreldb_core::ExecutionControl,
1826 ) -> DFResult<ExternalScan> {
1827 project_scan_with_control(
1828 self.schema.clone(),
1829 self.batches.clone(),
1830 request.projection.as_deref(),
1831 request.limit,
1832 Some(control),
1833 )
1834 }
1835}
1836
1837fn kv_store_schema() -> CoreSchema {
1838 CoreSchema {
1839 schema_id: 0,
1840 columns: vec![
1841 CoreColumnDef {
1842 id: 1,
1843 name: "key".to_string(),
1844 ty: TypeId::Bytes,
1845 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1846 default_value: None,
1847 },
1848 CoreColumnDef {
1849 id: 2,
1850 name: "value".to_string(),
1851 ty: TypeId::Bytes,
1852 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1853 default_value: None,
1854 },
1855 ],
1856 indexes: Vec::new(),
1857 colocation: Vec::new(),
1858 constraints: Default::default(),
1859 clustered: false,
1860 }
1861}
1862
1863#[derive(Serialize, Deserialize)]
1864struct ExternalRowState {
1865 cells: Vec<(u16, Value)>,
1866}
1867
1868const KV_STATE_MAGIC: &[u8] = b"mongreldb.external.kv.v1\n";
1869
1870#[derive(Serialize, Deserialize)]
1871struct ExternalKvState {
1872 entries: Vec<(Vec<u8>, Vec<u8>)>,
1873}
1874
1875fn state_file(db: &Arc<Database>, entry: &ExternalTableEntry) -> PathBuf {
1876 db.root()
1877 .join(VTAB_DIR)
1878 .join(&entry.name)
1879 .join("state.json")
1880}
1881
1882pub(crate) fn external_table_state_bytes(
1883 db: &Arc<Database>,
1884 entry: &ExternalTableEntry,
1885) -> Result<Vec<u8>> {
1886 db.ensure_consistent_read()?;
1887 let path = state_file(db, entry);
1888 if !path.exists() {
1889 return Ok(Vec::new());
1890 }
1891 let metadata = fs::metadata(&path)
1892 .map_err(|e| MongrelQueryError::Schema(format!("stat external state {:?}: {e}", path)))?;
1893 if metadata.len() > EXTERNAL_STATE_BYTES_LIMIT as u64 {
1894 return Err(external_resource_limit(
1895 "external table state file bytes",
1896 usize::try_from(metadata.len()).unwrap_or(usize::MAX),
1897 EXTERNAL_STATE_BYTES_LIMIT,
1898 ));
1899 }
1900 let state = fs::read(&path)
1901 .map_err(|e| MongrelQueryError::Schema(format!("read external state {:?}: {e}", path)))?;
1902 enforce_external_state_limit(&state)?;
1903 Ok(state)
1904}
1905
1906fn read_state_rows(
1907 db: &Arc<Database>,
1908 entry: &ExternalTableEntry,
1909) -> Result<Vec<HashMap<u16, Value>>> {
1910 let bytes = external_table_state_bytes(db, entry)?;
1911 if bytes.is_empty() {
1912 return Ok(Vec::new());
1913 }
1914 decode_state_rows(&bytes)
1915}
1916
1917struct LimitedStateWriter {
1918 bytes: Vec<u8>,
1919 requested: usize,
1920}
1921
1922impl LimitedStateWriter {
1923 fn new(prefix: &[u8]) -> Self {
1924 Self {
1925 bytes: prefix.to_vec(),
1926 requested: prefix.len(),
1927 }
1928 }
1929}
1930
1931impl std::io::Write for LimitedStateWriter {
1932 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1933 self.requested = self.requested.saturating_add(buf.len());
1934 if self.requested > EXTERNAL_STATE_BYTES_LIMIT {
1935 return Err(std::io::Error::other("external state byte limit exceeded"));
1936 }
1937 self.bytes.extend_from_slice(buf);
1938 Ok(buf.len())
1939 }
1940
1941 fn flush(&mut self) -> std::io::Result<()> {
1942 Ok(())
1943 }
1944}
1945
1946fn encode_external_json<T: Serialize + ?Sized>(value: &T, prefix: &[u8]) -> Result<Vec<u8>> {
1947 let mut writer = LimitedStateWriter::new(prefix);
1948 if let Err(error) = serde_json::to_writer(&mut writer, value) {
1949 if writer.requested > EXTERNAL_STATE_BYTES_LIMIT {
1950 return Err(external_resource_limit(
1951 "external table encoded state bytes",
1952 writer.requested,
1953 EXTERNAL_STATE_BYTES_LIMIT,
1954 ));
1955 }
1956 return Err(MongrelQueryError::Schema(format!(
1957 "encode external state: {error}"
1958 )));
1959 }
1960 Ok(writer.bytes)
1961}
1962
1963fn encode_state_rows(rows: &[HashMap<u16, Value>]) -> Result<Vec<u8>> {
1964 enforce_external_rows_limit(rows, None)?;
1965 let state = rows
1966 .iter()
1967 .map(|row| {
1968 let mut cells = row
1969 .iter()
1970 .map(|(id, value)| (*id, value.clone()))
1971 .collect::<Vec<_>>();
1972 cells.sort_by_key(|(id, _)| *id);
1973 ExternalRowState { cells }
1974 })
1975 .collect::<Vec<_>>();
1976 encode_external_json(&state, &[])
1977}
1978
1979fn decode_state_rows(state: &[u8]) -> Result<Vec<HashMap<u16, Value>>> {
1980 enforce_external_state_limit(state)?;
1981 let rows: Vec<ExternalRowState> = serde_json::from_slice(state)
1982 .map_err(|e| MongrelQueryError::Schema(format!("decode external state: {e}")))?;
1983 let rows = rows
1984 .into_iter()
1985 .map(|row| row.cells.into_iter().collect())
1986 .collect::<Vec<_>>();
1987 enforce_external_rows_limit(&rows, None)?;
1988 Ok(rows)
1989}
1990
1991fn decode_kv_state(state: &[u8]) -> Result<BTreeMap<Vec<u8>, Vec<u8>>> {
1992 enforce_external_state_limit(state)?;
1993 if state.is_empty() {
1994 return Ok(BTreeMap::new());
1995 }
1996 let json = state.strip_prefix(KV_STATE_MAGIC).ok_or_else(|| {
1997 MongrelQueryError::Schema(
1998 "external transaction state is not in key/value module format".into(),
1999 )
2000 })?;
2001 let decoded: ExternalKvState = serde_json::from_slice(json)
2002 .map_err(|e| MongrelQueryError::Schema(format!("decode external kv state: {e}")))?;
2003 if decoded.entries.len() > EXTERNAL_ROWS_LIMIT {
2004 return Err(external_resource_limit(
2005 "external transaction key/value count",
2006 decoded.entries.len(),
2007 EXTERNAL_ROWS_LIMIT,
2008 ));
2009 }
2010 let bytes = decoded.entries.iter().fold(0_usize, |total, (key, value)| {
2011 total.saturating_add(key.len()).saturating_add(value.len())
2012 });
2013 if bytes > EXTERNAL_STATE_BYTES_LIMIT {
2014 return Err(external_resource_limit(
2015 "external transaction key/value bytes",
2016 bytes,
2017 EXTERNAL_STATE_BYTES_LIMIT,
2018 ));
2019 }
2020 Ok(decoded.entries.into_iter().collect())
2021}
2022
2023fn encode_kv_state(state: &BTreeMap<Vec<u8>, Vec<u8>>) -> Result<Vec<u8>> {
2024 if state.len() > EXTERNAL_ROWS_LIMIT {
2025 return Err(external_resource_limit(
2026 "external transaction key/value count",
2027 state.len(),
2028 EXTERNAL_ROWS_LIMIT,
2029 ));
2030 }
2031 let bytes = state.iter().fold(0_usize, |total, (key, value)| {
2032 total.saturating_add(key.len()).saturating_add(value.len())
2033 });
2034 if bytes > EXTERNAL_STATE_BYTES_LIMIT {
2035 return Err(external_resource_limit(
2036 "external transaction key/value bytes",
2037 bytes,
2038 EXTERNAL_STATE_BYTES_LIMIT,
2039 ));
2040 }
2041 let encoded = ExternalKvState {
2042 entries: state
2043 .iter()
2044 .map(|(key, value)| (key.clone(), value.clone()))
2045 .collect(),
2046 };
2047 encode_external_json(&encoded, KV_STATE_MAGIC)
2048}
2049
2050fn validate_external_rows(schema: &CoreSchema, rows: &[HashMap<u16, Value>]) -> Result<()> {
2051 enforce_external_rows_limit(rows, None)?;
2052 let mut known = HashSet::new();
2053 let mut pk_cols = Vec::new();
2054 for column in &schema.columns {
2055 known.insert(column.id);
2056 if column.flags.contains(ColumnFlags::PRIMARY_KEY) {
2057 pk_cols.push(column.id);
2058 }
2059 }
2060 let mut keys = HashSet::new();
2061 for row in rows {
2062 for column_id in row.keys() {
2063 if !known.contains(column_id) {
2064 return Err(MongrelQueryError::Schema(format!(
2065 "external row contains unknown column id {column_id}"
2066 )));
2067 }
2068 }
2069 if !pk_cols.is_empty() {
2070 let mut key = Vec::new();
2071 for column_id in &pk_cols {
2072 let value = row.get(column_id).ok_or_else(|| {
2073 MongrelQueryError::Schema(format!(
2074 "external row is missing primary key column {column_id}"
2075 ))
2076 })?;
2077 key.extend_from_slice(&column_id.to_be_bytes());
2078 key.extend_from_slice(&value.encode_key());
2079 key.push(0);
2080 }
2081 if !keys.insert(key) {
2082 return Err(MongrelQueryError::Schema(
2083 "external table primary key conflict".into(),
2084 ));
2085 }
2086 }
2087 }
2088 Ok(())
2089}
2090
2091fn core_rows_to_batches(
2092 schema: &CoreSchema,
2093 rows: Vec<HashMap<u16, Value>>,
2094) -> Result<Vec<RecordBatch>> {
2095 let arrow_schema = arrow_conv::arrow_schema(schema)?;
2096 let arrays = schema
2097 .columns
2098 .iter()
2099 .map(|column| {
2100 let values = rows
2101 .iter()
2102 .map(|row| row.get(&column.id).cloned().unwrap_or(Value::Null))
2103 .collect::<Vec<_>>();
2104 arrow_conv::build_array(column.ty.clone(), &values)
2105 })
2106 .collect::<Result<Vec<_>>>()?;
2107 Ok(vec![RecordBatch::try_new(arrow_schema, arrays)
2108 .map_err(|e| MongrelQueryError::Arrow(e.to_string()))?])
2109}
2110
2111struct FtsDocsModule;
2112
2113impl ExternalTableModule for FtsDocsModule {
2114 fn name(&self) -> &str {
2115 "fts_docs"
2116 }
2117
2118 fn descriptor(&self) -> ExternalModuleDescriptor {
2119 ExternalModuleDescriptor {
2120 schema: fts_docs_schema(),
2121 hidden_columns: vec!["query".to_string()],
2122 capabilities: ModuleCapabilities {
2123 writable: true,
2124 deterministic: true,
2125 trigger_safe: true,
2126 ..ModuleCapabilities::default()
2127 },
2128 }
2129 }
2130
2131 fn indexes(&self, entry: &ExternalTableEntry) -> Result<Vec<ExternalModuleIndex>> {
2132 Ok(vec![ExternalModuleIndex::new(
2133 format!("{}_fts_inverted", entry.name),
2134 vec![2],
2135 )])
2136 }
2137
2138 fn connect(
2139 &self,
2140 ctx: &ModuleConnectCtx<'_>,
2141 entry: &ExternalTableEntry,
2142 ) -> Result<Arc<dyn ExternalTable>> {
2143 let options = fts_options(entry)?;
2144 let rows = read_state_rows(ctx.db, entry)?;
2145 let index = FtsInvertedIndex::build(&rows, &options);
2146 Ok(Arc::new(FtsDocsExternalTable {
2147 schema: arrow_conv::arrow_schema(&entry.declared_schema)?,
2148 core_schema: entry.declared_schema.clone(),
2149 options,
2150 index,
2151 rows,
2152 }))
2153 }
2154
2155 fn read_rows(
2156 &self,
2157 ctx: &ModuleConnectCtx<'_>,
2158 entry: &ExternalTableEntry,
2159 ) -> Result<Vec<HashMap<u16, Value>>> {
2160 let _ = fts_options(entry)?;
2161 read_state_rows(ctx.db, entry)
2162 }
2163
2164 fn prepare_rows(
2165 &self,
2166 _ctx: &ModuleConnectCtx<'_>,
2167 entry: &ExternalTableEntry,
2168 rows: Vec<HashMap<u16, Value>>,
2169 ) -> Result<Vec<u8>> {
2170 let _ = fts_options(entry)?;
2171 let rows = rows
2172 .into_iter()
2173 .map(|mut row| {
2174 row.remove(&3);
2175 row.remove(&4);
2176 row.remove(&5);
2177 row.remove(&6);
2178 row
2179 })
2180 .collect::<Vec<_>>();
2181 validate_external_rows(&entry.declared_schema, &rows)?;
2182 encode_state_rows(&rows)
2183 }
2184}
2185
2186#[derive(Clone)]
2187struct FtsDocsExternalTable {
2188 schema: SchemaRef,
2189 core_schema: CoreSchema,
2190 options: FtsOptions,
2191 index: FtsInvertedIndex,
2192 rows: Vec<HashMap<u16, Value>>,
2193}
2194
2195impl std::fmt::Debug for FtsDocsExternalTable {
2196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2197 f.debug_struct("FtsDocsExternalTable")
2198 .field("schema", &self.schema)
2199 .finish_non_exhaustive()
2200 }
2201}
2202
2203impl ExternalTable for FtsDocsExternalTable {
2204 fn schema(&self) -> SchemaRef {
2205 self.schema.clone()
2206 }
2207
2208 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
2209 Ok(ExternalPlan::new(
2210 request
2211 .raw_filters
2212 .iter()
2213 .map(|filter| {
2214 if fts_query_from_expr(filter, &self.options).is_some() {
2215 TableProviderFilterPushDown::Exact
2216 } else {
2217 TableProviderFilterPushDown::Unsupported
2218 }
2219 })
2220 .collect(),
2221 None,
2222 1.0,
2223 false,
2224 ))
2225 }
2226
2227 fn scan_with_control(
2228 &self,
2229 request: &ExternalPlanRequest<'_>,
2230 control: &mongreldb_core::ExecutionControl,
2231 ) -> DFResult<ExternalScan> {
2232 external_checkpoint(Some(control), 0)?;
2233 let query = request
2234 .raw_filters
2235 .iter()
2236 .filter_map(|filter| fts_query_from_expr(filter, &self.options))
2237 .reduce(FtsQuery::and);
2238 let mut rows = Vec::new();
2239 if let Some(query) = query.as_ref() {
2240 for (index, row_index) in self.index.candidates(query).into_iter().enumerate() {
2241 external_checkpoint(Some(control), index)?;
2242 let Some(row) = self.rows.get(row_index) else {
2243 continue;
2244 };
2245 let Some(score) = fts_match_score(row, query, &self.options) else {
2246 continue;
2247 };
2248 rows.push(fts_enrich_row(
2249 row.clone(),
2250 Some(query),
2251 &self.options,
2252 Some(score),
2253 ));
2254 }
2255 } else {
2256 rows.reserve(self.rows.len());
2257 for (index, row) in self.rows.iter().cloned().enumerate() {
2258 external_checkpoint(Some(control), index)?;
2259 rows.push(fts_enrich_row(row, None, &self.options, None));
2260 }
2261 }
2262 external_checkpoint(Some(control), 0)?;
2263 project_scan_with_control(
2264 self.schema.clone(),
2265 core_rows_to_batches(&self.core_schema, rows)
2266 .map_err(|e| DataFusionError::Execution(e.to_string()))?,
2267 request.projection.as_deref(),
2268 request.limit,
2269 Some(control),
2270 )
2271 }
2272}
2273
2274fn fts_docs_schema() -> CoreSchema {
2275 CoreSchema {
2276 schema_id: 0,
2277 columns: vec![
2278 CoreColumnDef {
2279 id: 1,
2280 name: "doc_id".to_string(),
2281 ty: TypeId::Int64,
2282 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
2283 default_value: None,
2284 },
2285 CoreColumnDef {
2286 id: 2,
2287 name: "text".to_string(),
2288 ty: TypeId::Bytes,
2289 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2290 default_value: None,
2291 },
2292 CoreColumnDef {
2293 id: 3,
2294 name: "query".to_string(),
2295 ty: TypeId::Bytes,
2296 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2297 default_value: None,
2298 },
2299 CoreColumnDef {
2300 id: 4,
2301 name: "rank".to_string(),
2302 ty: TypeId::Float64,
2303 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2304 default_value: None,
2305 },
2306 CoreColumnDef {
2307 id: 5,
2308 name: "snippet".to_string(),
2309 ty: TypeId::Bytes,
2310 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2311 default_value: None,
2312 },
2313 CoreColumnDef {
2314 id: 6,
2315 name: "highlight".to_string(),
2316 ty: TypeId::Bytes,
2317 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2318 default_value: None,
2319 },
2320 ],
2321 indexes: Vec::new(),
2322 colocation: Vec::new(),
2323 constraints: Default::default(),
2324 clustered: false,
2325 }
2326}
2327
2328#[derive(Clone)]
2329struct FtsOptions {
2330 prefix_queries: bool,
2331 case_sensitive: bool,
2332 min_token_len: usize,
2333 stopwords: HashSet<String>,
2334}
2335
2336impl Default for FtsOptions {
2337 fn default() -> Self {
2338 Self {
2339 prefix_queries: false,
2340 case_sensitive: false,
2341 min_token_len: 1,
2342 stopwords: HashSet::new(),
2343 }
2344 }
2345}
2346
2347impl FtsOptions {
2348 fn normalize(&self, value: &str) -> String {
2349 if self.case_sensitive {
2350 value.to_string()
2351 } else {
2352 value.to_ascii_lowercase()
2353 }
2354 }
2355
2356 fn keep_term(&self, term: &str) -> bool {
2357 term.len() >= self.min_token_len && !self.stopwords.contains(term)
2358 }
2359}
2360
2361#[derive(Clone)]
2362struct FtsQuery {
2363 groups: Vec<Vec<FtsClause>>,
2364 prohibited: Vec<FtsClause>,
2365}
2366
2367impl FtsQuery {
2368 fn and(self, other: Self) -> Self {
2369 let left_groups = if self.groups.is_empty() {
2370 vec![Vec::new()]
2371 } else {
2372 self.groups
2373 };
2374 let right_groups = if other.groups.is_empty() {
2375 vec![Vec::new()]
2376 } else {
2377 other.groups
2378 };
2379 let mut groups = Vec::new();
2380 for left in &left_groups {
2381 for right in &right_groups {
2382 let mut combined = left.clone();
2383 combined.extend(right.clone());
2384 groups.push(combined);
2385 }
2386 }
2387 let mut prohibited = self.prohibited;
2388 prohibited.extend(other.prohibited);
2389 Self { groups, prohibited }
2390 }
2391
2392 fn positive_clauses(&self) -> impl Iterator<Item = &FtsClause> {
2393 self.groups.iter().flatten()
2394 }
2395}
2396
2397#[derive(Clone)]
2398struct FtsInvertedIndex {
2399 terms: HashMap<String, Vec<usize>>,
2400 all_rows: Vec<usize>,
2401}
2402
2403impl FtsInvertedIndex {
2404 fn build(rows: &[HashMap<u16, Value>], options: &FtsOptions) -> Self {
2405 let mut term_sets: HashMap<String, BTreeSet<usize>> = HashMap::new();
2406 for (idx, row) in rows.iter().enumerate() {
2407 let Some(Value::Bytes(text)) = row.get(&2) else {
2408 continue;
2409 };
2410 let text = String::from_utf8_lossy(text);
2411 let mut row_terms = HashSet::new();
2412 for span in token_spans_with_options(&text, options) {
2413 if row_terms.insert(span.term.clone()) {
2414 term_sets.entry(span.term).or_default().insert(idx);
2415 }
2416 }
2417 }
2418 let terms = term_sets
2419 .into_iter()
2420 .map(|(term, rows)| (term, rows.into_iter().collect()))
2421 .collect();
2422 Self {
2423 terms,
2424 all_rows: (0..rows.len()).collect(),
2425 }
2426 }
2427
2428 fn candidates(&self, query: &FtsQuery) -> Vec<usize> {
2429 let mut rows = if query.groups.is_empty() {
2430 self.all_rows.iter().copied().collect::<BTreeSet<_>>()
2431 } else {
2432 query
2433 .groups
2434 .iter()
2435 .filter_map(|group| self.group_candidates(group))
2436 .flatten()
2437 .collect::<BTreeSet<_>>()
2438 };
2439 for clause in &query.prohibited {
2440 for idx in self.clause_candidates(clause) {
2441 rows.remove(&idx);
2442 }
2443 }
2444 rows.into_iter().collect()
2445 }
2446
2447 fn group_candidates(&self, group: &[FtsClause]) -> Option<Vec<usize>> {
2448 let mut iter = group.iter().map(|clause| self.clause_candidates(clause));
2449 let first = iter.next()?;
2450 let mut current = first.into_iter().collect::<BTreeSet<_>>();
2451 for rows in iter {
2452 let next = rows.into_iter().collect::<BTreeSet<_>>();
2453 current = current.intersection(&next).copied().collect();
2454 if current.is_empty() {
2455 break;
2456 }
2457 }
2458 Some(current.into_iter().collect())
2459 }
2460
2461 fn clause_candidates(&self, clause: &FtsClause) -> Vec<usize> {
2462 match clause {
2463 FtsClause::Term { term, prefix } => {
2464 if *prefix {
2465 self.terms
2466 .iter()
2467 .filter(|(indexed, _)| indexed.starts_with(term))
2468 .flat_map(|(_, rows)| rows.iter().copied())
2469 .collect::<BTreeSet<_>>()
2470 .into_iter()
2471 .collect()
2472 } else {
2473 self.terms.get(term).cloned().unwrap_or_default()
2474 }
2475 }
2476 FtsClause::Phrase(terms) => {
2477 let clauses = terms
2478 .iter()
2479 .map(|term| FtsClause::Term {
2480 term: term.clone(),
2481 prefix: false,
2482 })
2483 .collect::<Vec<_>>();
2484 self.group_candidates(&clauses).unwrap_or_default()
2485 }
2486 }
2487 }
2488}
2489
2490#[derive(Clone, PartialEq, Eq)]
2491enum FtsClause {
2492 Term { term: String, prefix: bool },
2493 Phrase(Vec<String>),
2494}
2495
2496enum FtsQueryToken {
2497 Clause(FtsClause),
2498 Or,
2499 Not,
2500}
2501
2502fn fts_options(entry: &ExternalTableEntry) -> Result<FtsOptions> {
2503 let mut options = FtsOptions::default();
2504 for arg in &entry.args {
2505 let raw = module_arg_string(arg).trim();
2506 if raw.is_empty() {
2507 continue;
2508 }
2509 let (key, value) = raw
2510 .split_once('=')
2511 .map_or((raw, "true"), |(key, value)| (key.trim(), value.trim()));
2512 match key.to_ascii_lowercase().as_str() {
2513 "tokenizer" => match value.to_ascii_lowercase().as_str() {
2514 "simple" | "ascii" | "unicode61" => {}
2515 other => {
2516 return Err(MongrelQueryError::Schema(format!(
2517 "fts_docs tokenizer {other:?} is not supported"
2518 )))
2519 }
2520 },
2521 "prefix" | "prefix_queries" => options.prefix_queries = parse_bool_option(value)?,
2522 "case_sensitive" => options.case_sensitive = parse_bool_option(value)?,
2523 "min_token_len" => {
2524 options.min_token_len = value.parse::<usize>().map_err(|e| {
2525 MongrelQueryError::Schema(format!(
2526 "fts_docs min_token_len {value:?} must be an integer: {e}"
2527 ))
2528 })?;
2529 if options.min_token_len == 0 {
2530 return Err(MongrelQueryError::Schema(
2531 "fts_docs min_token_len must be at least 1".into(),
2532 ));
2533 }
2534 }
2535 "stopwords" => {
2536 options.stopwords = value
2537 .split('|')
2538 .map(str::trim)
2539 .filter(|word| !word.is_empty())
2540 .map(|word| options.normalize(word))
2541 .collect();
2542 }
2543 other => {
2544 return Err(MongrelQueryError::Schema(format!(
2545 "fts_docs option {other:?} is not supported"
2546 )))
2547 }
2548 }
2549 }
2550 Ok(options)
2551}
2552
2553fn parse_bool_option(value: &str) -> Result<bool> {
2554 match value.to_ascii_lowercase().as_str() {
2555 "1" | "true" | "yes" | "on" => Ok(true),
2556 "0" | "false" | "no" | "off" => Ok(false),
2557 _ => Err(MongrelQueryError::Schema(format!(
2558 "expected boolean option value, got {value:?}"
2559 ))),
2560 }
2561}
2562
2563fn fts_query_from_expr(expr: &Expr, options: &FtsOptions) -> Option<FtsQuery> {
2564 match expr {
2565 Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::And => {
2566 Some(fts_query_from_expr(left, options)?.and(fts_query_from_expr(right, options)?))
2567 }
2568 Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::Eq => {
2569 let literal = match (left.as_ref(), right.as_ref()) {
2570 (Expr::Column(column), Expr::Literal(literal, _)) if column.name == "query" => {
2571 literal_string(literal)?
2572 }
2573 (Expr::Literal(literal, _), Expr::Column(column)) if column.name == "query" => {
2574 literal_string(literal)?
2575 }
2576 _ => return None,
2577 };
2578 parse_fts_query(&literal, options)
2579 }
2580 _ => None,
2581 }
2582}
2583
2584fn parse_fts_query(input: &str, options: &FtsOptions) -> Option<FtsQuery> {
2585 let tokens = fts_query_tokens(input, options);
2586 if tokens.is_empty() {
2587 return None;
2588 }
2589 let mut groups: Vec<Vec<FtsClause>> = vec![Vec::new()];
2590 let mut prohibited = Vec::new();
2591 let mut negate = false;
2592 for token in tokens {
2593 match token {
2594 FtsQueryToken::Or => {
2595 if groups.last().is_some_and(|group| !group.is_empty()) {
2596 groups.push(Vec::new());
2597 }
2598 negate = false;
2599 }
2600 FtsQueryToken::Not => negate = true,
2601 FtsQueryToken::Clause(clause) => {
2602 if negate {
2603 prohibited.push(clause);
2604 negate = false;
2605 } else if let Some(group) = groups.last_mut() {
2606 group.push(clause);
2607 }
2608 }
2609 }
2610 }
2611 groups.retain(|group| !group.is_empty());
2612 if groups.is_empty() && prohibited.is_empty() {
2613 None
2614 } else {
2615 Some(FtsQuery { groups, prohibited })
2616 }
2617}
2618
2619fn fts_query_tokens(input: &str, options: &FtsOptions) -> Vec<FtsQueryToken> {
2620 let mut tokens = Vec::new();
2621 let mut chars = input.char_indices().peekable();
2622 while let Some((idx, ch)) = chars.next() {
2623 if ch.is_whitespace() {
2624 continue;
2625 }
2626 if ch == '"' {
2627 let start = idx + ch.len_utf8();
2628 let mut end = input.len();
2629 for (next_idx, next_ch) in chars.by_ref() {
2630 if next_ch == '"' {
2631 end = next_idx;
2632 break;
2633 }
2634 }
2635 let terms = token_spans_with_options(&input[start..end], options)
2636 .into_iter()
2637 .map(|span| span.term)
2638 .collect::<Vec<_>>();
2639 if !terms.is_empty() {
2640 tokens.push(FtsQueryToken::Clause(FtsClause::Phrase(terms)));
2641 }
2642 continue;
2643 }
2644 let start = idx;
2645 let mut end = idx + ch.len_utf8();
2646 while let Some((next_idx, next_ch)) = chars.peek().copied() {
2647 if next_ch.is_whitespace() {
2648 break;
2649 }
2650 chars.next();
2651 end = next_idx + next_ch.len_utf8();
2652 }
2653 let raw = &input[start..end];
2654 if raw.eq_ignore_ascii_case("OR") {
2655 tokens.push(FtsQueryToken::Or);
2656 } else if raw.eq_ignore_ascii_case("NOT") {
2657 tokens.push(FtsQueryToken::Not);
2658 } else if let Some(rest) = raw.strip_prefix('-') {
2659 tokens.push(FtsQueryToken::Not);
2660 tokens.extend(
2661 word_clauses(rest, options)
2662 .into_iter()
2663 .map(FtsQueryToken::Clause),
2664 );
2665 } else {
2666 tokens.extend(
2667 word_clauses(raw, options)
2668 .into_iter()
2669 .map(FtsQueryToken::Clause),
2670 );
2671 }
2672 }
2673 tokens
2674}
2675
2676fn word_clauses(raw: &str, options: &FtsOptions) -> Vec<FtsClause> {
2677 let raw = raw.trim_matches(|ch: char| !ch.is_ascii_alphanumeric() && ch != '*');
2678 if raw.is_empty() {
2679 return Vec::new();
2680 }
2681 let prefix = options.prefix_queries && raw.ends_with('*');
2682 let raw = raw.trim_end_matches('*');
2683 token_spans_with_options(raw, options)
2684 .into_iter()
2685 .map(|span| FtsClause::Term {
2686 term: span.term,
2687 prefix,
2688 })
2689 .collect()
2690}
2691
2692fn fts_match_score(
2693 row: &HashMap<u16, Value>,
2694 query: &FtsQuery,
2695 options: &FtsOptions,
2696) -> Option<f64> {
2697 let Some(Value::Bytes(text)) = row.get(&2) else {
2698 return None;
2699 };
2700 let text = String::from_utf8_lossy(text);
2701 let tokens = token_spans_with_options(&text, options);
2702 if query
2703 .prohibited
2704 .iter()
2705 .any(|clause| clause_matches(&tokens, clause))
2706 {
2707 return None;
2708 }
2709 let positive_score = if query.groups.is_empty() {
2710 Some(0.0)
2711 } else {
2712 query
2713 .groups
2714 .iter()
2715 .filter(|group| group.iter().all(|clause| clause_matches(&tokens, clause)))
2716 .map(|group| {
2717 group
2718 .iter()
2719 .map(|clause| clause_score(&tokens, clause))
2720 .sum()
2721 })
2722 .max_by(|left: &f64, right: &f64| left.total_cmp(right))
2723 }?;
2724 Some(positive_score)
2725}
2726
2727fn clause_matches(tokens: &[TokenSpan], clause: &FtsClause) -> bool {
2728 clause_score(tokens, clause) > 0.0
2729}
2730
2731fn clause_score(tokens: &[TokenSpan], clause: &FtsClause) -> f64 {
2732 match clause {
2733 FtsClause::Term { term, prefix } => tokens
2734 .iter()
2735 .filter(|token| token_matches_term(&token.term, term, *prefix))
2736 .count() as f64,
2737 FtsClause::Phrase(terms) => phrase_occurrences(tokens, terms) as f64 * terms.len() as f64,
2738 }
2739}
2740
2741fn token_matches_term(token: &str, term: &str, prefix: bool) -> bool {
2742 if prefix {
2743 token.starts_with(term)
2744 } else {
2745 token == term
2746 }
2747}
2748
2749fn phrase_occurrences(tokens: &[TokenSpan], terms: &[String]) -> usize {
2750 if terms.is_empty() || tokens.len() < terms.len() {
2751 return 0;
2752 }
2753 tokens
2754 .windows(terms.len())
2755 .filter(|window| {
2756 window
2757 .iter()
2758 .zip(terms)
2759 .all(|(token, term)| token.term == *term)
2760 })
2761 .count()
2762}
2763
2764fn token_matches_positive_clause(token: &str, query: &FtsQuery) -> bool {
2765 query.positive_clauses().any(|clause| match clause {
2766 FtsClause::Term { term, prefix } => token_matches_term(token, term, *prefix),
2767 FtsClause::Phrase(terms) => terms.iter().any(|term| term == token),
2768 })
2769}
2770
2771fn fts_enrich_row(
2772 mut row: HashMap<u16, Value>,
2773 query: Option<&FtsQuery>,
2774 options: &FtsOptions,
2775 score: Option<f64>,
2776) -> HashMap<u16, Value> {
2777 row.remove(&3);
2778 row.remove(&4);
2779 row.remove(&5);
2780 row.remove(&6);
2781 let Some(score) = score else {
2782 return row;
2783 };
2784 let text = match row.get(&2) {
2785 Some(Value::Bytes(text)) => String::from_utf8_lossy(text).into_owned(),
2786 _ => String::new(),
2787 };
2788 row.insert(4, Value::Float64(score));
2789 if let Some(query) = query {
2790 row.insert(
2791 5,
2792 Value::Bytes(fts_snippet(&text, query, options).into_bytes()),
2793 );
2794 row.insert(
2795 6,
2796 Value::Bytes(fts_highlight(&text, query, options).into_bytes()),
2797 );
2798 }
2799 row
2800}
2801
2802#[derive(Debug, Clone)]
2803struct TokenSpan {
2804 term: String,
2805 start: usize,
2806 end: usize,
2807}
2808
2809fn token_spans_with_options(input: &str, options: &FtsOptions) -> Vec<TokenSpan> {
2810 let mut spans = Vec::new();
2811 let mut start = None;
2812 for (idx, ch) in input.char_indices() {
2813 if ch.is_ascii_alphanumeric() {
2814 start.get_or_insert(idx);
2815 } else if let Some(lo) = start.take() {
2816 push_token_span(input, &mut spans, lo, idx, options);
2817 }
2818 }
2819 if let Some(lo) = start {
2820 push_token_span(input, &mut spans, lo, input.len(), options);
2821 }
2822 spans
2823}
2824
2825fn push_token_span(
2826 input: &str,
2827 spans: &mut Vec<TokenSpan>,
2828 start: usize,
2829 end: usize,
2830 options: &FtsOptions,
2831) {
2832 if start < end {
2833 let term = options.normalize(&input[start..end]);
2834 if options.keep_term(&term) {
2835 spans.push(TokenSpan { term, start, end });
2836 }
2837 }
2838}
2839
2840fn fts_snippet(text: &str, query: &FtsQuery, options: &FtsOptions) -> String {
2841 let spans = token_spans_with_options(text, options);
2842 if spans.is_empty() {
2843 return String::new();
2844 }
2845 let first_match = spans
2846 .iter()
2847 .position(|span| token_matches_positive_clause(&span.term, query))
2848 .unwrap_or(0);
2849 let lo = first_match.saturating_sub(3);
2850 let hi = (first_match + 5).min(spans.len());
2851 let mut out = Vec::new();
2852 if lo > 0 {
2853 out.push("...".to_string());
2854 }
2855 for span in &spans[lo..hi] {
2856 let token = &text[span.start..span.end];
2857 if token_matches_positive_clause(&span.term, query) {
2858 out.push(format!("[{token}]"));
2859 } else {
2860 out.push(token.to_string());
2861 }
2862 }
2863 if hi < spans.len() {
2864 out.push("...".to_string());
2865 }
2866 out.join(" ")
2867}
2868
2869fn fts_highlight(text: &str, query: &FtsQuery, options: &FtsOptions) -> String {
2870 let spans = token_spans_with_options(text, options);
2871 if spans.is_empty() {
2872 return text.to_string();
2873 }
2874 let mut out = String::new();
2875 let mut cursor = 0;
2876 for span in spans {
2877 out.push_str(&text[cursor..span.start]);
2878 let token = &text[span.start..span.end];
2879 if token_matches_positive_clause(&span.term, query) {
2880 out.push_str("<b>");
2881 out.push_str(token);
2882 out.push_str("</b>");
2883 } else {
2884 out.push_str(token);
2885 }
2886 cursor = span.end;
2887 }
2888 out.push_str(&text[cursor..]);
2889 out
2890}
2891
2892struct RTreeRectsModule;
2893
2894impl ExternalTableModule for RTreeRectsModule {
2895 fn name(&self) -> &str {
2896 "rtree_rects"
2897 }
2898
2899 fn descriptor(&self) -> ExternalModuleDescriptor {
2900 ExternalModuleDescriptor {
2901 schema: rtree_rects_schema(),
2902 hidden_columns: vec![
2903 "query_min_x".to_string(),
2904 "query_max_x".to_string(),
2905 "query_min_y".to_string(),
2906 "query_max_y".to_string(),
2907 ],
2908 capabilities: ModuleCapabilities {
2909 writable: true,
2910 deterministic: true,
2911 trigger_safe: true,
2912 ..ModuleCapabilities::default()
2913 },
2914 }
2915 }
2916
2917 fn indexes(&self, entry: &ExternalTableEntry) -> Result<Vec<ExternalModuleIndex>> {
2918 Ok(vec![ExternalModuleIndex::new(
2919 format!("{}_rtree_spatial", entry.name),
2920 vec![2, 3, 4, 5],
2921 )])
2922 }
2923
2924 fn connect(
2925 &self,
2926 ctx: &ModuleConnectCtx<'_>,
2927 entry: &ExternalTableEntry,
2928 ) -> Result<Arc<dyn ExternalTable>> {
2929 ensure_no_args(entry, self.name())?;
2930 let rows = read_state_rows(ctx.db, entry)?;
2931 let index = RTreeSpatialIndex::build(&rows);
2932 Ok(Arc::new(RTreeRectsExternalTable {
2933 schema: arrow_conv::arrow_schema(&entry.declared_schema)?,
2934 core_schema: entry.declared_schema.clone(),
2935 index,
2936 rows,
2937 }))
2938 }
2939
2940 fn read_rows(
2941 &self,
2942 ctx: &ModuleConnectCtx<'_>,
2943 entry: &ExternalTableEntry,
2944 ) -> Result<Vec<HashMap<u16, Value>>> {
2945 ensure_no_args(entry, self.name())?;
2946 read_state_rows(ctx.db, entry)
2947 }
2948
2949 fn prepare_rows(
2950 &self,
2951 _ctx: &ModuleConnectCtx<'_>,
2952 entry: &ExternalTableEntry,
2953 rows: Vec<HashMap<u16, Value>>,
2954 ) -> Result<Vec<u8>> {
2955 ensure_no_args(entry, self.name())?;
2956 let rows = rows
2957 .into_iter()
2958 .map(|mut row| {
2959 for column_id in 6..=9 {
2960 row.remove(&column_id);
2961 }
2962 row
2963 })
2964 .collect::<Vec<_>>();
2965 validate_external_rows(&entry.declared_schema, &rows)?;
2966 encode_state_rows(&rows)
2967 }
2968}
2969
2970#[derive(Clone)]
2971struct RTreeRectsExternalTable {
2972 schema: SchemaRef,
2973 core_schema: CoreSchema,
2974 index: RTreeSpatialIndex,
2975 rows: Vec<HashMap<u16, Value>>,
2976}
2977
2978impl std::fmt::Debug for RTreeRectsExternalTable {
2979 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2980 f.debug_struct("RTreeRectsExternalTable")
2981 .field("schema", &self.schema)
2982 .finish_non_exhaustive()
2983 }
2984}
2985
2986impl ExternalTable for RTreeRectsExternalTable {
2987 fn schema(&self) -> SchemaRef {
2988 self.schema.clone()
2989 }
2990
2991 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
2992 Ok(ExternalPlan::new(
2993 request
2994 .raw_filters
2995 .iter()
2996 .map(|filter| {
2997 if rtree_query_bound(filter).is_some()
2998 || rtree_query_rect_function(filter).is_some()
2999 {
3000 TableProviderFilterPushDown::Exact
3001 } else {
3002 TableProviderFilterPushDown::Unsupported
3003 }
3004 })
3005 .collect(),
3006 None,
3007 1.0,
3008 false,
3009 ))
3010 }
3011
3012 fn scan_with_control(
3013 &self,
3014 request: &ExternalPlanRequest<'_>,
3015 control: &mongreldb_core::ExecutionControl,
3016 ) -> DFResult<ExternalScan> {
3017 external_checkpoint(Some(control), 0)?;
3018 let mut hidden_bounds = QueryRect::default();
3019 let mut has_hidden_bounds = false;
3020 for (column, value) in request
3021 .raw_filters
3022 .iter()
3023 .filter_map(|filter| rtree_query_bound(filter))
3024 {
3025 hidden_bounds.set(column, value);
3026 has_hidden_bounds = true;
3027 }
3028 let mut query_rects = request
3029 .raw_filters
3030 .iter()
3031 .filter_map(|filter| rtree_query_rect_function(filter))
3032 .collect::<Vec<_>>();
3033 if has_hidden_bounds || query_rects.is_empty() {
3034 query_rects.push(hidden_bounds);
3035 }
3036 let mut candidate_rows: Option<BTreeSet<usize>> = None;
3037 for bounds in &query_rects {
3038 let mut next = BTreeSet::new();
3039 for (index, row) in self.index.candidates(*bounds).into_iter().enumerate() {
3040 external_checkpoint(Some(control), index)?;
3041 next.insert(row);
3042 }
3043 candidate_rows = Some(match candidate_rows.take() {
3044 Some(current) => {
3045 let mut intersection = BTreeSet::new();
3046 for (index, row) in current.into_iter().enumerate() {
3047 external_checkpoint(Some(control), index)?;
3048 if next.contains(&row) {
3049 intersection.insert(row);
3050 }
3051 }
3052 intersection
3053 }
3054 None => next,
3055 });
3056 }
3057 let candidate_rows =
3058 candidate_rows.unwrap_or_else(|| self.index.all_rows.iter().copied().collect());
3059 let mut rows = Vec::new();
3060 for (index, row) in self.rows.iter().enumerate() {
3061 external_checkpoint(Some(control), index)?;
3062 if candidate_rows.contains(&index)
3063 && query_rects
3064 .iter()
3065 .all(|bounds| rtree_row_matches(row, *bounds))
3066 {
3067 rows.push(row.clone());
3068 }
3069 }
3070 external_checkpoint(Some(control), 0)?;
3071 project_scan_with_control(
3072 self.schema.clone(),
3073 core_rows_to_batches(&self.core_schema, rows)
3074 .map_err(|e| DataFusionError::Execution(e.to_string()))?,
3075 request.projection.as_deref(),
3076 request.limit,
3077 Some(control),
3078 )
3079 }
3080}
3081
3082#[derive(Clone, Copy)]
3083struct QueryRect {
3084 min_x: f64,
3085 max_x: f64,
3086 min_y: f64,
3087 max_y: f64,
3088}
3089
3090impl Default for QueryRect {
3091 fn default() -> Self {
3092 Self {
3093 min_x: f64::NEG_INFINITY,
3094 max_x: f64::INFINITY,
3095 min_y: f64::NEG_INFINITY,
3096 max_y: f64::INFINITY,
3097 }
3098 }
3099}
3100
3101impl QueryRect {
3102 fn set(&mut self, column: &str, value: f64) {
3103 match column {
3104 "query_min_x" => self.min_x = value,
3105 "query_max_x" => self.max_x = value,
3106 "query_min_y" => self.min_y = value,
3107 "query_max_y" => self.max_y = value,
3108 _ => {}
3109 }
3110 }
3111}
3112
3113#[derive(Clone)]
3114struct RTreeSpatialIndex {
3115 all_rows: Vec<usize>,
3116 min_x: Vec<(f64, usize)>,
3117 max_x: Vec<(f64, usize)>,
3118 min_y: Vec<(f64, usize)>,
3119 max_y: Vec<(f64, usize)>,
3120}
3121
3122impl RTreeSpatialIndex {
3123 fn build(rows: &[HashMap<u16, Value>]) -> Self {
3124 let mut all_rows = Vec::new();
3125 let mut min_x = Vec::new();
3126 let mut max_x = Vec::new();
3127 let mut min_y = Vec::new();
3128 let mut max_y = Vec::new();
3129 for (idx, row) in rows.iter().enumerate() {
3130 let Some(x0) = row_f64(row, 2) else {
3131 continue;
3132 };
3133 let Some(x1) = row_f64(row, 3) else {
3134 continue;
3135 };
3136 let Some(y0) = row_f64(row, 4) else {
3137 continue;
3138 };
3139 let Some(y1) = row_f64(row, 5) else {
3140 continue;
3141 };
3142 if !(x0.is_finite() && x1.is_finite() && y0.is_finite() && y1.is_finite()) {
3143 continue;
3144 }
3145 all_rows.push(idx);
3146 min_x.push((x0, idx));
3147 max_x.push((x1, idx));
3148 min_y.push((y0, idx));
3149 max_y.push((y1, idx));
3150 }
3151 for values in [&mut min_x, &mut max_x, &mut min_y, &mut max_y] {
3152 values.sort_by(|left, right| left.0.total_cmp(&right.0));
3153 }
3154 Self {
3155 all_rows,
3156 min_x,
3157 max_x,
3158 min_y,
3159 max_y,
3160 }
3161 }
3162
3163 fn candidates(&self, query: QueryRect) -> Vec<usize> {
3164 let mut rows = self.lte(&self.min_x, query.max_x);
3165 for next in [
3166 self.gte(&self.max_x, query.min_x),
3167 self.lte(&self.min_y, query.max_y),
3168 self.gte(&self.max_y, query.min_y),
3169 ] {
3170 rows = rows.intersection(&next).copied().collect();
3171 if rows.is_empty() {
3172 break;
3173 }
3174 }
3175 rows.into_iter().collect()
3176 }
3177
3178 fn lte(&self, values: &[(f64, usize)], bound: f64) -> BTreeSet<usize> {
3179 if bound == f64::INFINITY {
3180 return self.all_rows.iter().copied().collect();
3181 }
3182 if bound.is_nan() {
3183 return BTreeSet::new();
3184 }
3185 let end = values.partition_point(|(value, _)| *value <= bound);
3186 values[..end].iter().map(|(_, idx)| *idx).collect()
3187 }
3188
3189 fn gte(&self, values: &[(f64, usize)], bound: f64) -> BTreeSet<usize> {
3190 if bound == f64::NEG_INFINITY {
3191 return self.all_rows.iter().copied().collect();
3192 }
3193 if bound.is_nan() {
3194 return BTreeSet::new();
3195 }
3196 let start = values.partition_point(|(value, _)| *value < bound);
3197 values[start..].iter().map(|(_, idx)| *idx).collect()
3198 }
3199}
3200
3201fn rtree_query_rect_function(expr: &Expr) -> Option<QueryRect> {
3202 let Expr::ScalarFunction(sf) = expr else {
3203 return None;
3204 };
3205 if !sf.func.name().eq_ignore_ascii_case("rtree_intersects") || sf.args.len() != 8 {
3206 return None;
3207 }
3208 for (arg, expected) in sf
3209 .args
3210 .iter()
3211 .take(4)
3212 .zip(["min_x", "max_x", "min_y", "max_y"])
3213 {
3214 let Expr::Column(column) = arg else {
3215 return None;
3216 };
3217 if column.name != expected {
3218 return None;
3219 }
3220 }
3221 Some(QueryRect {
3222 min_x: literal_f64_from_expr(&sf.args[4])?,
3223 max_x: literal_f64_from_expr(&sf.args[5])?,
3224 min_y: literal_f64_from_expr(&sf.args[6])?,
3225 max_y: literal_f64_from_expr(&sf.args[7])?,
3226 })
3227}
3228
3229fn literal_f64_from_expr(expr: &Expr) -> Option<f64> {
3230 match expr {
3231 Expr::Literal(literal, _) => literal_f64(literal),
3232 _ => None,
3233 }
3234}
3235
3236fn rtree_rects_schema() -> CoreSchema {
3237 CoreSchema {
3238 schema_id: 0,
3239 columns: vec![
3240 CoreColumnDef {
3241 id: 1,
3242 name: "id".to_string(),
3243 ty: TypeId::Int64,
3244 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
3245 default_value: None,
3246 },
3247 rect_column(2, "min_x"),
3248 rect_column(3, "max_x"),
3249 rect_column(4, "min_y"),
3250 rect_column(5, "max_y"),
3251 rect_column(6, "query_min_x"),
3252 rect_column(7, "query_max_x"),
3253 rect_column(8, "query_min_y"),
3254 rect_column(9, "query_max_y"),
3255 ],
3256 indexes: Vec::new(),
3257 colocation: Vec::new(),
3258 constraints: Default::default(),
3259 clustered: false,
3260 }
3261}
3262
3263fn rect_column(id: u16, name: &str) -> CoreColumnDef {
3264 CoreColumnDef {
3265 id,
3266 name: name.to_string(),
3267 ty: TypeId::Float64,
3268 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
3269 default_value: None,
3270 }
3271}
3272
3273fn rtree_query_bound(expr: &Expr) -> Option<(&str, f64)> {
3274 match expr {
3275 Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::Eq => {
3276 match (left.as_ref(), right.as_ref()) {
3277 (Expr::Column(column), Expr::Literal(literal, _)) => {
3278 hidden_rect_column(&column.name).zip(literal_f64(literal))
3279 }
3280 (Expr::Literal(literal, _), Expr::Column(column)) => {
3281 hidden_rect_column(&column.name).zip(literal_f64(literal))
3282 }
3283 _ => None,
3284 }
3285 }
3286 _ => None,
3287 }
3288}
3289
3290fn hidden_rect_column(name: &str) -> Option<&str> {
3291 match name {
3292 "query_min_x" | "query_max_x" | "query_min_y" | "query_max_y" => Some(name),
3293 _ => None,
3294 }
3295}
3296
3297fn rtree_row_matches(row: &HashMap<u16, Value>, query: QueryRect) -> bool {
3298 let Some(min_x) = row_f64(row, 2) else {
3299 return false;
3300 };
3301 let Some(max_x) = row_f64(row, 3) else {
3302 return false;
3303 };
3304 let Some(min_y) = row_f64(row, 4) else {
3305 return false;
3306 };
3307 let Some(max_y) = row_f64(row, 5) else {
3308 return false;
3309 };
3310 max_x >= query.min_x && min_x <= query.max_x && max_y >= query.min_y && min_y <= query.max_y
3311}
3312
3313fn row_f64(row: &HashMap<u16, Value>, column_id: u16) -> Option<f64> {
3314 match row.get(&column_id) {
3315 Some(Value::Float64(value)) => Some(*value),
3316 Some(Value::Int64(value)) => Some(*value as f64),
3317 _ => None,
3318 }
3319}
3320
3321struct SeriesModule;
3322
3323impl ExternalTableModule for SeriesModule {
3324 fn name(&self) -> &str {
3325 "series"
3326 }
3327
3328 fn descriptor(&self) -> ExternalModuleDescriptor {
3329 ExternalModuleDescriptor {
3330 schema: CoreSchema {
3331 schema_id: 0,
3332 columns: vec![
3333 CoreColumnDef {
3334 id: 1,
3335 name: "value".to_string(),
3336 ty: TypeId::Int64,
3337 flags: ColumnFlags::empty(),
3338 default_value: None,
3339 },
3340 CoreColumnDef {
3341 id: 2,
3342 name: "start".to_string(),
3343 ty: TypeId::Int64,
3344 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
3345 default_value: None,
3346 },
3347 CoreColumnDef {
3348 id: 3,
3349 name: "stop".to_string(),
3350 ty: TypeId::Int64,
3351 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
3352 default_value: None,
3353 },
3354 CoreColumnDef {
3355 id: 4,
3356 name: "step".to_string(),
3357 ty: TypeId::Int64,
3358 flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
3359 default_value: None,
3360 },
3361 ],
3362 indexes: Vec::new(),
3363 colocation: Vec::new(),
3364 constraints: Default::default(),
3365 clustered: false,
3366 },
3367 hidden_columns: vec!["start".to_string(), "stop".to_string(), "step".to_string()],
3368 capabilities: ModuleCapabilities {
3369 read_only: true,
3370 deterministic: true,
3371 trigger_safe: true,
3372 ..ModuleCapabilities::default()
3373 },
3374 }
3375 }
3376
3377 fn connect(
3378 &self,
3379 _ctx: &ModuleConnectCtx<'_>,
3380 entry: &ExternalTableEntry,
3381 ) -> Result<Arc<dyn ExternalTable>> {
3382 let (start, stop, step) = series_args(entry)?;
3383 let table = SeriesExternalTable::new(start, stop, step)?;
3384 Ok(Arc::new(table))
3385 }
3386}
3387
3388struct SeriesExternalTable {
3389 start: i64,
3390 stop: i64,
3391 step: i64,
3392 schema: SchemaRef,
3393}
3394
3395impl SeriesExternalTable {
3396 fn new(start: i64, stop: i64, step: i64) -> Result<Self> {
3397 if step == 0 {
3398 return Err(MongrelQueryError::Schema(
3399 "series step must not be 0".into(),
3400 ));
3401 }
3402 Ok(Self {
3403 start,
3404 stop,
3405 step,
3406 schema: Arc::new(ArrowSchema::new(vec![Field::new(
3407 "value",
3408 DataType::Int64,
3409 false,
3410 )])),
3411 })
3412 }
3413
3414 fn batches(
3415 &self,
3416 request: &ExternalPlanRequest<'_>,
3417 control: &mongreldb_core::ExecutionControl,
3418 ) -> DFResult<Vec<RecordBatch>> {
3419 let mut values = Vec::new();
3420 let mut current = self.start;
3421 let mut scanned = 0;
3422 while if self.step > 0 {
3423 current <= self.stop
3424 } else {
3425 current >= self.stop
3426 } {
3427 external_checkpoint(Some(control), scanned)?;
3428 scanned += 1;
3429 if values.len() >= 1_000_000 {
3430 return Err(DataFusionError::Plan(
3431 "series output is capped at 1,000,000 rows".into(),
3432 ));
3433 }
3434 if request
3435 .filters
3436 .iter()
3437 .all(|filter| series_filter_matches(filter, current).unwrap_or(true))
3438 {
3439 values.push(current);
3440 if request.limit.is_some_and(|limit| values.len() >= limit) {
3441 break;
3442 }
3443 }
3444 current = current.saturating_add(self.step);
3445 if (self.step > 0 && current == i64::MAX) || (self.step < 0 && current == i64::MIN) {
3446 break;
3447 }
3448 }
3449 let batch = RecordBatch::try_new(
3450 self.schema.clone(),
3451 vec![Arc::new(Int64Array::from(values)) as ArrayRef],
3452 )?;
3453 Ok(vec![batch])
3454 }
3455}
3456
3457impl std::fmt::Debug for SeriesExternalTable {
3458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3459 f.debug_struct("SeriesExternalTable")
3460 .field("start", &self.start)
3461 .field("stop", &self.stop)
3462 .field("step", &self.step)
3463 .finish_non_exhaustive()
3464 }
3465}
3466
3467impl ExternalTable for SeriesExternalTable {
3468 fn schema(&self) -> SchemaRef {
3469 self.schema.clone()
3470 }
3471
3472 fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
3473 Ok(ExternalPlan::new(
3474 request
3475 .filters
3476 .iter()
3477 .map(|filter| {
3478 if series_filter_supported(filter) {
3479 TableProviderFilterPushDown::Exact
3480 } else {
3481 TableProviderFilterPushDown::Unsupported
3482 }
3483 })
3484 .collect(),
3485 None,
3486 1.0,
3487 self.step > 0,
3488 ))
3489 }
3490
3491 fn scan_with_control(
3492 &self,
3493 request: &ExternalPlanRequest<'_>,
3494 control: &mongreldb_core::ExecutionControl,
3495 ) -> DFResult<ExternalScan> {
3496 external_checkpoint(Some(control), 0)?;
3497 let batches = self.batches(request, control)?;
3498 external_checkpoint(Some(control), 0)?;
3499 project_scan_with_control(
3500 self.schema.clone(),
3501 batches,
3502 request.projection.as_deref(),
3503 request.limit,
3504 Some(control),
3505 )
3506 }
3507}
3508
3509fn series_filter_supported(filter: &ExternalFilter) -> bool {
3510 match filter {
3511 ExternalFilter::And(filters) => filters.iter().all(series_filter_supported),
3512 ExternalFilter::Compare {
3513 column_index,
3514 value,
3515 ..
3516 } => *column_index == 0 && literal_i64(value).is_some(),
3517 ExternalFilter::Unsupported { .. } => false,
3518 }
3519}
3520
3521fn series_filter_matches(filter: &ExternalFilter, value: i64) -> Option<bool> {
3522 match filter {
3523 ExternalFilter::And(filters) => filters.iter().try_fold(true, |matches, filter| {
3524 Some(matches && series_filter_matches(filter, value)?)
3525 }),
3526 ExternalFilter::Compare {
3527 column_index,
3528 op,
3529 value: literal,
3530 } if *column_index == 0 => {
3531 let literal = literal_i64(literal)?;
3532 Some(match op {
3533 ExternalFilterOp::Eq => value == literal,
3534 ExternalFilterOp::NotEq => value != literal,
3535 ExternalFilterOp::Gt => value > literal,
3536 ExternalFilterOp::GtEq => value >= literal,
3537 ExternalFilterOp::Lt => value < literal,
3538 ExternalFilterOp::LtEq => value <= literal,
3539 })
3540 }
3541 _ => None,
3542 }
3543}
3544
3545fn literal_i64(value: &ScalarValue) -> Option<i64> {
3546 match value {
3547 ScalarValue::Int8(Some(v)) => Some(*v as i64),
3548 ScalarValue::Int16(Some(v)) => Some(*v as i64),
3549 ScalarValue::Int32(Some(v)) => Some(*v as i64),
3550 ScalarValue::Int64(Some(v)) => Some(*v),
3551 ScalarValue::UInt8(Some(v)) => Some(*v as i64),
3552 ScalarValue::UInt16(Some(v)) => Some(*v as i64),
3553 ScalarValue::UInt32(Some(v)) => Some(*v as i64),
3554 ScalarValue::UInt64(Some(v)) => i64::try_from(*v).ok(),
3555 _ => None,
3556 }
3557}
3558
3559fn literal_f64(value: &ScalarValue) -> Option<f64> {
3560 match value {
3561 ScalarValue::Float32(Some(v)) => Some(*v as f64),
3562 ScalarValue::Float64(Some(v)) => Some(*v),
3563 _ => literal_i64(value).map(|value| value as f64),
3564 }
3565}
3566
3567fn literal_string(value: &ScalarValue) -> Option<String> {
3568 match value {
3569 ScalarValue::Utf8(Some(v))
3570 | ScalarValue::Utf8View(Some(v))
3571 | ScalarValue::LargeUtf8(Some(v)) => Some(v.clone()),
3572 ScalarValue::Binary(Some(v))
3573 | ScalarValue::BinaryView(Some(v))
3574 | ScalarValue::LargeBinary(Some(v)) => String::from_utf8(v.clone()).ok(),
3575 _ => None,
3576 }
3577}
3578
3579fn series_args(entry: &ExternalTableEntry) -> Result<(i64, i64, i64)> {
3580 let values = entry
3581 .args
3582 .iter()
3583 .map(|arg| match arg {
3584 ModuleArg::Ident(value) | ModuleArg::String(value) | ModuleArg::Number(value) => {
3585 value.parse::<i64>().map_err(|e| {
3586 MongrelQueryError::Schema(format!(
3587 "series module argument {value:?} must be an integer: {e}"
3588 ))
3589 })
3590 }
3591 })
3592 .collect::<Result<Vec<_>>>()?;
3593 match values.as_slice() {
3594 [] => Ok((0, -1, 1)),
3595 [stop] => Ok((0, *stop, 1)),
3596 [start, stop] => Ok((*start, *stop, 1)),
3597 [start, stop, step] => Ok((*start, *stop, *step)),
3598 _ => Err(MongrelQueryError::Schema(
3599 "series external table accepts at most three arguments".into(),
3600 )),
3601 }
3602}