Skip to main content

mongreldb_query/
external_modules.rs

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}