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