akar-processor 0.1.2

Query processor and execution engine for the Akar embedded graph database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! Query processor — maps logical operators to physical operators and executes them.
//!
//! Pipeline execution model:
//! 1. Scan operators produce raw DataChunks
//! 2. Filter removes non-matching rows
//! 3. Projection selects/transforms columns
//! 4. Limit/OrderBy/Aggregate are applied last

pub mod chunk_helpers;
pub mod join_helpers;
pub mod mapper;
pub mod plan_serializer;
pub mod projection_helper;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_only;
pub mod union_helpers;

pub use chunk_helpers::*;
pub use join_helpers::*;
pub use mapper::*;
pub use plan_serializer::*;
pub use projection_helper::*;
pub use union_helpers::*;

use crate::physical_operator::*;
use akar_common::error::ProcessorError;
use akar_common::types::{PhysicalTypeID, Value};
use akar_common::vector::{DataChunk, ValueVector};
use akar_function::registry::{FunctionRegistry, TableFunction};
use akar_planner::logical_operator::LogicalOperator;
use akar_storage::table::TableCatalog;
use std::sync::{Arc, Mutex};

pub type SequenceFn = Arc<dyn Fn(&str, bool) -> Result<Value, ProcessorError> + Send + Sync>;
pub type SubqueryFn = Arc<dyn Fn(&akar_parser::ast::Query) -> Result<Vec<DataChunk>, ProcessorError> + Send + Sync>;

/// DDL operations that require the schema-level Catalog (from Akar-catalog).
/// These are dispatched via callback because the processor layer doesn't
/// directly own the schema catalog.
#[derive(Debug, Clone)]
pub enum SchemaDdlOp {
    CreateSequence {
        name: String,
        if_not_exists: bool,
        start_value: i64,
        increment: i64,
        min_value: i64,
        max_value: i64,
        cycle: bool,
    },
    DropSequence {
        name: String,
        if_exists: bool,
    },
    ExportDatabase {
        file_path: String,
        file_type: String,
        schema_only: bool,
    },
    ImportDatabase {
        file_path: String,
        query: String,
        index_query: String,
    },
}
pub type SchemaDdlFn = Arc<dyn Fn(SchemaDdlOp) -> Result<String, ProcessorError> + Send + Sync>;

pub trait StandaloneCallHandler: Send + Sync {
    fn execute_call(
        &self,
        name: &str,
        args: &[akar_parser::ast::Expression],
    ) -> Result<Vec<akar_common::vector::DataChunk>, ProcessorError>;
}

pub trait StandaloneCallFn: Send + Sync {
    fn execute(
        &self,
        args: &[akar_parser::ast::Expression],
    ) -> Result<Vec<Vec<akar_common::types::Value>>, ProcessorError>;
    fn aliases(&self) -> Vec<&'static str>;
}

#[derive(Default)]
pub struct StandaloneCallRegistry {
    handlers: std::collections::HashMap<String, std::sync::Arc<dyn StandaloneCallFn>>,
}

impl StandaloneCallRegistry {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn register(&mut self, handler: std::sync::Arc<dyn StandaloneCallFn>) {
        for alias in handler.aliases() {
            self.handlers.insert(alias.to_lowercase(), handler.clone());
        }
    }

    pub fn get(&self, name: &str) -> Option<std::sync::Arc<dyn StandaloneCallFn>> {
        self.handlers.get(&name.to_lowercase()).cloned()
    }
}

/// The query processor executes a physical plan and produces result chunks.
pub struct QueryProcessor {
    function_registry: Option<Arc<Mutex<FunctionRegistry>>>,
    table_catalog: Option<Arc<TableCatalog>>,
    vfs: Option<Arc<akar_common::file_system::VirtualFileSystemRegistry>>,
    standalone_call_handler: Option<Arc<dyn StandaloneCallHandler>>,
    /// Callback for sequence operations (nextval/currval).
    /// Takes (sequence_name, is_nextval) and returns the resulting value.
    sequence_fn: Option<SequenceFn>,
    /// Callback for executing subqueries.
    subquery_fn: Option<SubqueryFn>,
    /// Callback for schema-level DDL operations (CREATE/DROP SEQUENCE, EXPORT/IMPORT DATABASE).
    schema_ddl_fn: Option<SchemaDdlFn>,
    /// MVCC snapshot timestamp for read isolation.
    snapshot_ts: Option<u64>,
    /// Commit history for MVCC visibility checks.
    commit_history: Vec<(u64, u64)>,
    /// Row-level write set accumulated during execution.
    /// Populated by the mapper after each write operation (SET, DELETE, INSERT).
    /// Read by the connection layer after execution for OCC conflict detection.
    written_rows: Mutex<Vec<(u64, u64)>>,
}

impl QueryProcessor {
    pub fn new() -> Self {
        Self {
            function_registry: None,
            table_catalog: None,
            vfs: None,
            standalone_call_handler: None,
            sequence_fn: None,
            subquery_fn: None,
            schema_ddl_fn: None,
            snapshot_ts: None,
            commit_history: Vec::new(),
            written_rows: Mutex::new(Vec::new()),
        }
    }

    /// Create a processor with access to the function registry.
    pub fn with_registry(registry: Arc<Mutex<FunctionRegistry>>) -> Self {
        Self {
            function_registry: Some(registry),
            table_catalog: None,
            vfs: None,
            standalone_call_handler: None,
            sequence_fn: None,
            subquery_fn: None,
            schema_ddl_fn: None,
            snapshot_ts: None,
            commit_history: Vec::new(),
            written_rows: Mutex::new(Vec::new()),
        }
    }

    /// Create a processor with function registry, table catalog access, and VFS.
    pub fn with_catalog(
        registry: Arc<Mutex<FunctionRegistry>>,
        table_catalog: Arc<TableCatalog>,
        vfs: Arc<akar_common::file_system::VirtualFileSystemRegistry>,
    ) -> Self {
        Self {
            function_registry: Some(registry),
            table_catalog: Some(table_catalog),
            vfs: Some(vfs),
            standalone_call_handler: None,
            sequence_fn: None,
            subquery_fn: None,
            schema_ddl_fn: None,
            snapshot_ts: None,
            commit_history: Vec::new(),
            written_rows: Mutex::new(Vec::new()),
        }
    }

    /// Set the sequence operation callback (for nextval/currval).

    pub fn with_standalone_call_handler(mut self, handler: Arc<dyn StandaloneCallHandler>) -> Self {
        self.standalone_call_handler = Some(handler);
        self
    }

    pub fn with_sequence_fn(mut self, f: SequenceFn) -> Self {
        self.sequence_fn = Some(f);
        self
    }

    /// Set the subquery operation callback.
    pub fn with_subquery_fn(mut self, f: SubqueryFn) -> Self {
        self.subquery_fn = Some(f);
        self
    }

    /// Set the schema DDL callback (for CREATE/DROP SEQUENCE, EXPORT/IMPORT DATABASE).
    pub fn with_schema_ddl_fn(mut self, f: SchemaDdlFn) -> Self {
        self.schema_ddl_fn = Some(f);
        self
    }

    /// Set MVCC snapshot parameters for read isolation.
    pub fn with_snapshot(mut self, snapshot_ts: Option<u64>, commit_history: Vec<(u64, u64)>) -> Self {
        self.snapshot_ts = snapshot_ts;
        self.commit_history = commit_history;
        self
    }

    /// Execute a sequence of logical operators by mapping them to physical operators.
    pub fn execute(&self, operators: &[LogicalOperator]) -> Result<Vec<DataChunk>, ProcessorError> {
        self.execute_internal(operators)
    }

    pub fn execute_internal(&self, operators: &[LogicalOperator]) -> Result<Vec<DataChunk>, ProcessorError> {
        if operators.is_empty() {
            return Ok(vec![DataChunk {
                fields: vec![],
                field_types: vec![],
                size: 0,
                field_names: vec![],
                sel_vector: None,
            }]);
        }

        let mut intermediate_result: Option<Vec<DataChunk>> = None;

        for (i, op) in operators.iter().enumerate() {
            let current = intermediate_result.take().unwrap_or_else(|| {
                let mut dummy = DataChunk::new(vec![], vec![]);
                dummy.size = 1;
                vec![dummy]
            });
            let next_op = operators.get(i + 1);

            let mut ctx = mapper::ExecutionContext {
                processor: self,
                function_registry: self.function_registry.clone(),
                table_catalog: self.table_catalog.clone(),
                vfs: self.vfs.clone(),
                standalone_call_handler: self.standalone_call_handler.clone(),
                sequence_fn: self.sequence_fn.clone(),
                subquery_fn: self.subquery_fn.clone(),
                schema_ddl_fn: self.schema_ddl_fn.clone(),
                snapshot_ts: self.snapshot_ts,
                commit_history: self.commit_history.clone(),
                written_rows: Vec::new(),
            };

            let result = mapper::PlanMapper::map_and_execute(op, next_op, current, &mut ctx)?;

            // Accumulate written rows from this operator into the processor's write set
            if !ctx.written_rows.is_empty() {
                if let Ok(mut writes) = self.written_rows.lock() {
                    writes.append(&mut ctx.written_rows);
                }
            }

            if let LogicalOperator::ScanRel(_) = op {
                // Accumulate: extend rather than replace for ScanRel
                match &mut intermediate_result {
                    Some(existing) => existing.extend(result),
                    None => intermediate_result = Some(result),
                }
            } else {
                intermediate_result = Some(result);
            }
        }

        Ok(intermediate_result.unwrap_or_default())
    }

    /// Take the accumulated write set from the processor.
    /// Returns all (table_id, row_id) pairs written during the last execution.
    pub fn take_written_rows(&self) -> Vec<(u64, u64)> {
        self.written_rows
            .lock()
            .map(|mut w| std::mem::take(&mut *w))
            .unwrap_or_default()
    }

    /// Execute a table function call by looking up the function in the registry
    /// and dispatching to the appropriate handler.
    fn execute_table_function(
        &self,
        tf: &akar_planner::logical_operator::LogicalTableFunctionCall,
    ) -> Result<Vec<DataChunk>, ProcessorError> {
        let func_name = &tf.function_name;
        let args: Vec<Value> = Vec::new(); // args would be evaluated from expressions

        // Look up the function in the registry
        if let Some(ref registry) = self.function_registry {
            let reg = registry.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
            if let Some(tbl_fn) = reg.get_table(func_name) {
                match tbl_fn {
                    TableFunction::CustomTable { execute, .. } => {
                        let mut chunk = DataChunk::new(Vec::new(), Vec::new());
                        (execute)(&args, &mut chunk)?;
                        Ok(vec![chunk])
                    }
                    TableFunction::ScanCsv { .. }
                    | TableFunction::ScanParquet { .. }
                    | TableFunction::ScanJson { .. }
                    | TableFunction::ListTables
                    | TableFunction::ShowColumns { .. }
                    | TableFunction::CurrentSetting { .. } => Err(format!(
                        "Table function '{}' cannot be executed dynamically (no callback)",
                        func_name
                    )
                    .into()),
                    TableFunction::Custom { name } if name == "vector_similarity_scan" => {
                        // Evaluate args: [table_name, column_name, query_vector, top_k]
                        // For CALL statement, args are parsed as expressions. We need to evaluate them.
                        // For now, parse from the function args
                        drop(reg);
                        self.execute_vector_similarity_scan(tf)
                    }
                    TableFunction::Custom { name } => {
                        Err(format!("Custom table function '{}' has no registered handler", name).into())
                    }
                }
            } else {
                Err(format!("Table function '{}' not found", func_name).into())
            }
        } else {
            Err(format!(
                "Cannot execute table function '{}': no function registry available",
                func_name
            )
            .into())
        }
    }

    /// Execute a `vector_similarity_scan` table function call.
    ///
    /// Expects CALL vector_similarity_scan(table_name, column_name, query_vector, top_k)
    /// and dispatches to PhysicalVectorSimilarityScan with the processor's TableCatalog.
    fn execute_vector_similarity_scan(
        &self,
        tf: &akar_planner::logical_operator::LogicalTableFunctionCall,
    ) -> Result<Vec<DataChunk>, ProcessorError> {
        // Evaluate arguments from expressions (they should be constants or simple vars)
        if tf.args.len() < 4 {
            return Err(
                "vector_similarity_scan requires 4 arguments: table_name, column_name, query_vector, top_k".into(),
            );
        }

        // For CALL statements, args arrive as Expression AST nodes.
        // Evaluate them to Values. The simplest approach: evaluate constants inline.
        fn eval_expr_to_value(expr: &akar_parser::ast::Expression) -> Option<Value> {
            match expr {
                akar_parser::ast::Expression::Constant(c) => match c {
                    akar_parser::ast::Constant::String(s) => Some(Value::String(s.clone())),
                    akar_parser::ast::Constant::Integer(i) => Some(Value::Int64(*i)),
                    akar_parser::ast::Constant::Float(f) => Some(Value::Double(*f)),
                    akar_parser::ast::Constant::Bool(b) => Some(Value::Bool(*b)),
                    akar_parser::ast::Constant::Null => Some(Value::Null),
                },
                akar_parser::ast::Expression::List(items) => {
                    let vals: Vec<Value> = items.iter().filter_map(eval_expr_to_value).collect();
                    Some(Value::List(vals))
                }
                _ => None, // Non-constant expression — skip
            }
        }

        let table_name = match eval_expr_to_value(&tf.args[0]) {
            Some(Value::String(s)) => s,
            _ => return Err("First argument to vector_similarity_scan must be a table name string".into()),
        };

        let _column_name = match eval_expr_to_value(&tf.args[1]) {
            Some(Value::String(s)) => s,
            _ => return Err("Second argument to vector_similarity_scan must be a column name string".into()),
        };

        let query_vector = match eval_expr_to_value(&tf.args[2]) {
            Some(Value::List(items)) => {
                let mut vec = Vec::with_capacity(items.len());
                for item in &items {
                    match item {
                        Value::Double(d) => vec.push(*d),
                        Value::Int64(i) => vec.push(*i as f64),
                        Value::Int32(i) => vec.push(*i as f64),
                        Value::Float(f) => vec.push(*f as f64),
                        _ => return Err("query_vector must be a list of numbers".into()),
                    }
                }
                vec
            }
            _ => return Err("Third argument to vector_similarity_scan must be a list of numbers".into()),
        };

        let top_k = match eval_expr_to_value(&tf.args[3]) {
            Some(Value::Int64(k)) if k > 0 => k as u64,
            _ => return Err("Fourth argument to vector_similarity_scan must be a positive integer".into()),
        };

        // Find the vector index on this table
        let tc = self
            .table_catalog
            .clone()
            .ok_or_else(|| "No table catalog available for vector_similarity_scan".to_string())?;

        // Look for a vector index matching this table name
        let index_name = {
            let mut found = None;
            for entry in tc.all_vector_indexes() {
                if entry.table_name == table_name {
                    found = Some(entry.name.clone());
                    break;
                }
            }
            found.ok_or_else(|| format!("No vector index found on table '{}'", table_name))?
        };

        // Dispatch to PhysicalVectorSimilarityScan
        let scan = PhysicalVectorSimilarityScan {
            index_name,
            index_id: 0,
            query_vector,
            top_k,
            table_name,
            table_catalog: Some(tc),
        };
        scan.execute(vec![])
    }

    /// Execute a single expression against a DataChunk and return a ValueVector of results.
    pub fn evaluate_expression(
        _expr: &akar_parser::ast::Expression,
        _chunk: &DataChunk,
    ) -> Result<ValueVector, ProcessorError> {
        // Placeholder: return a dummy Int64 vector
        let size = _chunk.size;
        let mut v = ValueVector::new(PhysicalTypeID::Int64, size);
        for i in 0..size {
            v.set_i64(i, 0);
        }
        v.resize(size);
        Ok(v)
    }
}

impl Default for QueryProcessor {
    fn default() -> Self {
        Self::new()
    }
}