1use alopex_core::kv::KVStore;
2use alopex_core::types::TxnMode;
3use alopex_core::KVTransaction;
4use alopex_sql::catalog::CatalogOverlay;
5use alopex_sql::catalog::TxnCatalogView;
6use alopex_sql::executor::query::execute_query_streaming;
7use alopex_sql::executor::query::iterator::VecIterator;
8use alopex_sql::executor::query::RowIterator;
9use alopex_sql::executor::{
10 build_streaming_pipeline, ColumnInfo, ExecutionResult, Executor, QueryRowIterator, Row,
11};
12use alopex_sql::planner::typed_expr::Projection;
13use alopex_sql::storage::{SqlValue, TxnBridge};
14use alopex_sql::AlopexDialect;
15use alopex_sql::Parser;
16use alopex_sql::Planner;
17use alopex_sql::Statement;
18use alopex_sql::StatementKind;
19
20use crate::Database;
21use crate::Error;
22use crate::Result;
23use crate::SqlResult;
24use crate::Transaction;
25
26pub struct StreamingRows<'a> {
32 columns: Vec<ColumnInfo>,
33 iter: Box<dyn RowIterator + 'a>,
34 projection: Projection,
35 schema: Vec<alopex_sql::catalog::ColumnMetadata>,
36}
37
38impl<'a> StreamingRows<'a> {
39 pub fn columns(&self) -> &[ColumnInfo] {
41 &self.columns
42 }
43
44 pub fn next_row(&mut self) -> Result<Option<Vec<SqlValue>>> {
48 match self.iter.next_row() {
49 Some(result) => {
50 let row = result.map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
51 let projected = self.project_row(&row)?;
52 Ok(Some(projected))
53 }
54 None => Ok(None),
55 }
56 }
57
58 fn project_row(&self, row: &Row) -> Result<Vec<SqlValue>> {
60 match &self.projection {
61 Projection::All(names) => {
62 let mut result = Vec::with_capacity(names.len());
64 for name in names {
65 let idx = self
66 .schema
67 .iter()
68 .position(|c| &c.name == name)
69 .ok_or_else(|| {
70 Error::Sql(alopex_sql::SqlError::Execution {
71 message: format!("column not found: {}", name),
72 code: "ALOPEX-E020",
73 })
74 })?;
75 result.push(row.values.get(idx).cloned().unwrap_or(SqlValue::Null));
76 }
77 Ok(result)
78 }
79 Projection::Columns(cols) => {
80 use alopex_sql::executor::evaluator::{evaluate, EvalContext};
81 let ctx = EvalContext::new(&row.values);
82 let mut result = Vec::with_capacity(cols.len());
83 for col in cols {
84 let value = evaluate(&col.expr, &ctx)
85 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
86 result.push(value);
87 }
88 Ok(result)
89 }
90 }
91 }
92}
93
94pub enum StreamingQueryResult<R> {
96 Success,
98 RowsAffected(u64),
100 QueryProcessed(R),
102}
103
104pub enum SqlStreamingResult {
109 Success,
111 RowsAffected(u64),
113 Query(QueryRowIterator<'static>),
115}
116
117fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
118 let dialect = AlopexDialect;
119 Parser::parse_sql(&dialect, sql).map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
120}
121
122fn stmt_requires_write(stmt: &Statement) -> bool {
123 !matches!(stmt.kind, StatementKind::Select(_))
124}
125
126fn plan_stmt<'a, S: KVStore>(
127 catalog: &'a alopex_sql::catalog::PersistentCatalog<S>,
128 overlay: &'a CatalogOverlay,
129 stmt: &Statement,
130) -> Result<alopex_sql::LogicalPlan> {
131 let view = TxnCatalogView::new(catalog, overlay);
132 let planner = Planner::new(&view);
133 planner
134 .plan(stmt)
135 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
136}
137
138fn build_column_info(
140 projection: &Projection,
141 schema: &[alopex_sql::catalog::ColumnMetadata],
142) -> Result<Vec<ColumnInfo>> {
143 match projection {
144 Projection::All(names) => {
145 let mut cols = Vec::with_capacity(names.len());
146 for name in names {
147 let meta = schema.iter().find(|c| &c.name == name).ok_or_else(|| {
148 Error::Sql(alopex_sql::SqlError::Execution {
149 message: format!("column not found: {}", name),
150 code: "ALOPEX-E020",
151 })
152 })?;
153 cols.push(ColumnInfo::new(name.clone(), meta.data_type.clone()));
154 }
155 Ok(cols)
156 }
157 Projection::Columns(cols) => {
158 let mut result = Vec::with_capacity(cols.len());
159 for (i, col) in cols.iter().enumerate() {
160 let name = col
161 .alias
162 .clone()
163 .or_else(|| {
164 if let alopex_sql::planner::typed_expr::TypedExprKind::ColumnRef {
165 column,
166 ..
167 } = &col.expr.kind
168 {
169 Some(column.clone())
170 } else {
171 None
172 }
173 })
174 .unwrap_or_else(|| format!("col_{}", i));
175 result.push(ColumnInfo::new(name, col.expr.resolved_type.clone()));
176 }
177 Ok(result)
178 }
179 }
180}
181
182impl Database {
183 pub fn execute_sql(&self, sql: &str) -> Result<SqlResult> {
203 Ok(self
204 .execute_sql_multi(sql)?
205 .pop()
206 .unwrap_or(alopex_sql::ExecutionResult::Success))
207 }
208
209 pub fn execute_sql_multi(&self, sql: &str) -> Result<Vec<SqlResult>> {
230 let stmts = parse_sql(sql)?;
231 if stmts.is_empty() {
232 return Ok(Vec::new());
233 }
234
235 if stmts.len() == 1 {
240 let overlay = CatalogOverlay::new();
241 let plan = {
242 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
243 plan_stmt(&*catalog, &overlay, &stmts[0])?
244 };
245 if matches!(stmts[0].kind, StatementKind::Pragma { .. })
246 || alopex_sql::executor::is_store_direct_plan(&plan)
247 {
248 let mut executor: Executor<_, _> =
249 Executor::new(self.store.clone(), self.sql_catalog.clone());
250 return executor
251 .execute(plan)
252 .map(|result| vec![result])
253 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)));
254 }
255 }
256
257 let requires_write = stmts.iter().any(stmt_requires_write);
258 let mode = if requires_write {
259 TxnMode::ReadWrite
260 } else {
261 TxnMode::ReadOnly
262 };
263
264 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
265 let mut overlay = CatalogOverlay::new();
266 let mut borrowed =
267 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
268
269 let mut executor: Executor<_, _> =
270 Executor::new(self.store.clone(), self.sql_catalog.clone());
271
272 let mut results = Vec::with_capacity(stmts.len());
273 for stmt in &stmts {
274 let plan = {
275 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
276 let (_, overlay) = borrowed.split_parts();
277 plan_stmt(&*catalog, &*overlay, stmt)?
278 };
279
280 results.push(
281 executor
282 .execute_in_txn(plan, &mut borrowed)
283 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?,
284 );
285 }
286
287 drop(borrowed);
288
289 txn.commit_self().map_err(Error::Core)?;
294 if mode == TxnMode::ReadWrite {
295 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
296 catalog.apply_overlay(overlay);
297 }
298 if requires_write {
299 let mut cache = self.hnsw_cache.write().expect("hnsw cache lock poisoned");
300 cache.clear();
301 let mut vector_cache = self
302 .vector_cache
303 .write()
304 .expect("vector cache lock poisoned");
305 *vector_cache = None;
306 }
307 Ok(results)
308 }
309
310 pub fn execute_sql_with_rows<F, R>(&self, sql: &str, f: F) -> Result<StreamingQueryResult<R>>
344 where
345 F: FnOnce(StreamingRows<'_>) -> Result<R>,
346 {
347 let stmts = parse_sql(sql)?;
348 if stmts.is_empty() {
349 return Ok(StreamingQueryResult::Success);
350 }
351
352 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
354 let stmt = &stmts[0];
355 let mode = TxnMode::ReadOnly;
356
357 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
358 let mut overlay = CatalogOverlay::new();
359 let mut borrowed =
360 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
361
362 let plan = {
363 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
364 let (_, overlay_ref) = borrowed.split_parts();
365 plan_stmt(&*catalog, overlay_ref, stmt)?
366 };
367
368 if alopex_sql::executor::is_store_direct_plan(&plan) {
369 let mut executor: Executor<_, _> =
370 Executor::new(self.store.clone(), self.sql_catalog.clone());
371 let result = executor
372 .execute(plan)
373 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
374 let ExecutionResult::Query(query) = result else {
375 return Err(Error::Sql(alopex_sql::SqlError::Execution {
376 message: "store-direct system function did not return rows".into(),
377 code: "ALOPEX-E022",
378 }));
379 };
380 let column_names: Vec<String> = query
381 .columns
382 .iter()
383 .map(|column| column.name.clone())
384 .collect();
385 let schema: Vec<alopex_sql::catalog::ColumnMetadata> = query
386 .columns
387 .iter()
388 .map(|column| {
389 alopex_sql::catalog::ColumnMetadata::new(
390 &column.name,
391 column.data_type.clone(),
392 )
393 })
394 .collect();
395 let rows: Vec<Row> = query
396 .rows
397 .into_iter()
398 .enumerate()
399 .map(|(index, values)| Row::new(index as u64, values))
400 .collect();
401 let iter = VecIterator::new(rows, schema.clone());
402 let streaming_rows = StreamingRows {
403 columns: query.columns,
404 iter: Box::new(iter),
405 projection: Projection::All(column_names),
406 schema,
407 };
408 let result = f(streaming_rows)?;
409 return Ok(StreamingQueryResult::QueryProcessed(result));
410 }
411
412 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
413 let (mut sql_txn, overlay_ref) = borrowed.split_parts();
414 let view = TxnCatalogView::new(&*catalog, overlay_ref);
415
416 let (iter, projection, schema) = build_streaming_pipeline(&mut sql_txn, &view, plan)
418 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
419
420 let columns = build_column_info(&projection, &schema)?;
422
423 let streaming_rows = StreamingRows {
425 columns,
426 iter,
427 projection,
428 schema,
429 };
430
431 let result = f(streaming_rows)?;
433
434 drop(catalog);
436 drop(borrowed);
437 txn.commit_self().map_err(Error::Core)?;
438
439 return Ok(StreamingQueryResult::QueryProcessed(result));
440 }
441
442 let exec_result = self.execute_sql(sql)?;
444 match exec_result {
445 alopex_sql::ExecutionResult::Success => Ok(StreamingQueryResult::Success),
446 alopex_sql::ExecutionResult::RowsAffected(n) => {
447 Ok(StreamingQueryResult::RowsAffected(n))
448 }
449 alopex_sql::ExecutionResult::Query(_qr) => {
450 Err(Error::Sql(alopex_sql::SqlError::Execution {
453 message: "Streaming not available for multi-statement or complex queries"
454 .into(),
455 code: "ALOPEX-E021",
456 }))
457 }
458 }
459 }
460
461 pub fn execute_sql_streaming(&self, sql: &str) -> Result<SqlStreamingResult> {
494 let stmts = parse_sql(sql)?;
495 if stmts.is_empty() {
496 return Ok(SqlStreamingResult::Success);
497 }
498
499 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
501 let stmt = &stmts[0];
502 let mode = TxnMode::ReadOnly;
503
504 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
505 let mut overlay = CatalogOverlay::new();
506 let mut borrowed =
507 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
508
509 let plan = {
510 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
511 let (_, overlay) = borrowed.split_parts();
512 plan_stmt(&*catalog, &*overlay, stmt)?
513 };
514
515 let (mut sql_txn, _overlay) = borrowed.split_parts();
516
517 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
518 let view = TxnCatalogView::new(&*catalog, _overlay);
519 let iter = execute_query_streaming(&mut sql_txn, &view, plan)
520 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
521
522 drop(catalog);
523 drop(borrowed);
524
525 txn.commit_self().map_err(Error::Core)?;
526
527 return Ok(SqlStreamingResult::Query(iter));
528 }
529
530 let result = self.execute_sql(sql)?;
532 match result {
533 alopex_sql::ExecutionResult::Success => Ok(SqlStreamingResult::Success),
534 alopex_sql::ExecutionResult::RowsAffected(n) => Ok(SqlStreamingResult::RowsAffected(n)),
535 alopex_sql::ExecutionResult::Query(qr) => {
536 use alopex_sql::executor::query::iterator::VecIterator;
538 use alopex_sql::executor::Row;
539 use alopex_sql::planner::typed_expr::Projection;
540
541 let column_names: Vec<String> = qr.columns.iter().map(|c| c.name.clone()).collect();
542 let schema: Vec<alopex_sql::catalog::ColumnMetadata> = qr
543 .columns
544 .iter()
545 .map(|c| alopex_sql::catalog::ColumnMetadata::new(&c.name, c.data_type.clone()))
546 .collect();
547 let rows: Vec<Row> = qr
548 .rows
549 .into_iter()
550 .enumerate()
551 .map(|(i, values)| Row::new(i as u64, values))
552 .collect();
553 let iter = VecIterator::new(rows, schema.clone());
554 let query_iter =
555 QueryRowIterator::new(Box::new(iter), Projection::All(column_names), schema);
556 Ok(SqlStreamingResult::Query(query_iter))
557 }
558 }
559 }
560}
561
562impl<'a> Transaction<'a> {
563 pub fn execute_sql(&mut self, sql: &str) -> Result<SqlResult> {
580 let stmts = parse_sql(sql)?;
581 if stmts.is_empty() {
582 return Ok(alopex_sql::ExecutionResult::Success);
583 }
584
585 if stmts.iter().any(stmt_requires_write) {
586 let mut cache = self
587 .db
588 .hnsw_cache
589 .write()
590 .expect("hnsw cache lock poisoned");
591 cache.clear();
592 let mut vector_cache = self
593 .db
594 .vector_cache
595 .write()
596 .expect("vector cache lock poisoned");
597 *vector_cache = None;
598 }
599
600 let store = self.db.store.clone();
601 let sql_catalog = self.db.sql_catalog.clone();
602
603 let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
604 let mode = txn.mode();
605
606 let mut borrowed =
607 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(txn, mode, &mut self.overlay);
608 let mut executor: Executor<_, _> = Executor::new(store, sql_catalog.clone());
609
610 let mut last = alopex_sql::ExecutionResult::Success;
611 for stmt in &stmts {
612 let plan = {
613 let catalog = sql_catalog.read().expect("catalog lock poisoned");
614 let (_, overlay) = borrowed.split_parts();
615 plan_stmt(&*catalog, &*overlay, stmt)?
616 };
617
618 last = executor
619 .execute_in_txn(plan, &mut borrowed)
620 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
621 }
622
623 Ok(last)
624 }
625}