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