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