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            clustered: false,
783        },
784        hidden_columns: vec!["json".to_string(), "root".to_string()],
785        capabilities: ModuleCapabilities {
786            read_only: true,
787            deterministic: true,
788            trigger_safe: true,
789            ..ModuleCapabilities::default()
790        },
791    }
792}
793
794fn json_column(id: u16, name: &str, ty: TypeId, nullable: bool) -> CoreColumnDef {
795    CoreColumnDef {
796        id,
797        name: name.to_string(),
798        ty,
799        flags: if nullable {
800            ColumnFlags::empty().with(ColumnFlags::NULLABLE)
801        } else {
802            ColumnFlags::empty()
803        },
804        default_value: None,
805    }
806}
807
808fn json_args(entry: &ExternalTableEntry, module: &str) -> Result<(String, Option<String>)> {
809    match entry.args.as_slice() {
810        [json] => Ok((module_arg_string(json).to_string(), None)),
811        [json, root] => Ok((
812            module_arg_string(json).to_string(),
813            Some(module_arg_string(root).to_string()),
814        )),
815        _ => Err(MongrelQueryError::Schema(format!(
816            "{module} external table requires one JSON argument and an optional root path"
817        ))),
818    }
819}
820
821fn module_arg_string(arg: &ModuleArg) -> &str {
822    match arg {
823        ModuleArg::Ident(value) | ModuleArg::String(value) | ModuleArg::Number(value) => value,
824    }
825}
826
827struct SchemaTablesModule;
828
829impl ExternalTableModule for SchemaTablesModule {
830    fn name(&self) -> &str {
831        "schema_tables"
832    }
833
834    fn descriptor(&self) -> ExternalModuleDescriptor {
835        ExternalModuleDescriptor {
836            schema: CoreSchema {
837                schema_id: 0,
838                columns: vec![
839                    catalog_column(1, "schema_name", TypeId::Bytes, false),
840                    catalog_column(2, "name", TypeId::Bytes, false),
841                    catalog_column(3, "type", TypeId::Bytes, false),
842                    catalog_column(4, "ncol", TypeId::Int64, false),
843                    catalog_column(5, "module", TypeId::Bytes, true),
844                    catalog_column(6, "created_epoch", TypeId::Int64, false),
845                ],
846                indexes: Vec::new(),
847                colocation: Vec::new(),
848                constraints: Default::default(),
849                clustered: false,
850            },
851            hidden_columns: Vec::new(),
852            capabilities: catalog_capabilities(),
853        }
854    }
855
856    fn connect(
857        &self,
858        ctx: &ModuleConnectCtx<'_>,
859        entry: &ExternalTableEntry,
860    ) -> Result<Arc<dyn ExternalTable>> {
861        ensure_no_args(entry, self.name())?;
862        Ok(Arc::new(SchemaTablesExternalTable {
863            db: Arc::clone(ctx.db),
864            schema: schema_tables_arrow_schema(),
865        }))
866    }
867}
868
869#[derive(Clone)]
870struct SchemaTablesExternalTable {
871    db: Arc<Database>,
872    schema: SchemaRef,
873}
874
875impl std::fmt::Debug for SchemaTablesExternalTable {
876    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877        f.debug_struct("SchemaTablesExternalTable")
878            .field("schema", &self.schema)
879            .finish_non_exhaustive()
880    }
881}
882
883impl ExternalTable for SchemaTablesExternalTable {
884    fn schema(&self) -> SchemaRef {
885        self.schema.clone()
886    }
887
888    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
889        unsupported_plan(request)
890    }
891
892    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
893        let batches = schema_tables_batches(&self.db, self.schema.clone())?;
894        project_scan(
895            self.schema.clone(),
896            batches,
897            request.projection.as_deref(),
898            request.limit,
899        )
900    }
901}
902
903struct DbStatModule;
904
905impl ExternalTableModule for DbStatModule {
906    fn name(&self) -> &str {
907        "dbstat"
908    }
909
910    fn descriptor(&self) -> ExternalModuleDescriptor {
911        ExternalModuleDescriptor {
912            schema: CoreSchema {
913                schema_id: 0,
914                columns: vec![
915                    catalog_column(1, "name", TypeId::Bytes, false),
916                    catalog_column(2, "type", TypeId::Bytes, false),
917                    catalog_column(3, "rows", TypeId::Int64, true),
918                    catalog_column(4, "runs", TypeId::Int64, true),
919                    catalog_column(5, "memtable_rows", TypeId::Int64, true),
920                    catalog_column(6, "columns", TypeId::Int64, false),
921                    catalog_column(7, "storage_bytes", TypeId::Int64, false),
922                ],
923                indexes: Vec::new(),
924                colocation: Vec::new(),
925                constraints: Default::default(),
926                clustered: false,
927            },
928            hidden_columns: Vec::new(),
929            capabilities: catalog_capabilities(),
930        }
931    }
932
933    fn connect(
934        &self,
935        ctx: &ModuleConnectCtx<'_>,
936        entry: &ExternalTableEntry,
937    ) -> Result<Arc<dyn ExternalTable>> {
938        ensure_no_args(entry, self.name())?;
939        Ok(Arc::new(DbStatExternalTable {
940            db: Arc::clone(ctx.db),
941            schema: dbstat_arrow_schema(),
942        }))
943    }
944}
945
946#[derive(Clone)]
947struct DbStatExternalTable {
948    db: Arc<Database>,
949    schema: SchemaRef,
950}
951
952impl std::fmt::Debug for DbStatExternalTable {
953    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
954        f.debug_struct("DbStatExternalTable")
955            .field("schema", &self.schema)
956            .finish_non_exhaustive()
957    }
958}
959
960impl ExternalTable for DbStatExternalTable {
961    fn schema(&self) -> SchemaRef {
962        self.schema.clone()
963    }
964
965    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
966        unsupported_plan(request)
967    }
968
969    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
970        let batches = dbstat_batches(&self.db, self.schema.clone())?;
971        project_scan(
972            self.schema.clone(),
973            batches,
974            request.projection.as_deref(),
975            request.limit,
976        )
977    }
978}
979
980fn catalog_column(id: u16, name: &str, ty: TypeId, nullable: bool) -> CoreColumnDef {
981    CoreColumnDef {
982        id,
983        name: name.to_string(),
984        ty,
985        flags: if nullable {
986            ColumnFlags::empty().with(ColumnFlags::NULLABLE)
987        } else {
988            ColumnFlags::empty()
989        },
990        default_value: None,
991    }
992}
993
994fn catalog_capabilities() -> ModuleCapabilities {
995    ModuleCapabilities {
996        read_only: true,
997        trigger_safe: true,
998        ..ModuleCapabilities::default()
999    }
1000}
1001
1002fn ensure_no_args(entry: &ExternalTableEntry, module: &str) -> Result<()> {
1003    if entry.args.is_empty() {
1004        Ok(())
1005    } else {
1006        Err(MongrelQueryError::Schema(format!(
1007            "{module} external table does not accept arguments"
1008        )))
1009    }
1010}
1011
1012fn schema_tables_arrow_schema() -> SchemaRef {
1013    Arc::new(ArrowSchema::new(vec![
1014        Field::new("schema_name", DataType::Utf8, false),
1015        Field::new("name", DataType::Utf8, false),
1016        Field::new("type", DataType::Utf8, false),
1017        Field::new("ncol", DataType::Int64, false),
1018        Field::new("module", DataType::Utf8, true),
1019        Field::new("created_epoch", DataType::Int64, false),
1020    ]))
1021}
1022
1023fn schema_tables_batches(db: &Arc<Database>, schema: SchemaRef) -> DFResult<Vec<RecordBatch>> {
1024    struct Row {
1025        name: String,
1026        ty: String,
1027        ncol: i64,
1028        module: Option<String>,
1029        created_epoch: i64,
1030    }
1031
1032    let catalog = db.catalog_snapshot();
1033    let mut rows = Vec::new();
1034    for table in catalog
1035        .tables
1036        .into_iter()
1037        .filter(|table| matches!(table.state, TableState::Live))
1038    {
1039        rows.push(Row {
1040            name: table.name,
1041            ty: "table".to_string(),
1042            ncol: saturating_i64(table.schema.columns.len() as u64),
1043            module: None,
1044            created_epoch: saturating_i64(table.created_epoch),
1045        });
1046    }
1047    for table in catalog.external_tables {
1048        rows.push(Row {
1049            name: table.name,
1050            ty: "external".to_string(),
1051            ncol: saturating_i64(table.declared_schema.columns.len() as u64),
1052            module: Some(table.module),
1053            created_epoch: saturating_i64(table.created_epoch),
1054        });
1055    }
1056    rows.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.ty.cmp(&b.ty)));
1057
1058    let schema_names = vec!["main".to_string(); rows.len()];
1059    let names = rows.iter().map(|row| row.name.clone()).collect::<Vec<_>>();
1060    let types = rows.iter().map(|row| row.ty.clone()).collect::<Vec<_>>();
1061    let ncols = rows.iter().map(|row| row.ncol).collect::<Vec<_>>();
1062    let modules = rows
1063        .iter()
1064        .map(|row| row.module.clone())
1065        .collect::<Vec<_>>();
1066    let created_epochs = rows.iter().map(|row| row.created_epoch).collect::<Vec<_>>();
1067
1068    Ok(vec![RecordBatch::try_new(
1069        schema,
1070        vec![
1071            Arc::new(StringArray::from(schema_names)) as ArrayRef,
1072            Arc::new(StringArray::from(names)),
1073            Arc::new(StringArray::from(types)),
1074            Arc::new(Int64Array::from(ncols)),
1075            Arc::new(StringArray::from(modules)),
1076            Arc::new(Int64Array::from(created_epochs)),
1077        ],
1078    )?])
1079}
1080
1081fn dbstat_arrow_schema() -> SchemaRef {
1082    Arc::new(ArrowSchema::new(vec![
1083        Field::new("name", DataType::Utf8, false),
1084        Field::new("type", DataType::Utf8, false),
1085        Field::new("rows", DataType::Int64, true),
1086        Field::new("runs", DataType::Int64, true),
1087        Field::new("memtable_rows", DataType::Int64, true),
1088        Field::new("columns", DataType::Int64, false),
1089        Field::new("storage_bytes", DataType::Int64, false),
1090    ]))
1091}
1092
1093fn dbstat_batches(db: &Arc<Database>, schema: SchemaRef) -> DFResult<Vec<RecordBatch>> {
1094    struct Row {
1095        name: String,
1096        ty: String,
1097        rows: Option<i64>,
1098        runs: Option<i64>,
1099        memtable_rows: Option<i64>,
1100        columns: i64,
1101        storage_bytes: i64,
1102    }
1103
1104    let catalog = db.catalog_snapshot();
1105    let mut rows = Vec::new();
1106    for table in catalog
1107        .tables
1108        .into_iter()
1109        .filter(|table| matches!(table.state, TableState::Live))
1110    {
1111        let handle = db
1112            .table(&table.name)
1113            .map_err(|e| DataFusionError::Execution(e.to_string()))?;
1114        let table_guard = handle.lock();
1115        let table_dir = db.root().join(TABLES_DIR).join(table.table_id.to_string());
1116        rows.push(Row {
1117            name: table.name,
1118            ty: "table".to_string(),
1119            rows: Some(saturating_i64(table_guard.count())),
1120            runs: Some(saturating_i64(table_guard.run_count() as u64)),
1121            memtable_rows: Some(saturating_i64(table_guard.memtable_len() as u64)),
1122            columns: saturating_i64(table.schema.columns.len() as u64),
1123            storage_bytes: saturating_i64(dir_size(&table_dir)?),
1124        });
1125    }
1126    for table in catalog.external_tables {
1127        let state_dir = db.root().join(VTAB_DIR).join(&table.name);
1128        rows.push(Row {
1129            name: table.name,
1130            ty: "external".to_string(),
1131            rows: None,
1132            runs: None,
1133            memtable_rows: None,
1134            columns: saturating_i64(table.declared_schema.columns.len() as u64),
1135            storage_bytes: saturating_i64(dir_size(&state_dir)?),
1136        });
1137    }
1138    rows.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.ty.cmp(&b.ty)));
1139
1140    let names = rows.iter().map(|row| row.name.clone()).collect::<Vec<_>>();
1141    let types = rows.iter().map(|row| row.ty.clone()).collect::<Vec<_>>();
1142    let row_counts = rows.iter().map(|row| row.rows).collect::<Vec<_>>();
1143    let runs = rows.iter().map(|row| row.runs).collect::<Vec<_>>();
1144    let memtable_rows = rows.iter().map(|row| row.memtable_rows).collect::<Vec<_>>();
1145    let columns = rows.iter().map(|row| row.columns).collect::<Vec<_>>();
1146    let storage_bytes = rows.iter().map(|row| row.storage_bytes).collect::<Vec<_>>();
1147
1148    Ok(vec![RecordBatch::try_new(
1149        schema,
1150        vec![
1151            Arc::new(StringArray::from(names)) as ArrayRef,
1152            Arc::new(StringArray::from(types)),
1153            Arc::new(Int64Array::from(row_counts)),
1154            Arc::new(Int64Array::from(runs)),
1155            Arc::new(Int64Array::from(memtable_rows)),
1156            Arc::new(Int64Array::from(columns)),
1157            Arc::new(Int64Array::from(storage_bytes)),
1158        ],
1159    )?])
1160}
1161
1162fn dir_size(path: &Path) -> DFResult<u64> {
1163    if !path.exists() {
1164        return Ok(0);
1165    }
1166    let metadata = std::fs::symlink_metadata(path)
1167        .map_err(|e| DataFusionError::Execution(format!("stat {:?}: {e}", path)))?;
1168    if metadata.is_file() {
1169        return Ok(metadata.len());
1170    }
1171    if !metadata.is_dir() {
1172        return Ok(0);
1173    }
1174
1175    let mut total = 0_u64;
1176    for entry in std::fs::read_dir(path).map_err(|e| DataFusionError::Execution(format!("{e}")))? {
1177        let entry = entry.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
1178        total = total.saturating_add(dir_size(&entry.path())?);
1179    }
1180    Ok(total)
1181}
1182
1183fn saturating_i64(value: u64) -> i64 {
1184    i64::try_from(value).unwrap_or(i64::MAX)
1185}
1186
1187fn unsupported_plan(request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1188    Ok(ExternalPlan::new(
1189        request
1190            .filters
1191            .iter()
1192            .map(|_| TableProviderFilterPushDown::Unsupported)
1193            .collect(),
1194        None,
1195        1.0,
1196        false,
1197    ))
1198}
1199
1200fn project_scan(
1201    full_schema: SchemaRef,
1202    batches: Vec<RecordBatch>,
1203    projection: Option<&[usize]>,
1204    limit: Option<usize>,
1205) -> DFResult<ExternalScan> {
1206    let Some(projection) = projection else {
1207        return Ok(ExternalScan {
1208            schema: full_schema,
1209            batches: limit_batches(batches, limit),
1210        });
1211    };
1212    let schema = Arc::new(ArrowSchema::new(
1213        projection
1214            .iter()
1215            .map(|idx| full_schema.field(*idx).clone())
1216            .collect::<Vec<_>>(),
1217    ));
1218    let projected = batches
1219        .into_iter()
1220        .map(|batch| {
1221            let columns = projection
1222                .iter()
1223                .map(|idx| batch.column(*idx).clone())
1224                .collect::<Vec<_>>();
1225            if columns.is_empty() {
1226                RecordBatch::try_new_with_options(
1227                    schema.clone(),
1228                    columns,
1229                    &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
1230                )
1231            } else {
1232                RecordBatch::try_new(schema.clone(), columns)
1233            }
1234        })
1235        .collect::<std::result::Result<Vec<_>, _>>()?;
1236    Ok(ExternalScan {
1237        schema,
1238        batches: limit_batches(projected, limit),
1239    })
1240}
1241
1242fn limit_batches(batches: Vec<RecordBatch>, limit: Option<usize>) -> Vec<RecordBatch> {
1243    let Some(mut remaining) = limit else {
1244        return batches;
1245    };
1246    let mut limited = Vec::new();
1247    for batch in batches {
1248        if remaining == 0 {
1249            break;
1250        }
1251        let take = remaining.min(batch.num_rows());
1252        limited.push(batch.slice(0, take));
1253        remaining -= take;
1254    }
1255    limited
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260    use super::*;
1261
1262    #[test]
1263    fn project_scan_applies_limit_after_projection() {
1264        let schema = Arc::new(ArrowSchema::new(vec![
1265            Field::new("id", DataType::Int64, false),
1266            Field::new("value", DataType::Int64, false),
1267        ]));
1268        let batch = RecordBatch::try_new(
1269            schema.clone(),
1270            vec![
1271                Arc::new(Int64Array::from(vec![1, 2, 3, 4])) as ArrayRef,
1272                Arc::new(Int64Array::from(vec![10, 11, 12, 13])) as ArrayRef,
1273            ],
1274        )
1275        .unwrap();
1276
1277        let scan = project_scan(schema, vec![batch], Some(&[1]), Some(2)).unwrap();
1278
1279        assert_eq!(scan.schema.fields().len(), 1);
1280        assert_eq!(scan.schema.field(0).name(), "value");
1281        assert_eq!(scan.batches.len(), 1);
1282        assert_eq!(scan.batches[0].num_columns(), 1);
1283        assert_eq!(scan.batches[0].num_rows(), 2);
1284        let values = scan.batches[0]
1285            .column(0)
1286            .as_any()
1287            .downcast_ref::<Int64Array>()
1288            .unwrap();
1289        assert_eq!(values.value(0), 10);
1290        assert_eq!(values.value(1), 11);
1291    }
1292
1293    #[test]
1294    fn external_plan_derives_accepted_and_residual_filter_metadata() {
1295        let plan = ExternalPlan::new(
1296            vec![
1297                TableProviderFilterPushDown::Exact,
1298                TableProviderFilterPushDown::Inexact,
1299                TableProviderFilterPushDown::Unsupported,
1300            ],
1301            Some(42),
1302            3.5,
1303            true,
1304        );
1305
1306        assert_eq!(
1307            plan.accepted_filters,
1308            vec![
1309                AcceptedFilter {
1310                    filter_index: 0,
1311                    pushdown: TableProviderFilterPushDown::Exact,
1312                },
1313                AcceptedFilter {
1314                    filter_index: 1,
1315                    pushdown: TableProviderFilterPushDown::Inexact,
1316                },
1317            ]
1318        );
1319        assert!(plan.residual_filters_required);
1320        assert_eq!(plan.estimated_rows, Some(42));
1321        assert_eq!(plan.estimated_cost, 3.5);
1322        assert!(plan.order_satisfied);
1323    }
1324}
1325
1326struct KvStoreModule;
1327
1328impl ExternalTableModule for KvStoreModule {
1329    fn name(&self) -> &str {
1330        "kv_store"
1331    }
1332
1333    fn descriptor(&self) -> ExternalModuleDescriptor {
1334        ExternalModuleDescriptor {
1335            schema: kv_store_schema(),
1336            hidden_columns: Vec::new(),
1337            capabilities: ModuleCapabilities {
1338                writable: true,
1339                deterministic: true,
1340                trigger_safe: true,
1341                transaction_safe: true,
1342                ..ModuleCapabilities::default()
1343            },
1344        }
1345    }
1346
1347    fn connect(
1348        &self,
1349        ctx: &ModuleConnectCtx<'_>,
1350        entry: &ExternalTableEntry,
1351    ) -> Result<Arc<dyn ExternalTable>> {
1352        ensure_no_args(entry, self.name())?;
1353        let rows = read_state_rows(ctx.db, entry)?;
1354        let schema = arrow_conv::arrow_schema(&entry.declared_schema)?;
1355        let batches = core_rows_to_batches(&entry.declared_schema, rows)?;
1356        Ok(Arc::new(KvStoreExternalTable { schema, batches }))
1357    }
1358
1359    fn read_rows(
1360        &self,
1361        ctx: &ModuleConnectCtx<'_>,
1362        entry: &ExternalTableEntry,
1363    ) -> Result<Vec<HashMap<u16, Value>>> {
1364        ensure_no_args(entry, self.name())?;
1365        read_state_rows(ctx.db, entry)
1366    }
1367
1368    fn prepare_rows(
1369        &self,
1370        _ctx: &ModuleConnectCtx<'_>,
1371        entry: &ExternalTableEntry,
1372        rows: Vec<HashMap<u16, Value>>,
1373    ) -> Result<Vec<u8>> {
1374        ensure_no_args(entry, self.name())?;
1375        validate_external_rows(&entry.declared_schema, &rows)?;
1376        encode_state_rows(&rows)
1377    }
1378}
1379
1380#[derive(Clone)]
1381struct KvStoreExternalTable {
1382    schema: SchemaRef,
1383    batches: Vec<RecordBatch>,
1384}
1385
1386impl std::fmt::Debug for KvStoreExternalTable {
1387    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1388        f.debug_struct("KvStoreExternalTable")
1389            .field("schema", &self.schema)
1390            .finish_non_exhaustive()
1391    }
1392}
1393
1394impl ExternalTable for KvStoreExternalTable {
1395    fn schema(&self) -> SchemaRef {
1396        self.schema.clone()
1397    }
1398
1399    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1400        unsupported_plan(request)
1401    }
1402
1403    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
1404        project_scan(
1405            self.schema.clone(),
1406            self.batches.clone(),
1407            request.projection.as_deref(),
1408            request.limit,
1409        )
1410    }
1411}
1412
1413fn kv_store_schema() -> CoreSchema {
1414    CoreSchema {
1415        schema_id: 0,
1416        columns: vec![
1417            CoreColumnDef {
1418                id: 1,
1419                name: "key".to_string(),
1420                ty: TypeId::Bytes,
1421                flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1422                default_value: None,
1423            },
1424            CoreColumnDef {
1425                id: 2,
1426                name: "value".to_string(),
1427                ty: TypeId::Bytes,
1428                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1429                default_value: None,
1430            },
1431        ],
1432        indexes: Vec::new(),
1433        colocation: Vec::new(),
1434        constraints: Default::default(),
1435        clustered: false,
1436    }
1437}
1438
1439#[derive(Serialize, Deserialize)]
1440struct ExternalRowState {
1441    cells: Vec<(u16, Value)>,
1442}
1443
1444const KV_STATE_MAGIC: &[u8] = b"mongreldb.external.kv.v1\n";
1445
1446#[derive(Serialize, Deserialize)]
1447struct ExternalKvState {
1448    entries: Vec<(Vec<u8>, Vec<u8>)>,
1449}
1450
1451fn state_file(db: &Arc<Database>, entry: &ExternalTableEntry) -> PathBuf {
1452    db.root()
1453        .join(VTAB_DIR)
1454        .join(&entry.name)
1455        .join("state.json")
1456}
1457
1458pub(crate) fn external_table_state_bytes(
1459    db: &Arc<Database>,
1460    entry: &ExternalTableEntry,
1461) -> Result<Vec<u8>> {
1462    let path = state_file(db, entry);
1463    if !path.exists() {
1464        return Ok(Vec::new());
1465    }
1466    fs::read(&path)
1467        .map_err(|e| MongrelQueryError::Schema(format!("read external state {:?}: {e}", path)))
1468}
1469
1470fn read_state_rows(
1471    db: &Arc<Database>,
1472    entry: &ExternalTableEntry,
1473) -> Result<Vec<HashMap<u16, Value>>> {
1474    let bytes = external_table_state_bytes(db, entry)?;
1475    if bytes.is_empty() {
1476        return Ok(Vec::new());
1477    }
1478    decode_state_rows(&bytes)
1479}
1480
1481fn encode_state_rows(rows: &[HashMap<u16, Value>]) -> Result<Vec<u8>> {
1482    let state = rows
1483        .iter()
1484        .map(|row| {
1485            let mut cells = row
1486                .iter()
1487                .map(|(id, value)| (*id, value.clone()))
1488                .collect::<Vec<_>>();
1489            cells.sort_by_key(|(id, _)| *id);
1490            ExternalRowState { cells }
1491        })
1492        .collect::<Vec<_>>();
1493    serde_json::to_vec(&state)
1494        .map_err(|e| MongrelQueryError::Schema(format!("encode external state: {e}")))
1495}
1496
1497fn decode_state_rows(state: &[u8]) -> Result<Vec<HashMap<u16, Value>>> {
1498    let rows: Vec<ExternalRowState> = serde_json::from_slice(state)
1499        .map_err(|e| MongrelQueryError::Schema(format!("decode external state: {e}")))?;
1500    Ok(rows
1501        .into_iter()
1502        .map(|row| row.cells.into_iter().collect())
1503        .collect())
1504}
1505
1506fn decode_kv_state(state: &[u8]) -> Result<BTreeMap<Vec<u8>, Vec<u8>>> {
1507    if state.is_empty() {
1508        return Ok(BTreeMap::new());
1509    }
1510    let json = state.strip_prefix(KV_STATE_MAGIC).ok_or_else(|| {
1511        MongrelQueryError::Schema(
1512            "external transaction state is not in key/value module format".into(),
1513        )
1514    })?;
1515    let decoded: ExternalKvState = serde_json::from_slice(json)
1516        .map_err(|e| MongrelQueryError::Schema(format!("decode external kv state: {e}")))?;
1517    Ok(decoded.entries.into_iter().collect())
1518}
1519
1520fn encode_kv_state(state: &BTreeMap<Vec<u8>, Vec<u8>>) -> Result<Vec<u8>> {
1521    let encoded = ExternalKvState {
1522        entries: state
1523            .iter()
1524            .map(|(key, value)| (key.clone(), value.clone()))
1525            .collect(),
1526    };
1527    let mut out = KV_STATE_MAGIC.to_vec();
1528    out.extend(
1529        serde_json::to_vec(&encoded)
1530            .map_err(|e| MongrelQueryError::Schema(format!("encode external kv state: {e}")))?,
1531    );
1532    Ok(out)
1533}
1534
1535fn validate_external_rows(schema: &CoreSchema, rows: &[HashMap<u16, Value>]) -> Result<()> {
1536    let mut known = HashSet::new();
1537    let mut pk_cols = Vec::new();
1538    for column in &schema.columns {
1539        known.insert(column.id);
1540        if column.flags.contains(ColumnFlags::PRIMARY_KEY) {
1541            pk_cols.push(column.id);
1542        }
1543    }
1544    let mut keys = HashSet::new();
1545    for row in rows {
1546        for column_id in row.keys() {
1547            if !known.contains(column_id) {
1548                return Err(MongrelQueryError::Schema(format!(
1549                    "external row contains unknown column id {column_id}"
1550                )));
1551            }
1552        }
1553        if !pk_cols.is_empty() {
1554            let mut key = Vec::new();
1555            for column_id in &pk_cols {
1556                let value = row.get(column_id).ok_or_else(|| {
1557                    MongrelQueryError::Schema(format!(
1558                        "external row is missing primary key column {column_id}"
1559                    ))
1560                })?;
1561                key.extend_from_slice(&column_id.to_be_bytes());
1562                key.extend_from_slice(&value.encode_key());
1563                key.push(0);
1564            }
1565            if !keys.insert(key) {
1566                return Err(MongrelQueryError::Schema(
1567                    "external table primary key conflict".into(),
1568                ));
1569            }
1570        }
1571    }
1572    Ok(())
1573}
1574
1575fn core_rows_to_batches(
1576    schema: &CoreSchema,
1577    rows: Vec<HashMap<u16, Value>>,
1578) -> Result<Vec<RecordBatch>> {
1579    let arrow_schema = arrow_conv::arrow_schema(schema)?;
1580    let arrays = schema
1581        .columns
1582        .iter()
1583        .map(|column| {
1584            let values = rows
1585                .iter()
1586                .map(|row| row.get(&column.id).cloned().unwrap_or(Value::Null))
1587                .collect::<Vec<_>>();
1588            arrow_conv::build_array(column.ty.clone(), &values)
1589        })
1590        .collect::<Result<Vec<_>>>()?;
1591    Ok(vec![RecordBatch::try_new(arrow_schema, arrays)
1592        .map_err(|e| MongrelQueryError::Arrow(e.to_string()))?])
1593}
1594
1595struct FtsDocsModule;
1596
1597impl ExternalTableModule for FtsDocsModule {
1598    fn name(&self) -> &str {
1599        "fts_docs"
1600    }
1601
1602    fn descriptor(&self) -> ExternalModuleDescriptor {
1603        ExternalModuleDescriptor {
1604            schema: fts_docs_schema(),
1605            hidden_columns: vec!["query".to_string()],
1606            capabilities: ModuleCapabilities {
1607                writable: true,
1608                deterministic: true,
1609                trigger_safe: true,
1610                ..ModuleCapabilities::default()
1611            },
1612        }
1613    }
1614
1615    fn indexes(&self, entry: &ExternalTableEntry) -> Result<Vec<ExternalModuleIndex>> {
1616        Ok(vec![ExternalModuleIndex::new(
1617            format!("{}_fts_inverted", entry.name),
1618            vec![2],
1619        )])
1620    }
1621
1622    fn connect(
1623        &self,
1624        ctx: &ModuleConnectCtx<'_>,
1625        entry: &ExternalTableEntry,
1626    ) -> Result<Arc<dyn ExternalTable>> {
1627        let options = fts_options(entry)?;
1628        let rows = read_state_rows(ctx.db, entry)?;
1629        let index = FtsInvertedIndex::build(&rows, &options);
1630        Ok(Arc::new(FtsDocsExternalTable {
1631            schema: arrow_conv::arrow_schema(&entry.declared_schema)?,
1632            core_schema: entry.declared_schema.clone(),
1633            options,
1634            index,
1635            rows,
1636        }))
1637    }
1638
1639    fn read_rows(
1640        &self,
1641        ctx: &ModuleConnectCtx<'_>,
1642        entry: &ExternalTableEntry,
1643    ) -> Result<Vec<HashMap<u16, Value>>> {
1644        let _ = fts_options(entry)?;
1645        read_state_rows(ctx.db, entry)
1646    }
1647
1648    fn prepare_rows(
1649        &self,
1650        _ctx: &ModuleConnectCtx<'_>,
1651        entry: &ExternalTableEntry,
1652        rows: Vec<HashMap<u16, Value>>,
1653    ) -> Result<Vec<u8>> {
1654        let _ = fts_options(entry)?;
1655        let rows = rows
1656            .into_iter()
1657            .map(|mut row| {
1658                row.remove(&3);
1659                row.remove(&4);
1660                row.remove(&5);
1661                row.remove(&6);
1662                row
1663            })
1664            .collect::<Vec<_>>();
1665        validate_external_rows(&entry.declared_schema, &rows)?;
1666        encode_state_rows(&rows)
1667    }
1668}
1669
1670#[derive(Clone)]
1671struct FtsDocsExternalTable {
1672    schema: SchemaRef,
1673    core_schema: CoreSchema,
1674    options: FtsOptions,
1675    index: FtsInvertedIndex,
1676    rows: Vec<HashMap<u16, Value>>,
1677}
1678
1679impl std::fmt::Debug for FtsDocsExternalTable {
1680    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1681        f.debug_struct("FtsDocsExternalTable")
1682            .field("schema", &self.schema)
1683            .finish_non_exhaustive()
1684    }
1685}
1686
1687impl ExternalTable for FtsDocsExternalTable {
1688    fn schema(&self) -> SchemaRef {
1689        self.schema.clone()
1690    }
1691
1692    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
1693        Ok(ExternalPlan::new(
1694            request
1695                .raw_filters
1696                .iter()
1697                .map(|filter| {
1698                    if fts_query_from_expr(filter, &self.options).is_some() {
1699                        TableProviderFilterPushDown::Exact
1700                    } else {
1701                        TableProviderFilterPushDown::Unsupported
1702                    }
1703                })
1704                .collect(),
1705            None,
1706            1.0,
1707            false,
1708        ))
1709    }
1710
1711    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
1712        let query = request
1713            .raw_filters
1714            .iter()
1715            .filter_map(|filter| fts_query_from_expr(filter, &self.options))
1716            .reduce(FtsQuery::and);
1717        let rows = if let Some(query) = query.as_ref() {
1718            self.index
1719                .candidates(query)
1720                .into_iter()
1721                .filter_map(|idx| {
1722                    let row = self.rows.get(idx)?;
1723                    let score = fts_match_score(row, query, &self.options)?;
1724                    Some(fts_enrich_row(
1725                        row.clone(),
1726                        Some(query),
1727                        &self.options,
1728                        Some(score),
1729                    ))
1730                })
1731                .collect::<Vec<_>>()
1732        } else {
1733            self.rows
1734                .iter()
1735                .cloned()
1736                .map(|row| fts_enrich_row(row, None, &self.options, None))
1737                .collect::<Vec<_>>()
1738        };
1739        project_scan(
1740            self.schema.clone(),
1741            core_rows_to_batches(&self.core_schema, rows)
1742                .map_err(|e| DataFusionError::Execution(e.to_string()))?,
1743            request.projection.as_deref(),
1744            request.limit,
1745        )
1746    }
1747}
1748
1749fn fts_docs_schema() -> CoreSchema {
1750    CoreSchema {
1751        schema_id: 0,
1752        columns: vec![
1753            CoreColumnDef {
1754                id: 1,
1755                name: "doc_id".to_string(),
1756                ty: TypeId::Int64,
1757                flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
1758                default_value: None,
1759            },
1760            CoreColumnDef {
1761                id: 2,
1762                name: "text".to_string(),
1763                ty: TypeId::Bytes,
1764                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1765                default_value: None,
1766            },
1767            CoreColumnDef {
1768                id: 3,
1769                name: "query".to_string(),
1770                ty: TypeId::Bytes,
1771                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1772                default_value: None,
1773            },
1774            CoreColumnDef {
1775                id: 4,
1776                name: "rank".to_string(),
1777                ty: TypeId::Float64,
1778                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1779                default_value: None,
1780            },
1781            CoreColumnDef {
1782                id: 5,
1783                name: "snippet".to_string(),
1784                ty: TypeId::Bytes,
1785                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1786                default_value: None,
1787            },
1788            CoreColumnDef {
1789                id: 6,
1790                name: "highlight".to_string(),
1791                ty: TypeId::Bytes,
1792                flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
1793                default_value: None,
1794            },
1795        ],
1796        indexes: Vec::new(),
1797        colocation: Vec::new(),
1798        constraints: Default::default(),
1799        clustered: false,
1800    }
1801}
1802
1803#[derive(Clone)]
1804struct FtsOptions {
1805    prefix_queries: bool,
1806    case_sensitive: bool,
1807    min_token_len: usize,
1808    stopwords: HashSet<String>,
1809}
1810
1811impl Default for FtsOptions {
1812    fn default() -> Self {
1813        Self {
1814            prefix_queries: false,
1815            case_sensitive: false,
1816            min_token_len: 1,
1817            stopwords: HashSet::new(),
1818        }
1819    }
1820}
1821
1822impl FtsOptions {
1823    fn normalize(&self, value: &str) -> String {
1824        if self.case_sensitive {
1825            value.to_string()
1826        } else {
1827            value.to_ascii_lowercase()
1828        }
1829    }
1830
1831    fn keep_term(&self, term: &str) -> bool {
1832        term.len() >= self.min_token_len && !self.stopwords.contains(term)
1833    }
1834}
1835
1836#[derive(Clone)]
1837struct FtsQuery {
1838    groups: Vec<Vec<FtsClause>>,
1839    prohibited: Vec<FtsClause>,
1840}
1841
1842impl FtsQuery {
1843    fn and(self, other: Self) -> Self {
1844        let left_groups = if self.groups.is_empty() {
1845            vec![Vec::new()]
1846        } else {
1847            self.groups
1848        };
1849        let right_groups = if other.groups.is_empty() {
1850            vec![Vec::new()]
1851        } else {
1852            other.groups
1853        };
1854        let mut groups = Vec::new();
1855        for left in &left_groups {
1856            for right in &right_groups {
1857                let mut combined = left.clone();
1858                combined.extend(right.clone());
1859                groups.push(combined);
1860            }
1861        }
1862        let mut prohibited = self.prohibited;
1863        prohibited.extend(other.prohibited);
1864        Self { groups, prohibited }
1865    }
1866
1867    fn positive_clauses(&self) -> impl Iterator<Item = &FtsClause> {
1868        self.groups.iter().flatten()
1869    }
1870}
1871
1872#[derive(Clone)]
1873struct FtsInvertedIndex {
1874    terms: HashMap<String, Vec<usize>>,
1875    all_rows: Vec<usize>,
1876}
1877
1878impl FtsInvertedIndex {
1879    fn build(rows: &[HashMap<u16, Value>], options: &FtsOptions) -> Self {
1880        let mut term_sets: HashMap<String, BTreeSet<usize>> = HashMap::new();
1881        for (idx, row) in rows.iter().enumerate() {
1882            let Some(Value::Bytes(text)) = row.get(&2) else {
1883                continue;
1884            };
1885            let text = String::from_utf8_lossy(text);
1886            let mut row_terms = HashSet::new();
1887            for span in token_spans_with_options(&text, options) {
1888                if row_terms.insert(span.term.clone()) {
1889                    term_sets.entry(span.term).or_default().insert(idx);
1890                }
1891            }
1892        }
1893        let terms = term_sets
1894            .into_iter()
1895            .map(|(term, rows)| (term, rows.into_iter().collect()))
1896            .collect();
1897        Self {
1898            terms,
1899            all_rows: (0..rows.len()).collect(),
1900        }
1901    }
1902
1903    fn candidates(&self, query: &FtsQuery) -> Vec<usize> {
1904        let mut rows = if query.groups.is_empty() {
1905            self.all_rows.iter().copied().collect::<BTreeSet<_>>()
1906        } else {
1907            query
1908                .groups
1909                .iter()
1910                .filter_map(|group| self.group_candidates(group))
1911                .flatten()
1912                .collect::<BTreeSet<_>>()
1913        };
1914        for clause in &query.prohibited {
1915            for idx in self.clause_candidates(clause) {
1916                rows.remove(&idx);
1917            }
1918        }
1919        rows.into_iter().collect()
1920    }
1921
1922    fn group_candidates(&self, group: &[FtsClause]) -> Option<Vec<usize>> {
1923        let mut iter = group.iter().map(|clause| self.clause_candidates(clause));
1924        let first = iter.next()?;
1925        let mut current = first.into_iter().collect::<BTreeSet<_>>();
1926        for rows in iter {
1927            let next = rows.into_iter().collect::<BTreeSet<_>>();
1928            current = current.intersection(&next).copied().collect();
1929            if current.is_empty() {
1930                break;
1931            }
1932        }
1933        Some(current.into_iter().collect())
1934    }
1935
1936    fn clause_candidates(&self, clause: &FtsClause) -> Vec<usize> {
1937        match clause {
1938            FtsClause::Term { term, prefix } => {
1939                if *prefix {
1940                    self.terms
1941                        .iter()
1942                        .filter(|(indexed, _)| indexed.starts_with(term))
1943                        .flat_map(|(_, rows)| rows.iter().copied())
1944                        .collect::<BTreeSet<_>>()
1945                        .into_iter()
1946                        .collect()
1947                } else {
1948                    self.terms.get(term).cloned().unwrap_or_default()
1949                }
1950            }
1951            FtsClause::Phrase(terms) => {
1952                let clauses = terms
1953                    .iter()
1954                    .map(|term| FtsClause::Term {
1955                        term: term.clone(),
1956                        prefix: false,
1957                    })
1958                    .collect::<Vec<_>>();
1959                self.group_candidates(&clauses).unwrap_or_default()
1960            }
1961        }
1962    }
1963}
1964
1965#[derive(Clone, PartialEq, Eq)]
1966enum FtsClause {
1967    Term { term: String, prefix: bool },
1968    Phrase(Vec<String>),
1969}
1970
1971enum FtsQueryToken {
1972    Clause(FtsClause),
1973    Or,
1974    Not,
1975}
1976
1977fn fts_options(entry: &ExternalTableEntry) -> Result<FtsOptions> {
1978    let mut options = FtsOptions::default();
1979    for arg in &entry.args {
1980        let raw = module_arg_string(arg).trim();
1981        if raw.is_empty() {
1982            continue;
1983        }
1984        let (key, value) = raw
1985            .split_once('=')
1986            .map_or((raw, "true"), |(key, value)| (key.trim(), value.trim()));
1987        match key.to_ascii_lowercase().as_str() {
1988            "tokenizer" => match value.to_ascii_lowercase().as_str() {
1989                "simple" | "ascii" | "unicode61" => {}
1990                other => {
1991                    return Err(MongrelQueryError::Schema(format!(
1992                        "fts_docs tokenizer {other:?} is not supported"
1993                    )))
1994                }
1995            },
1996            "prefix" | "prefix_queries" => options.prefix_queries = parse_bool_option(value)?,
1997            "case_sensitive" => options.case_sensitive = parse_bool_option(value)?,
1998            "min_token_len" => {
1999                options.min_token_len = value.parse::<usize>().map_err(|e| {
2000                    MongrelQueryError::Schema(format!(
2001                        "fts_docs min_token_len {value:?} must be an integer: {e}"
2002                    ))
2003                })?;
2004                if options.min_token_len == 0 {
2005                    return Err(MongrelQueryError::Schema(
2006                        "fts_docs min_token_len must be at least 1".into(),
2007                    ));
2008                }
2009            }
2010            "stopwords" => {
2011                options.stopwords = value
2012                    .split('|')
2013                    .map(str::trim)
2014                    .filter(|word| !word.is_empty())
2015                    .map(|word| options.normalize(word))
2016                    .collect();
2017            }
2018            other => {
2019                return Err(MongrelQueryError::Schema(format!(
2020                    "fts_docs option {other:?} is not supported"
2021                )))
2022            }
2023        }
2024    }
2025    Ok(options)
2026}
2027
2028fn parse_bool_option(value: &str) -> Result<bool> {
2029    match value.to_ascii_lowercase().as_str() {
2030        "1" | "true" | "yes" | "on" => Ok(true),
2031        "0" | "false" | "no" | "off" => Ok(false),
2032        _ => Err(MongrelQueryError::Schema(format!(
2033            "expected boolean option value, got {value:?}"
2034        ))),
2035    }
2036}
2037
2038fn fts_query_from_expr(expr: &Expr, options: &FtsOptions) -> Option<FtsQuery> {
2039    match expr {
2040        Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::And => {
2041            Some(fts_query_from_expr(left, options)?.and(fts_query_from_expr(right, options)?))
2042        }
2043        Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::Eq => {
2044            let literal = match (left.as_ref(), right.as_ref()) {
2045                (Expr::Column(column), Expr::Literal(literal, _)) if column.name == "query" => {
2046                    literal_string(literal)?
2047                }
2048                (Expr::Literal(literal, _), Expr::Column(column)) if column.name == "query" => {
2049                    literal_string(literal)?
2050                }
2051                _ => return None,
2052            };
2053            parse_fts_query(&literal, options)
2054        }
2055        _ => None,
2056    }
2057}
2058
2059fn parse_fts_query(input: &str, options: &FtsOptions) -> Option<FtsQuery> {
2060    let tokens = fts_query_tokens(input, options);
2061    if tokens.is_empty() {
2062        return None;
2063    }
2064    let mut groups: Vec<Vec<FtsClause>> = vec![Vec::new()];
2065    let mut prohibited = Vec::new();
2066    let mut negate = false;
2067    for token in tokens {
2068        match token {
2069            FtsQueryToken::Or => {
2070                if groups.last().is_some_and(|group| !group.is_empty()) {
2071                    groups.push(Vec::new());
2072                }
2073                negate = false;
2074            }
2075            FtsQueryToken::Not => negate = true,
2076            FtsQueryToken::Clause(clause) => {
2077                if negate {
2078                    prohibited.push(clause);
2079                    negate = false;
2080                } else if let Some(group) = groups.last_mut() {
2081                    group.push(clause);
2082                }
2083            }
2084        }
2085    }
2086    groups.retain(|group| !group.is_empty());
2087    if groups.is_empty() && prohibited.is_empty() {
2088        None
2089    } else {
2090        Some(FtsQuery { groups, prohibited })
2091    }
2092}
2093
2094fn fts_query_tokens(input: &str, options: &FtsOptions) -> Vec<FtsQueryToken> {
2095    let mut tokens = Vec::new();
2096    let mut chars = input.char_indices().peekable();
2097    while let Some((idx, ch)) = chars.next() {
2098        if ch.is_whitespace() {
2099            continue;
2100        }
2101        if ch == '"' {
2102            let start = idx + ch.len_utf8();
2103            let mut end = input.len();
2104            for (next_idx, next_ch) in chars.by_ref() {
2105                if next_ch == '"' {
2106                    end = next_idx;
2107                    break;
2108                }
2109            }
2110            let terms = token_spans_with_options(&input[start..end], options)
2111                .into_iter()
2112                .map(|span| span.term)
2113                .collect::<Vec<_>>();
2114            if !terms.is_empty() {
2115                tokens.push(FtsQueryToken::Clause(FtsClause::Phrase(terms)));
2116            }
2117            continue;
2118        }
2119        let start = idx;
2120        let mut end = idx + ch.len_utf8();
2121        while let Some((next_idx, next_ch)) = chars.peek().copied() {
2122            if next_ch.is_whitespace() {
2123                break;
2124            }
2125            chars.next();
2126            end = next_idx + next_ch.len_utf8();
2127        }
2128        let raw = &input[start..end];
2129        if raw.eq_ignore_ascii_case("OR") {
2130            tokens.push(FtsQueryToken::Or);
2131        } else if raw.eq_ignore_ascii_case("NOT") {
2132            tokens.push(FtsQueryToken::Not);
2133        } else if let Some(rest) = raw.strip_prefix('-') {
2134            tokens.push(FtsQueryToken::Not);
2135            tokens.extend(
2136                word_clauses(rest, options)
2137                    .into_iter()
2138                    .map(FtsQueryToken::Clause),
2139            );
2140        } else {
2141            tokens.extend(
2142                word_clauses(raw, options)
2143                    .into_iter()
2144                    .map(FtsQueryToken::Clause),
2145            );
2146        }
2147    }
2148    tokens
2149}
2150
2151fn word_clauses(raw: &str, options: &FtsOptions) -> Vec<FtsClause> {
2152    let raw = raw.trim_matches(|ch: char| !ch.is_ascii_alphanumeric() && ch != '*');
2153    if raw.is_empty() {
2154        return Vec::new();
2155    }
2156    let prefix = options.prefix_queries && raw.ends_with('*');
2157    let raw = raw.trim_end_matches('*');
2158    token_spans_with_options(raw, options)
2159        .into_iter()
2160        .map(|span| FtsClause::Term {
2161            term: span.term,
2162            prefix,
2163        })
2164        .collect()
2165}
2166
2167fn fts_match_score(
2168    row: &HashMap<u16, Value>,
2169    query: &FtsQuery,
2170    options: &FtsOptions,
2171) -> Option<f64> {
2172    let Some(Value::Bytes(text)) = row.get(&2) else {
2173        return None;
2174    };
2175    let text = String::from_utf8_lossy(text);
2176    let tokens = token_spans_with_options(&text, options);
2177    if query
2178        .prohibited
2179        .iter()
2180        .any(|clause| clause_matches(&tokens, clause))
2181    {
2182        return None;
2183    }
2184    let positive_score = if query.groups.is_empty() {
2185        Some(0.0)
2186    } else {
2187        query
2188            .groups
2189            .iter()
2190            .filter(|group| group.iter().all(|clause| clause_matches(&tokens, clause)))
2191            .map(|group| {
2192                group
2193                    .iter()
2194                    .map(|clause| clause_score(&tokens, clause))
2195                    .sum()
2196            })
2197            .max_by(|left: &f64, right: &f64| left.total_cmp(right))
2198    }?;
2199    Some(positive_score)
2200}
2201
2202fn clause_matches(tokens: &[TokenSpan], clause: &FtsClause) -> bool {
2203    clause_score(tokens, clause) > 0.0
2204}
2205
2206fn clause_score(tokens: &[TokenSpan], clause: &FtsClause) -> f64 {
2207    match clause {
2208        FtsClause::Term { term, prefix } => tokens
2209            .iter()
2210            .filter(|token| token_matches_term(&token.term, term, *prefix))
2211            .count() as f64,
2212        FtsClause::Phrase(terms) => phrase_occurrences(tokens, terms) as f64 * terms.len() as f64,
2213    }
2214}
2215
2216fn token_matches_term(token: &str, term: &str, prefix: bool) -> bool {
2217    if prefix {
2218        token.starts_with(term)
2219    } else {
2220        token == term
2221    }
2222}
2223
2224fn phrase_occurrences(tokens: &[TokenSpan], terms: &[String]) -> usize {
2225    if terms.is_empty() || tokens.len() < terms.len() {
2226        return 0;
2227    }
2228    tokens
2229        .windows(terms.len())
2230        .filter(|window| {
2231            window
2232                .iter()
2233                .zip(terms)
2234                .all(|(token, term)| token.term == *term)
2235        })
2236        .count()
2237}
2238
2239fn token_matches_positive_clause(token: &str, query: &FtsQuery) -> bool {
2240    query.positive_clauses().any(|clause| match clause {
2241        FtsClause::Term { term, prefix } => token_matches_term(token, term, *prefix),
2242        FtsClause::Phrase(terms) => terms.iter().any(|term| term == token),
2243    })
2244}
2245
2246fn fts_enrich_row(
2247    mut row: HashMap<u16, Value>,
2248    query: Option<&FtsQuery>,
2249    options: &FtsOptions,
2250    score: Option<f64>,
2251) -> HashMap<u16, Value> {
2252    row.remove(&3);
2253    row.remove(&4);
2254    row.remove(&5);
2255    row.remove(&6);
2256    let Some(score) = score else {
2257        return row;
2258    };
2259    let text = match row.get(&2) {
2260        Some(Value::Bytes(text)) => String::from_utf8_lossy(text).into_owned(),
2261        _ => String::new(),
2262    };
2263    row.insert(4, Value::Float64(score));
2264    if let Some(query) = query {
2265        row.insert(
2266            5,
2267            Value::Bytes(fts_snippet(&text, query, options).into_bytes()),
2268        );
2269        row.insert(
2270            6,
2271            Value::Bytes(fts_highlight(&text, query, options).into_bytes()),
2272        );
2273    }
2274    row
2275}
2276
2277#[derive(Debug, Clone)]
2278struct TokenSpan {
2279    term: String,
2280    start: usize,
2281    end: usize,
2282}
2283
2284fn token_spans_with_options(input: &str, options: &FtsOptions) -> Vec<TokenSpan> {
2285    let mut spans = Vec::new();
2286    let mut start = None;
2287    for (idx, ch) in input.char_indices() {
2288        if ch.is_ascii_alphanumeric() {
2289            start.get_or_insert(idx);
2290        } else if let Some(lo) = start.take() {
2291            push_token_span(input, &mut spans, lo, idx, options);
2292        }
2293    }
2294    if let Some(lo) = start {
2295        push_token_span(input, &mut spans, lo, input.len(), options);
2296    }
2297    spans
2298}
2299
2300fn push_token_span(
2301    input: &str,
2302    spans: &mut Vec<TokenSpan>,
2303    start: usize,
2304    end: usize,
2305    options: &FtsOptions,
2306) {
2307    if start < end {
2308        let term = options.normalize(&input[start..end]);
2309        if options.keep_term(&term) {
2310            spans.push(TokenSpan { term, start, end });
2311        }
2312    }
2313}
2314
2315fn fts_snippet(text: &str, query: &FtsQuery, options: &FtsOptions) -> String {
2316    let spans = token_spans_with_options(text, options);
2317    if spans.is_empty() {
2318        return String::new();
2319    }
2320    let first_match = spans
2321        .iter()
2322        .position(|span| token_matches_positive_clause(&span.term, query))
2323        .unwrap_or(0);
2324    let lo = first_match.saturating_sub(3);
2325    let hi = (first_match + 5).min(spans.len());
2326    let mut out = Vec::new();
2327    if lo > 0 {
2328        out.push("...".to_string());
2329    }
2330    for span in &spans[lo..hi] {
2331        let token = &text[span.start..span.end];
2332        if token_matches_positive_clause(&span.term, query) {
2333            out.push(format!("[{token}]"));
2334        } else {
2335            out.push(token.to_string());
2336        }
2337    }
2338    if hi < spans.len() {
2339        out.push("...".to_string());
2340    }
2341    out.join(" ")
2342}
2343
2344fn fts_highlight(text: &str, query: &FtsQuery, options: &FtsOptions) -> String {
2345    let spans = token_spans_with_options(text, options);
2346    if spans.is_empty() {
2347        return text.to_string();
2348    }
2349    let mut out = String::new();
2350    let mut cursor = 0;
2351    for span in spans {
2352        out.push_str(&text[cursor..span.start]);
2353        let token = &text[span.start..span.end];
2354        if token_matches_positive_clause(&span.term, query) {
2355            out.push_str("<b>");
2356            out.push_str(token);
2357            out.push_str("</b>");
2358        } else {
2359            out.push_str(token);
2360        }
2361        cursor = span.end;
2362    }
2363    out.push_str(&text[cursor..]);
2364    out
2365}
2366
2367struct RTreeRectsModule;
2368
2369impl ExternalTableModule for RTreeRectsModule {
2370    fn name(&self) -> &str {
2371        "rtree_rects"
2372    }
2373
2374    fn descriptor(&self) -> ExternalModuleDescriptor {
2375        ExternalModuleDescriptor {
2376            schema: rtree_rects_schema(),
2377            hidden_columns: vec![
2378                "query_min_x".to_string(),
2379                "query_max_x".to_string(),
2380                "query_min_y".to_string(),
2381                "query_max_y".to_string(),
2382            ],
2383            capabilities: ModuleCapabilities {
2384                writable: true,
2385                deterministic: true,
2386                trigger_safe: true,
2387                ..ModuleCapabilities::default()
2388            },
2389        }
2390    }
2391
2392    fn indexes(&self, entry: &ExternalTableEntry) -> Result<Vec<ExternalModuleIndex>> {
2393        Ok(vec![ExternalModuleIndex::new(
2394            format!("{}_rtree_spatial", entry.name),
2395            vec![2, 3, 4, 5],
2396        )])
2397    }
2398
2399    fn connect(
2400        &self,
2401        ctx: &ModuleConnectCtx<'_>,
2402        entry: &ExternalTableEntry,
2403    ) -> Result<Arc<dyn ExternalTable>> {
2404        ensure_no_args(entry, self.name())?;
2405        let rows = read_state_rows(ctx.db, entry)?;
2406        let index = RTreeSpatialIndex::build(&rows);
2407        Ok(Arc::new(RTreeRectsExternalTable {
2408            schema: arrow_conv::arrow_schema(&entry.declared_schema)?,
2409            core_schema: entry.declared_schema.clone(),
2410            index,
2411            rows,
2412        }))
2413    }
2414
2415    fn read_rows(
2416        &self,
2417        ctx: &ModuleConnectCtx<'_>,
2418        entry: &ExternalTableEntry,
2419    ) -> Result<Vec<HashMap<u16, Value>>> {
2420        ensure_no_args(entry, self.name())?;
2421        read_state_rows(ctx.db, entry)
2422    }
2423
2424    fn prepare_rows(
2425        &self,
2426        _ctx: &ModuleConnectCtx<'_>,
2427        entry: &ExternalTableEntry,
2428        rows: Vec<HashMap<u16, Value>>,
2429    ) -> Result<Vec<u8>> {
2430        ensure_no_args(entry, self.name())?;
2431        let rows = rows
2432            .into_iter()
2433            .map(|mut row| {
2434                for column_id in 6..=9 {
2435                    row.remove(&column_id);
2436                }
2437                row
2438            })
2439            .collect::<Vec<_>>();
2440        validate_external_rows(&entry.declared_schema, &rows)?;
2441        encode_state_rows(&rows)
2442    }
2443}
2444
2445#[derive(Clone)]
2446struct RTreeRectsExternalTable {
2447    schema: SchemaRef,
2448    core_schema: CoreSchema,
2449    index: RTreeSpatialIndex,
2450    rows: Vec<HashMap<u16, Value>>,
2451}
2452
2453impl std::fmt::Debug for RTreeRectsExternalTable {
2454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2455        f.debug_struct("RTreeRectsExternalTable")
2456            .field("schema", &self.schema)
2457            .finish_non_exhaustive()
2458    }
2459}
2460
2461impl ExternalTable for RTreeRectsExternalTable {
2462    fn schema(&self) -> SchemaRef {
2463        self.schema.clone()
2464    }
2465
2466    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
2467        Ok(ExternalPlan::new(
2468            request
2469                .raw_filters
2470                .iter()
2471                .map(|filter| {
2472                    if rtree_query_bound(filter).is_some()
2473                        || rtree_query_rect_function(filter).is_some()
2474                    {
2475                        TableProviderFilterPushDown::Exact
2476                    } else {
2477                        TableProviderFilterPushDown::Unsupported
2478                    }
2479                })
2480                .collect(),
2481            None,
2482            1.0,
2483            false,
2484        ))
2485    }
2486
2487    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
2488        let mut hidden_bounds = QueryRect::default();
2489        let mut has_hidden_bounds = false;
2490        for (column, value) in request
2491            .raw_filters
2492            .iter()
2493            .filter_map(|filter| rtree_query_bound(filter))
2494        {
2495            hidden_bounds.set(column, value);
2496            has_hidden_bounds = true;
2497        }
2498        let mut query_rects = request
2499            .raw_filters
2500            .iter()
2501            .filter_map(|filter| rtree_query_rect_function(filter))
2502            .collect::<Vec<_>>();
2503        if has_hidden_bounds || query_rects.is_empty() {
2504            query_rects.push(hidden_bounds);
2505        }
2506        let candidate_rows = query_rects
2507            .iter()
2508            .map(|bounds| {
2509                self.index
2510                    .candidates(*bounds)
2511                    .into_iter()
2512                    .collect::<BTreeSet<_>>()
2513            })
2514            .reduce(|left, right| left.intersection(&right).copied().collect())
2515            .unwrap_or_else(|| self.index.all_rows.iter().copied().collect());
2516        let rows = self
2517            .rows
2518            .iter()
2519            .enumerate()
2520            .filter(|(idx, row)| {
2521                candidate_rows.contains(idx)
2522                    && query_rects
2523                        .iter()
2524                        .all(|bounds| rtree_row_matches(row, *bounds))
2525            })
2526            .map(|(_, row)| row)
2527            .cloned()
2528            .collect::<Vec<_>>();
2529        project_scan(
2530            self.schema.clone(),
2531            core_rows_to_batches(&self.core_schema, rows)
2532                .map_err(|e| DataFusionError::Execution(e.to_string()))?,
2533            request.projection.as_deref(),
2534            request.limit,
2535        )
2536    }
2537}
2538
2539#[derive(Clone, Copy)]
2540struct QueryRect {
2541    min_x: f64,
2542    max_x: f64,
2543    min_y: f64,
2544    max_y: f64,
2545}
2546
2547impl Default for QueryRect {
2548    fn default() -> Self {
2549        Self {
2550            min_x: f64::NEG_INFINITY,
2551            max_x: f64::INFINITY,
2552            min_y: f64::NEG_INFINITY,
2553            max_y: f64::INFINITY,
2554        }
2555    }
2556}
2557
2558impl QueryRect {
2559    fn set(&mut self, column: &str, value: f64) {
2560        match column {
2561            "query_min_x" => self.min_x = value,
2562            "query_max_x" => self.max_x = value,
2563            "query_min_y" => self.min_y = value,
2564            "query_max_y" => self.max_y = value,
2565            _ => {}
2566        }
2567    }
2568}
2569
2570#[derive(Clone)]
2571struct RTreeSpatialIndex {
2572    all_rows: Vec<usize>,
2573    min_x: Vec<(f64, usize)>,
2574    max_x: Vec<(f64, usize)>,
2575    min_y: Vec<(f64, usize)>,
2576    max_y: Vec<(f64, usize)>,
2577}
2578
2579impl RTreeSpatialIndex {
2580    fn build(rows: &[HashMap<u16, Value>]) -> Self {
2581        let mut all_rows = Vec::new();
2582        let mut min_x = Vec::new();
2583        let mut max_x = Vec::new();
2584        let mut min_y = Vec::new();
2585        let mut max_y = Vec::new();
2586        for (idx, row) in rows.iter().enumerate() {
2587            let Some(x0) = row_f64(row, 2) else {
2588                continue;
2589            };
2590            let Some(x1) = row_f64(row, 3) else {
2591                continue;
2592            };
2593            let Some(y0) = row_f64(row, 4) else {
2594                continue;
2595            };
2596            let Some(y1) = row_f64(row, 5) else {
2597                continue;
2598            };
2599            if !(x0.is_finite() && x1.is_finite() && y0.is_finite() && y1.is_finite()) {
2600                continue;
2601            }
2602            all_rows.push(idx);
2603            min_x.push((x0, idx));
2604            max_x.push((x1, idx));
2605            min_y.push((y0, idx));
2606            max_y.push((y1, idx));
2607        }
2608        for values in [&mut min_x, &mut max_x, &mut min_y, &mut max_y] {
2609            values.sort_by(|left, right| left.0.total_cmp(&right.0));
2610        }
2611        Self {
2612            all_rows,
2613            min_x,
2614            max_x,
2615            min_y,
2616            max_y,
2617        }
2618    }
2619
2620    fn candidates(&self, query: QueryRect) -> Vec<usize> {
2621        let mut rows = self.lte(&self.min_x, query.max_x);
2622        for next in [
2623            self.gte(&self.max_x, query.min_x),
2624            self.lte(&self.min_y, query.max_y),
2625            self.gte(&self.max_y, query.min_y),
2626        ] {
2627            rows = rows.intersection(&next).copied().collect();
2628            if rows.is_empty() {
2629                break;
2630            }
2631        }
2632        rows.into_iter().collect()
2633    }
2634
2635    fn lte(&self, values: &[(f64, usize)], bound: f64) -> BTreeSet<usize> {
2636        if bound == f64::INFINITY {
2637            return self.all_rows.iter().copied().collect();
2638        }
2639        if bound.is_nan() {
2640            return BTreeSet::new();
2641        }
2642        let end = values.partition_point(|(value, _)| *value <= bound);
2643        values[..end].iter().map(|(_, idx)| *idx).collect()
2644    }
2645
2646    fn gte(&self, values: &[(f64, usize)], bound: f64) -> BTreeSet<usize> {
2647        if bound == f64::NEG_INFINITY {
2648            return self.all_rows.iter().copied().collect();
2649        }
2650        if bound.is_nan() {
2651            return BTreeSet::new();
2652        }
2653        let start = values.partition_point(|(value, _)| *value < bound);
2654        values[start..].iter().map(|(_, idx)| *idx).collect()
2655    }
2656}
2657
2658fn rtree_query_rect_function(expr: &Expr) -> Option<QueryRect> {
2659    let Expr::ScalarFunction(sf) = expr else {
2660        return None;
2661    };
2662    if !sf.func.name().eq_ignore_ascii_case("rtree_intersects") || sf.args.len() != 8 {
2663        return None;
2664    }
2665    for (arg, expected) in sf
2666        .args
2667        .iter()
2668        .take(4)
2669        .zip(["min_x", "max_x", "min_y", "max_y"])
2670    {
2671        let Expr::Column(column) = arg else {
2672            return None;
2673        };
2674        if column.name != expected {
2675            return None;
2676        }
2677    }
2678    Some(QueryRect {
2679        min_x: literal_f64_from_expr(&sf.args[4])?,
2680        max_x: literal_f64_from_expr(&sf.args[5])?,
2681        min_y: literal_f64_from_expr(&sf.args[6])?,
2682        max_y: literal_f64_from_expr(&sf.args[7])?,
2683    })
2684}
2685
2686fn literal_f64_from_expr(expr: &Expr) -> Option<f64> {
2687    match expr {
2688        Expr::Literal(literal, _) => literal_f64(literal),
2689        _ => None,
2690    }
2691}
2692
2693fn rtree_rects_schema() -> CoreSchema {
2694    CoreSchema {
2695        schema_id: 0,
2696        columns: vec![
2697            CoreColumnDef {
2698                id: 1,
2699                name: "id".to_string(),
2700                ty: TypeId::Int64,
2701                flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
2702                default_value: None,
2703            },
2704            rect_column(2, "min_x"),
2705            rect_column(3, "max_x"),
2706            rect_column(4, "min_y"),
2707            rect_column(5, "max_y"),
2708            rect_column(6, "query_min_x"),
2709            rect_column(7, "query_max_x"),
2710            rect_column(8, "query_min_y"),
2711            rect_column(9, "query_max_y"),
2712        ],
2713        indexes: Vec::new(),
2714        colocation: Vec::new(),
2715        constraints: Default::default(),
2716        clustered: false,
2717    }
2718}
2719
2720fn rect_column(id: u16, name: &str) -> CoreColumnDef {
2721    CoreColumnDef {
2722        id,
2723        name: name.to_string(),
2724        ty: TypeId::Float64,
2725        flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2726        default_value: None,
2727    }
2728}
2729
2730fn rtree_query_bound(expr: &Expr) -> Option<(&str, f64)> {
2731    match expr {
2732        Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == Operator::Eq => {
2733            match (left.as_ref(), right.as_ref()) {
2734                (Expr::Column(column), Expr::Literal(literal, _)) => {
2735                    hidden_rect_column(&column.name).zip(literal_f64(literal))
2736                }
2737                (Expr::Literal(literal, _), Expr::Column(column)) => {
2738                    hidden_rect_column(&column.name).zip(literal_f64(literal))
2739                }
2740                _ => None,
2741            }
2742        }
2743        _ => None,
2744    }
2745}
2746
2747fn hidden_rect_column(name: &str) -> Option<&str> {
2748    match name {
2749        "query_min_x" | "query_max_x" | "query_min_y" | "query_max_y" => Some(name),
2750        _ => None,
2751    }
2752}
2753
2754fn rtree_row_matches(row: &HashMap<u16, Value>, query: QueryRect) -> bool {
2755    let Some(min_x) = row_f64(row, 2) else {
2756        return false;
2757    };
2758    let Some(max_x) = row_f64(row, 3) else {
2759        return false;
2760    };
2761    let Some(min_y) = row_f64(row, 4) else {
2762        return false;
2763    };
2764    let Some(max_y) = row_f64(row, 5) else {
2765        return false;
2766    };
2767    max_x >= query.min_x && min_x <= query.max_x && max_y >= query.min_y && min_y <= query.max_y
2768}
2769
2770fn row_f64(row: &HashMap<u16, Value>, column_id: u16) -> Option<f64> {
2771    match row.get(&column_id) {
2772        Some(Value::Float64(value)) => Some(*value),
2773        Some(Value::Int64(value)) => Some(*value as f64),
2774        _ => None,
2775    }
2776}
2777
2778struct SeriesModule;
2779
2780impl ExternalTableModule for SeriesModule {
2781    fn name(&self) -> &str {
2782        "series"
2783    }
2784
2785    fn descriptor(&self) -> ExternalModuleDescriptor {
2786        ExternalModuleDescriptor {
2787            schema: CoreSchema {
2788                schema_id: 0,
2789                columns: vec![
2790                    CoreColumnDef {
2791                        id: 1,
2792                        name: "value".to_string(),
2793                        ty: TypeId::Int64,
2794                        flags: ColumnFlags::empty(),
2795                        default_value: None,
2796                    },
2797                    CoreColumnDef {
2798                        id: 2,
2799                        name: "start".to_string(),
2800                        ty: TypeId::Int64,
2801                        flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2802                        default_value: None,
2803                    },
2804                    CoreColumnDef {
2805                        id: 3,
2806                        name: "stop".to_string(),
2807                        ty: TypeId::Int64,
2808                        flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2809                        default_value: None,
2810                    },
2811                    CoreColumnDef {
2812                        id: 4,
2813                        name: "step".to_string(),
2814                        ty: TypeId::Int64,
2815                        flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
2816                        default_value: None,
2817                    },
2818                ],
2819                indexes: Vec::new(),
2820                colocation: Vec::new(),
2821                constraints: Default::default(),
2822                clustered: false,
2823            },
2824            hidden_columns: vec!["start".to_string(), "stop".to_string(), "step".to_string()],
2825            capabilities: ModuleCapabilities {
2826                read_only: true,
2827                deterministic: true,
2828                trigger_safe: true,
2829                ..ModuleCapabilities::default()
2830            },
2831        }
2832    }
2833
2834    fn connect(
2835        &self,
2836        _ctx: &ModuleConnectCtx<'_>,
2837        entry: &ExternalTableEntry,
2838    ) -> Result<Arc<dyn ExternalTable>> {
2839        let (start, stop, step) = series_args(entry)?;
2840        let table = SeriesExternalTable::new(start, stop, step)?;
2841        Ok(Arc::new(table))
2842    }
2843}
2844
2845struct SeriesExternalTable {
2846    start: i64,
2847    stop: i64,
2848    step: i64,
2849    schema: SchemaRef,
2850}
2851
2852impl SeriesExternalTable {
2853    fn new(start: i64, stop: i64, step: i64) -> Result<Self> {
2854        if step == 0 {
2855            return Err(MongrelQueryError::Schema(
2856                "series step must not be 0".into(),
2857            ));
2858        }
2859        Ok(Self {
2860            start,
2861            stop,
2862            step,
2863            schema: Arc::new(ArrowSchema::new(vec![Field::new(
2864                "value",
2865                DataType::Int64,
2866                false,
2867            )])),
2868        })
2869    }
2870
2871    fn batches(&self, request: &ExternalPlanRequest<'_>) -> DFResult<Vec<RecordBatch>> {
2872        let mut values = Vec::new();
2873        let mut current = self.start;
2874        while if self.step > 0 {
2875            current <= self.stop
2876        } else {
2877            current >= self.stop
2878        } {
2879            if values.len() >= 1_000_000 {
2880                return Err(DataFusionError::Plan(
2881                    "series output is capped at 1,000,000 rows".into(),
2882                ));
2883            }
2884            if request
2885                .filters
2886                .iter()
2887                .all(|filter| series_filter_matches(filter, current).unwrap_or(true))
2888            {
2889                values.push(current);
2890                if request.limit.is_some_and(|limit| values.len() >= limit) {
2891                    break;
2892                }
2893            }
2894            current = current.saturating_add(self.step);
2895            if (self.step > 0 && current == i64::MAX) || (self.step < 0 && current == i64::MIN) {
2896                break;
2897            }
2898        }
2899        let batch = RecordBatch::try_new(
2900            self.schema.clone(),
2901            vec![Arc::new(Int64Array::from(values)) as ArrayRef],
2902        )?;
2903        Ok(vec![batch])
2904    }
2905}
2906
2907impl std::fmt::Debug for SeriesExternalTable {
2908    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2909        f.debug_struct("SeriesExternalTable")
2910            .field("start", &self.start)
2911            .field("stop", &self.stop)
2912            .field("step", &self.step)
2913            .finish_non_exhaustive()
2914    }
2915}
2916
2917impl ExternalTable for SeriesExternalTable {
2918    fn schema(&self) -> SchemaRef {
2919        self.schema.clone()
2920    }
2921
2922    fn plan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalPlan> {
2923        Ok(ExternalPlan::new(
2924            request
2925                .filters
2926                .iter()
2927                .map(|filter| {
2928                    if series_filter_supported(filter) {
2929                        TableProviderFilterPushDown::Exact
2930                    } else {
2931                        TableProviderFilterPushDown::Unsupported
2932                    }
2933                })
2934                .collect(),
2935            None,
2936            1.0,
2937            self.step > 0,
2938        ))
2939    }
2940
2941    fn scan(&self, request: &ExternalPlanRequest<'_>) -> DFResult<ExternalScan> {
2942        project_scan(
2943            self.schema.clone(),
2944            self.batches(request)?,
2945            request.projection.as_deref(),
2946            request.limit,
2947        )
2948    }
2949}
2950
2951fn series_filter_supported(filter: &ExternalFilter) -> bool {
2952    match filter {
2953        ExternalFilter::And(filters) => filters.iter().all(series_filter_supported),
2954        ExternalFilter::Compare {
2955            column_index,
2956            value,
2957            ..
2958        } => *column_index == 0 && literal_i64(value).is_some(),
2959        ExternalFilter::Unsupported { .. } => false,
2960    }
2961}
2962
2963fn series_filter_matches(filter: &ExternalFilter, value: i64) -> Option<bool> {
2964    match filter {
2965        ExternalFilter::And(filters) => filters.iter().try_fold(true, |matches, filter| {
2966            Some(matches && series_filter_matches(filter, value)?)
2967        }),
2968        ExternalFilter::Compare {
2969            column_index,
2970            op,
2971            value: literal,
2972        } if *column_index == 0 => {
2973            let literal = literal_i64(literal)?;
2974            Some(match op {
2975                ExternalFilterOp::Eq => value == literal,
2976                ExternalFilterOp::NotEq => value != literal,
2977                ExternalFilterOp::Gt => value > literal,
2978                ExternalFilterOp::GtEq => value >= literal,
2979                ExternalFilterOp::Lt => value < literal,
2980                ExternalFilterOp::LtEq => value <= literal,
2981            })
2982        }
2983        _ => None,
2984    }
2985}
2986
2987fn literal_i64(value: &ScalarValue) -> Option<i64> {
2988    match value {
2989        ScalarValue::Int8(Some(v)) => Some(*v as i64),
2990        ScalarValue::Int16(Some(v)) => Some(*v as i64),
2991        ScalarValue::Int32(Some(v)) => Some(*v as i64),
2992        ScalarValue::Int64(Some(v)) => Some(*v),
2993        ScalarValue::UInt8(Some(v)) => Some(*v as i64),
2994        ScalarValue::UInt16(Some(v)) => Some(*v as i64),
2995        ScalarValue::UInt32(Some(v)) => Some(*v as i64),
2996        ScalarValue::UInt64(Some(v)) => i64::try_from(*v).ok(),
2997        _ => None,
2998    }
2999}
3000
3001fn literal_f64(value: &ScalarValue) -> Option<f64> {
3002    match value {
3003        ScalarValue::Float32(Some(v)) => Some(*v as f64),
3004        ScalarValue::Float64(Some(v)) => Some(*v),
3005        _ => literal_i64(value).map(|value| value as f64),
3006    }
3007}
3008
3009fn literal_string(value: &ScalarValue) -> Option<String> {
3010    match value {
3011        ScalarValue::Utf8(Some(v))
3012        | ScalarValue::Utf8View(Some(v))
3013        | ScalarValue::LargeUtf8(Some(v)) => Some(v.clone()),
3014        ScalarValue::Binary(Some(v))
3015        | ScalarValue::BinaryView(Some(v))
3016        | ScalarValue::LargeBinary(Some(v)) => String::from_utf8(v.clone()).ok(),
3017        _ => None,
3018    }
3019}
3020
3021fn series_args(entry: &ExternalTableEntry) -> Result<(i64, i64, i64)> {
3022    let values = entry
3023        .args
3024        .iter()
3025        .map(|arg| match arg {
3026            ModuleArg::Ident(value) | ModuleArg::String(value) | ModuleArg::Number(value) => {
3027                value.parse::<i64>().map_err(|e| {
3028                    MongrelQueryError::Schema(format!(
3029                        "series module argument {value:?} must be an integer: {e}"
3030                    ))
3031                })
3032            }
3033        })
3034        .collect::<Result<Vec<_>>>()?;
3035    match values.as_slice() {
3036        [] => Ok((0, -1, 1)),
3037        [stop] => Ok((0, *stop, 1)),
3038        [start, stop] => Ok((*start, *stop, 1)),
3039        [start, stop, step] => Ok((*start, *stop, *step)),
3040        _ => Err(MongrelQueryError::Schema(
3041            "series external table accepts at most three arguments".into(),
3042        )),
3043    }
3044}