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