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::RowIterator;
8use alopex_sql::executor::{build_streaming_pipeline, ColumnInfo, Executor, QueryRowIterator, Row};
9use alopex_sql::planner::typed_expr::Projection;
10use alopex_sql::storage::{SqlValue, TxnBridge};
11use alopex_sql::AlopexDialect;
12use alopex_sql::Parser;
13use alopex_sql::Planner;
14use alopex_sql::Statement;
15use alopex_sql::StatementKind;
16
17use crate::Database;
18use crate::Error;
19use crate::Result;
20use crate::SqlResult;
21use crate::Transaction;
22
23pub struct StreamingRows<'a> {
29 columns: Vec<ColumnInfo>,
30 iter: Box<dyn RowIterator + 'a>,
31 projection: Projection,
32 schema: Vec<alopex_sql::catalog::ColumnMetadata>,
33}
34
35impl<'a> StreamingRows<'a> {
36 pub fn columns(&self) -> &[ColumnInfo] {
38 &self.columns
39 }
40
41 pub fn next_row(&mut self) -> Result<Option<Vec<SqlValue>>> {
45 match self.iter.next_row() {
46 Some(result) => {
47 let row = result.map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
48 let projected = self.project_row(&row)?;
49 Ok(Some(projected))
50 }
51 None => Ok(None),
52 }
53 }
54
55 fn project_row(&self, row: &Row) -> Result<Vec<SqlValue>> {
57 match &self.projection {
58 Projection::All(names) => {
59 let mut result = Vec::with_capacity(names.len());
61 for name in names {
62 let idx = self
63 .schema
64 .iter()
65 .position(|c| &c.name == name)
66 .ok_or_else(|| {
67 Error::Sql(alopex_sql::SqlError::Execution {
68 message: format!("column not found: {}", name),
69 code: "ALOPEX-E020",
70 })
71 })?;
72 result.push(row.values.get(idx).cloned().unwrap_or(SqlValue::Null));
73 }
74 Ok(result)
75 }
76 Projection::Columns(cols) => {
77 use alopex_sql::executor::evaluator::{evaluate, EvalContext};
78 let ctx = EvalContext::new(&row.values);
79 let mut result = Vec::with_capacity(cols.len());
80 for col in cols {
81 let value = evaluate(&col.expr, &ctx)
82 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
83 result.push(value);
84 }
85 Ok(result)
86 }
87 }
88 }
89}
90
91pub enum StreamingQueryResult<R> {
93 Success,
95 RowsAffected(u64),
97 QueryProcessed(R),
99}
100
101pub enum SqlStreamingResult {
106 Success,
108 RowsAffected(u64),
110 Query(QueryRowIterator<'static>),
112}
113
114fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
115 let dialect = AlopexDialect;
116 Parser::parse_sql(&dialect, sql).map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
117}
118
119fn stmt_requires_write(stmt: &Statement) -> bool {
120 !matches!(stmt.kind, StatementKind::Select(_))
121}
122
123fn plan_stmt<'a, S: KVStore>(
124 catalog: &'a alopex_sql::catalog::PersistentCatalog<S>,
125 overlay: &'a CatalogOverlay,
126 stmt: &Statement,
127) -> Result<alopex_sql::LogicalPlan> {
128 let view = TxnCatalogView::new(catalog, overlay);
129 let planner = Planner::new(&view);
130 planner
131 .plan(stmt)
132 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
133}
134
135fn build_column_info(
137 projection: &Projection,
138 schema: &[alopex_sql::catalog::ColumnMetadata],
139) -> Result<Vec<ColumnInfo>> {
140 match projection {
141 Projection::All(names) => {
142 let mut cols = Vec::with_capacity(names.len());
143 for name in names {
144 let meta = schema.iter().find(|c| &c.name == name).ok_or_else(|| {
145 Error::Sql(alopex_sql::SqlError::Execution {
146 message: format!("column not found: {}", name),
147 code: "ALOPEX-E020",
148 })
149 })?;
150 cols.push(ColumnInfo::new(name.clone(), meta.data_type.clone()));
151 }
152 Ok(cols)
153 }
154 Projection::Columns(cols) => {
155 let mut result = Vec::with_capacity(cols.len());
156 for (i, col) in cols.iter().enumerate() {
157 let name = col
158 .alias
159 .clone()
160 .or_else(|| {
161 if let alopex_sql::planner::typed_expr::TypedExprKind::ColumnRef {
162 column,
163 ..
164 } = &col.expr.kind
165 {
166 Some(column.clone())
167 } else {
168 None
169 }
170 })
171 .unwrap_or_else(|| format!("col_{}", i));
172 result.push(ColumnInfo::new(name, col.expr.resolved_type.clone()));
173 }
174 Ok(result)
175 }
176 }
177}
178
179impl Database {
180 pub fn execute_sql(&self, sql: &str) -> Result<SqlResult> {
200 Ok(self
201 .execute_sql_multi(sql)?
202 .pop()
203 .unwrap_or(alopex_sql::ExecutionResult::Success))
204 }
205
206 pub fn execute_sql_multi(&self, sql: &str) -> Result<Vec<SqlResult>> {
227 let stmts = parse_sql(sql)?;
228 if stmts.is_empty() {
229 return Ok(Vec::new());
230 }
231
232 let requires_write = stmts.iter().any(stmt_requires_write);
233 let mode = if requires_write {
234 TxnMode::ReadWrite
235 } else {
236 TxnMode::ReadOnly
237 };
238
239 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
240 let mut overlay = CatalogOverlay::new();
241 let mut borrowed =
242 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
243
244 let mut executor: Executor<_, _> =
245 Executor::new(self.store.clone(), self.sql_catalog.clone());
246
247 let mut results = Vec::with_capacity(stmts.len());
248 for stmt in &stmts {
249 let plan = {
250 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
251 let (_, overlay) = borrowed.split_parts();
252 plan_stmt(&*catalog, &*overlay, stmt)?
253 };
254
255 results.push(
256 executor
257 .execute_in_txn(plan, &mut borrowed)
258 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?,
259 );
260 }
261
262 drop(borrowed);
263
264 txn.commit_self().map_err(Error::Core)?;
269 if mode == TxnMode::ReadWrite {
270 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
271 catalog.apply_overlay(overlay);
272 }
273 if requires_write {
274 let mut cache = self.hnsw_cache.write().expect("hnsw cache lock poisoned");
275 cache.clear();
276 let mut vector_cache = self
277 .vector_cache
278 .write()
279 .expect("vector cache lock poisoned");
280 *vector_cache = None;
281 }
282 Ok(results)
283 }
284
285 pub fn execute_sql_with_rows<F, R>(&self, sql: &str, f: F) -> Result<StreamingQueryResult<R>>
319 where
320 F: FnOnce(StreamingRows<'_>) -> Result<R>,
321 {
322 let stmts = parse_sql(sql)?;
323 if stmts.is_empty() {
324 return Ok(StreamingQueryResult::Success);
325 }
326
327 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
329 let stmt = &stmts[0];
330 let mode = TxnMode::ReadOnly;
331
332 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
333 let mut overlay = CatalogOverlay::new();
334 let mut borrowed =
335 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
336
337 let plan = {
338 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
339 let (_, overlay_ref) = borrowed.split_parts();
340 plan_stmt(&*catalog, overlay_ref, stmt)?
341 };
342
343 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
344 let (mut sql_txn, overlay_ref) = borrowed.split_parts();
345 let view = TxnCatalogView::new(&*catalog, overlay_ref);
346
347 let (iter, projection, schema) = build_streaming_pipeline(&mut sql_txn, &view, plan)
349 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
350
351 let columns = build_column_info(&projection, &schema)?;
353
354 let streaming_rows = StreamingRows {
356 columns,
357 iter,
358 projection,
359 schema,
360 };
361
362 let result = f(streaming_rows)?;
364
365 drop(catalog);
367 drop(borrowed);
368 txn.commit_self().map_err(Error::Core)?;
369
370 return Ok(StreamingQueryResult::QueryProcessed(result));
371 }
372
373 let exec_result = self.execute_sql(sql)?;
375 match exec_result {
376 alopex_sql::ExecutionResult::Success => Ok(StreamingQueryResult::Success),
377 alopex_sql::ExecutionResult::RowsAffected(n) => {
378 Ok(StreamingQueryResult::RowsAffected(n))
379 }
380 alopex_sql::ExecutionResult::Query(_qr) => {
381 Err(Error::Sql(alopex_sql::SqlError::Execution {
384 message: "Streaming not available for multi-statement or complex queries"
385 .into(),
386 code: "ALOPEX-E021",
387 }))
388 }
389 }
390 }
391
392 pub fn execute_sql_streaming(&self, sql: &str) -> Result<SqlStreamingResult> {
425 let stmts = parse_sql(sql)?;
426 if stmts.is_empty() {
427 return Ok(SqlStreamingResult::Success);
428 }
429
430 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
432 let stmt = &stmts[0];
433 let mode = TxnMode::ReadOnly;
434
435 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
436 let mut overlay = CatalogOverlay::new();
437 let mut borrowed =
438 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
439
440 let plan = {
441 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
442 let (_, overlay) = borrowed.split_parts();
443 plan_stmt(&*catalog, &*overlay, stmt)?
444 };
445
446 let (mut sql_txn, _overlay) = borrowed.split_parts();
447
448 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
449 let view = TxnCatalogView::new(&*catalog, _overlay);
450 let iter = execute_query_streaming(&mut sql_txn, &view, plan)
451 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
452
453 drop(catalog);
454 drop(borrowed);
455
456 txn.commit_self().map_err(Error::Core)?;
457
458 return Ok(SqlStreamingResult::Query(iter));
459 }
460
461 let result = self.execute_sql(sql)?;
463 match result {
464 alopex_sql::ExecutionResult::Success => Ok(SqlStreamingResult::Success),
465 alopex_sql::ExecutionResult::RowsAffected(n) => Ok(SqlStreamingResult::RowsAffected(n)),
466 alopex_sql::ExecutionResult::Query(qr) => {
467 use alopex_sql::executor::query::iterator::VecIterator;
469 use alopex_sql::executor::Row;
470 use alopex_sql::planner::typed_expr::Projection;
471
472 let column_names: Vec<String> = qr.columns.iter().map(|c| c.name.clone()).collect();
473 let schema: Vec<alopex_sql::catalog::ColumnMetadata> = qr
474 .columns
475 .iter()
476 .map(|c| alopex_sql::catalog::ColumnMetadata::new(&c.name, c.data_type.clone()))
477 .collect();
478 let rows: Vec<Row> = qr
479 .rows
480 .into_iter()
481 .enumerate()
482 .map(|(i, values)| Row::new(i as u64, values))
483 .collect();
484 let iter = VecIterator::new(rows, schema.clone());
485 let query_iter =
486 QueryRowIterator::new(Box::new(iter), Projection::All(column_names), schema);
487 Ok(SqlStreamingResult::Query(query_iter))
488 }
489 }
490 }
491}
492
493impl<'a> Transaction<'a> {
494 pub fn execute_sql(&mut self, sql: &str) -> Result<SqlResult> {
511 let stmts = parse_sql(sql)?;
512 if stmts.is_empty() {
513 return Ok(alopex_sql::ExecutionResult::Success);
514 }
515
516 if stmts.iter().any(stmt_requires_write) {
517 let mut cache = self
518 .db
519 .hnsw_cache
520 .write()
521 .expect("hnsw cache lock poisoned");
522 cache.clear();
523 let mut vector_cache = self
524 .db
525 .vector_cache
526 .write()
527 .expect("vector cache lock poisoned");
528 *vector_cache = None;
529 }
530
531 let store = self.db.store.clone();
532 let sql_catalog = self.db.sql_catalog.clone();
533
534 let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
535 let mode = txn.mode();
536
537 let mut borrowed =
538 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(txn, mode, &mut self.overlay);
539 let mut executor: Executor<_, _> = Executor::new(store, sql_catalog.clone());
540
541 let mut last = alopex_sql::ExecutionResult::Success;
542 for stmt in &stmts {
543 let plan = {
544 let catalog = sql_catalog.read().expect("catalog lock poisoned");
545 let (_, overlay) = borrowed.split_parts();
546 plan_stmt(&*catalog, &*overlay, stmt)?
547 };
548
549 last = executor
550 .execute_in_txn(plan, &mut borrowed)
551 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
552 }
553
554 Ok(last)
555 }
556}