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