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