1use alopex_core::kv::RangeChangeJournalCapability;
2use alopex_core::kv::{any::AnyKVTransaction, KVStore, OwnedKVTransactionAdapter, ReadAtPoint};
3use alopex_core::types::TxnMode;
4use alopex_core::KVTransaction;
5use alopex_sql::catalog::TxnCatalogView;
6use alopex_sql::catalog::{Catalog, CatalogOverlay};
7use alopex_sql::executor::query::execute_query_streaming;
8use alopex_sql::executor::query::iterator::VecIterator;
9use alopex_sql::executor::query::RowIterator;
10use alopex_sql::executor::{
11 build_streaming_pipeline, ColumnInfo, ExecutionResult, Executor, QueryRowIterator, Row,
12};
13use alopex_sql::planner::typed_expr::Projection;
14use alopex_sql::storage::{LocalRangeChangeJournal, RangeChangeJournalScope, SqlValue, TxnBridge};
15use alopex_sql::AlopexDialect;
16use alopex_sql::Parser;
17use alopex_sql::Planner;
18use alopex_sql::Statement;
19use alopex_sql::StatementKind;
20use std::collections::BTreeMap;
21use std::sync::Arc;
22
23use crate::Database;
24use crate::Error;
25use crate::OwnedEmbeddedTransaction;
26use crate::Result;
27use crate::SqlResult;
28use crate::Transaction;
29
30pub struct StreamingRows<'a> {
36 columns: Vec<ColumnInfo>,
37 iter: Box<dyn RowIterator + 'a>,
38 projection: Projection,
39 schema: Vec<alopex_sql::catalog::ColumnMetadata>,
40}
41
42impl<'a> StreamingRows<'a> {
43 pub fn columns(&self) -> &[ColumnInfo] {
45 &self.columns
46 }
47
48 pub fn next_row(&mut self) -> Result<Option<Vec<SqlValue>>> {
52 match self.iter.next_row() {
53 Some(result) => {
54 let row = result.map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
55 let projected = self.project_row(&row)?;
56 Ok(Some(projected))
57 }
58 None => Ok(None),
59 }
60 }
61
62 fn project_row(&self, row: &Row) -> Result<Vec<SqlValue>> {
64 match &self.projection {
65 Projection::All(names) => {
66 let mut result = Vec::with_capacity(names.len());
68 for name in names {
69 let idx = self
70 .schema
71 .iter()
72 .position(|c| &c.name == name)
73 .ok_or_else(|| {
74 Error::Sql(alopex_sql::SqlError::Execution {
75 message: format!("column not found: {}", name),
76 code: "ALOPEX-E020",
77 })
78 })?;
79 result.push(row.values.get(idx).cloned().unwrap_or(SqlValue::Null));
80 }
81 Ok(result)
82 }
83 Projection::Columns(cols) => {
84 use alopex_sql::executor::evaluator::{evaluate, EvalContext};
85 let ctx = EvalContext::new(&row.values);
86 let mut result = Vec::with_capacity(cols.len());
87 for col in cols {
88 let value = evaluate(&col.expr, &ctx)
89 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
90 result.push(value);
91 }
92 Ok(result)
93 }
94 }
95 }
96}
97
98pub enum StreamingQueryResult<R> {
100 Success,
102 RowsAffected(u64),
104 QueryProcessed(R),
106}
107
108pub enum SqlStreamingResult {
113 Success,
115 RowsAffected(u64),
117 Query(QueryRowIterator<'static>),
119}
120
121fn parse_sql(sql: &str) -> Result<Vec<Statement>> {
122 let dialect = AlopexDialect;
123 Parser::parse_sql(&dialect, sql).map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
124}
125
126fn stmt_requires_write(stmt: &Statement) -> bool {
127 !matches!(stmt.kind, StatementKind::Select(_))
128}
129
130fn stmt_changes_catalog(stmt: &Statement) -> bool {
131 matches!(
132 stmt.kind,
133 StatementKind::CreateTable(_)
134 | StatementKind::DropTable(_)
135 | StatementKind::CreateIndex(_)
136 | StatementKind::DropIndex(_)
137 )
138}
139
140fn stmt_changes_user_data(stmt: &Statement) -> bool {
141 matches!(
142 stmt.kind,
143 StatementKind::Insert(_) | StatementKind::Update(_) | StatementKind::Delete(_)
144 )
145}
146
147pub(crate) fn local_journal_scope<C: Catalog>(catalog: &C) -> RangeChangeJournalScope {
148 let mut index_tables = BTreeMap::new();
149 for table in catalog.list_tables() {
150 for index in catalog.get_indexes_for_table(&table.name) {
151 index_tables.insert(index.index_id, table.table_id);
152 }
153 }
154 RangeChangeJournalScope::local(index_tables)
155}
156
157fn plan_stmt<'a, S: KVStore>(
158 catalog: &'a alopex_sql::catalog::PersistentCatalog<S>,
159 overlay: &'a CatalogOverlay,
160 stmt: &Statement,
161) -> Result<alopex_sql::LogicalPlan> {
162 let view = TxnCatalogView::new(catalog, overlay);
163 let planner = Planner::new(&view);
164 planner
165 .plan(stmt)
166 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))
167}
168
169pub(crate) fn execute_sql_owned(
176 transaction: &mut OwnedEmbeddedTransaction,
177 sql: &str,
178) -> Result<SqlResult> {
179 let statements = parse_sql(sql)?;
180 if statements.is_empty() {
181 return Ok(alopex_sql::ExecutionResult::Success);
182 }
183
184 if statements.iter().any(stmt_requires_write) {
185 let mut cache = transaction
186 .db
187 .hnsw_cache
188 .write()
189 .expect("hnsw cache lock poisoned");
190 cache.clear();
191 let mut vector_cache = transaction
192 .db
193 .vector_cache
194 .write()
195 .expect("vector cache lock poisoned");
196 *vector_cache = None;
197 }
198
199 let db = Arc::clone(&transaction.db);
200 let session = transaction.session.clone();
201 let overlay = &mut transaction.overlay;
202 let catalog_modified = &mut transaction.catalog_modified;
203 let mut outcome = Ok(alopex_sql::ExecutionResult::Success);
204
205 session
206 .with_transaction(|owned| {
207 outcome = (|| {
208 let mut raw = AnyKVTransaction::Owned(OwnedKVTransactionAdapter::new(owned));
209 let mode = raw.mode();
210 let mut borrowed =
211 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut raw, mode, overlay);
212 let mut executor: Executor<_, _> =
213 Executor::new(db.store.clone(), db.sql_catalog.clone());
214 let mut last = alopex_sql::ExecutionResult::Success;
215
216 for (statement_index, stmt) in statements.iter().enumerate() {
217 let plan = {
218 let catalog = db.sql_catalog.read().expect("catalog lock poisoned");
219 let (_, overlay) = borrowed.split_parts();
220 plan_stmt(&*catalog, &*overlay, stmt)?
221 };
222
223 {
224 let catalog = db.sql_catalog.read().expect("catalog lock poisoned");
225 let (_, overlay) = borrowed.split_parts();
226 let view = TxnCatalogView::new(&*catalog, &*overlay);
227 db.record_routing(&view, stmt, statement_index);
228 }
229
230 last = executor
231 .execute_in_txn(plan, &mut borrowed)
232 .map_err(|error| Error::Sql(alopex_sql::SqlError::from(error)))?;
233 }
234
235 if statements.iter().any(stmt_changes_catalog) {
236 *catalog_modified = true;
237 }
238 Ok(last)
239 })();
240 Ok(())
241 })
242 .map_err(Error::Core)?;
243 outcome
244}
245
246fn build_column_info(
248 projection: &Projection,
249 schema: &[alopex_sql::catalog::ColumnMetadata],
250) -> Result<Vec<ColumnInfo>> {
251 match projection {
252 Projection::All(names) => {
253 let mut cols = Vec::with_capacity(names.len());
254 for name in names {
255 let meta = schema.iter().find(|c| &c.name == name).ok_or_else(|| {
256 Error::Sql(alopex_sql::SqlError::Execution {
257 message: format!("column not found: {}", name),
258 code: "ALOPEX-E020",
259 })
260 })?;
261 cols.push(ColumnInfo::new(name.clone(), meta.data_type.clone()));
262 }
263 Ok(cols)
264 }
265 Projection::Columns(cols) => {
266 let mut result = Vec::with_capacity(cols.len());
267 for (i, col) in cols.iter().enumerate() {
268 let name = col
269 .alias
270 .clone()
271 .or_else(|| {
272 if let alopex_sql::planner::typed_expr::TypedExprKind::ColumnRef {
273 column,
274 ..
275 } = &col.expr.kind
276 {
277 Some(column.clone())
278 } else {
279 None
280 }
281 })
282 .unwrap_or_else(|| format!("col_{}", i));
283 result.push(ColumnInfo::new(name, col.expr.resolved_type.clone()));
284 }
285 Ok(result)
286 }
287 }
288}
289
290impl Database {
291 pub fn begin_read_at_sql(
298 &self,
299 point: ReadAtPoint,
300 ) -> Result<alopex_sql::storage::SqlTransaction<'_, alopex_core::kv::AnyKV>> {
301 let transaction = self.store.begin_read_at(&point).map_err(Error::ReadAt)?;
302 Ok(TxnBridge::from_read_at(transaction, point))
303 }
304
305 pub fn execute_sql(&self, sql: &str) -> Result<SqlResult> {
325 Ok(self
326 .execute_sql_multi(sql)?
327 .pop()
328 .unwrap_or(alopex_sql::ExecutionResult::Success))
329 }
330
331 pub fn execute_sql_multi(&self, sql: &str) -> Result<Vec<SqlResult>> {
352 let stmts = parse_sql(sql)?;
353 if stmts.is_empty() {
354 return Ok(Vec::new());
355 }
356
357 if stmts.len() == 1 {
362 let overlay = CatalogOverlay::new();
363 let plan = {
364 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
365 plan_stmt(&*catalog, &overlay, &stmts[0])?
366 };
367 if matches!(stmts[0].kind, StatementKind::Pragma { .. })
368 || alopex_sql::executor::is_store_direct_plan(&plan)
369 {
370 let mut executor: Executor<_, _> =
371 Executor::new(self.store.clone(), self.sql_catalog.clone());
372 return executor
373 .execute(plan)
374 .map(|result| vec![result])
375 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)));
376 }
377 }
378
379 let requires_write = stmts.iter().any(stmt_requires_write);
380 let mode = if requires_write {
381 TxnMode::ReadWrite
382 } else {
383 TxnMode::ReadOnly
384 };
385
386 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
387 let journal = if mode == TxnMode::ReadWrite
388 && stmts.iter().any(stmt_changes_user_data)
389 && self.store.range_change_journal_capability()
390 == RangeChangeJournalCapability::Supported
391 {
392 let scope = {
393 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
394 local_journal_scope(&*catalog)
395 };
396 Some(LocalRangeChangeJournal::capture(&mut txn, scope).map_err(Error::Core)?)
397 } else {
398 None
399 };
400 let mut overlay = CatalogOverlay::new();
401 let mut borrowed =
402 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
403
404 let mut executor: Executor<_, _> =
405 Executor::new(self.store.clone(), self.sql_catalog.clone());
406
407 let mut results = Vec::with_capacity(stmts.len());
408 for (statement_index, stmt) in stmts.iter().enumerate() {
409 let plan = {
410 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
411 let (_, overlay) = borrowed.split_parts();
412 plan_stmt(&*catalog, &*overlay, stmt)?
413 };
414
415 {
416 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
417 let (_, overlay) = borrowed.split_parts();
418 let view = TxnCatalogView::new(&*catalog, &*overlay);
419 self.record_routing(&view, stmt, statement_index);
420 }
421
422 results.push(
423 executor
424 .execute_in_txn(plan, &mut borrowed)
425 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?,
426 );
427 }
428
429 drop(borrowed);
430
431 if let Some(journal) = journal {
432 journal.stage(&mut txn).map_err(Error::Core)?;
433 }
434
435 txn.commit_self().map_err(Error::Core)?;
440 if mode == TxnMode::ReadWrite {
441 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
442 catalog.apply_overlay(overlay);
443 }
444 if stmts.iter().any(stmt_changes_catalog) {
445 self.invalidate_table_info_cache();
446 }
447 if requires_write {
448 let mut cache = self.hnsw_cache.write().expect("hnsw cache lock poisoned");
449 cache.clear();
450 let mut vector_cache = self
451 .vector_cache
452 .write()
453 .expect("vector cache lock poisoned");
454 *vector_cache = None;
455 }
456 Ok(results)
457 }
458
459 pub fn execute_sql_with_rows<F, R>(&self, sql: &str, f: F) -> Result<StreamingQueryResult<R>>
493 where
494 F: FnOnce(StreamingRows<'_>) -> Result<R>,
495 {
496 let stmts = parse_sql(sql)?;
497 if stmts.is_empty() {
498 return Ok(StreamingQueryResult::Success);
499 }
500
501 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
503 let stmt = &stmts[0];
504 let mode = TxnMode::ReadOnly;
505
506 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
507 let mut overlay = CatalogOverlay::new();
508 let mut borrowed =
509 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
510
511 let plan = {
512 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
513 let (_, overlay_ref) = borrowed.split_parts();
514 plan_stmt(&*catalog, overlay_ref, stmt)?
515 };
516
517 if alopex_sql::executor::is_store_direct_plan(&plan) {
518 let mut executor: Executor<_, _> =
519 Executor::new(self.store.clone(), self.sql_catalog.clone());
520 let result = executor
521 .execute(plan)
522 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
523 let ExecutionResult::Query(query) = result else {
524 return Err(Error::Sql(alopex_sql::SqlError::Execution {
525 message: "store-direct system function did not return rows".into(),
526 code: "ALOPEX-E022",
527 }));
528 };
529 let column_names: Vec<String> = query
530 .columns
531 .iter()
532 .map(|column| column.name.clone())
533 .collect();
534 let schema: Vec<alopex_sql::catalog::ColumnMetadata> = query
535 .columns
536 .iter()
537 .map(|column| {
538 alopex_sql::catalog::ColumnMetadata::new(
539 &column.name,
540 column.data_type.clone(),
541 )
542 })
543 .collect();
544 let rows: Vec<Row> = query
545 .rows
546 .into_iter()
547 .enumerate()
548 .map(|(index, values)| Row::new(index as u64, values))
549 .collect();
550 let iter = VecIterator::new(rows, schema.clone());
551 let streaming_rows = StreamingRows {
552 columns: query.columns,
553 iter: Box::new(iter),
554 projection: Projection::All(column_names),
555 schema,
556 };
557 let result = f(streaming_rows)?;
558 return Ok(StreamingQueryResult::QueryProcessed(result));
559 }
560
561 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
562 let (mut sql_txn, overlay_ref) = borrowed.split_parts();
563 let view = TxnCatalogView::new(&*catalog, overlay_ref);
564
565 let (iter, projection, schema) = build_streaming_pipeline(&mut sql_txn, &view, plan)
567 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
568
569 let columns = build_column_info(&projection, &schema)?;
571
572 let streaming_rows = StreamingRows {
574 columns,
575 iter,
576 projection,
577 schema,
578 };
579
580 let result = f(streaming_rows)?;
582
583 drop(catalog);
585 drop(borrowed);
586 txn.commit_self().map_err(Error::Core)?;
587
588 return Ok(StreamingQueryResult::QueryProcessed(result));
589 }
590
591 let exec_result = self.execute_sql(sql)?;
593 match exec_result {
594 alopex_sql::ExecutionResult::Success => Ok(StreamingQueryResult::Success),
595 alopex_sql::ExecutionResult::RowsAffected(n) => {
596 Ok(StreamingQueryResult::RowsAffected(n))
597 }
598 alopex_sql::ExecutionResult::Query(_qr) => {
599 Err(Error::Sql(alopex_sql::SqlError::Execution {
602 message: "Streaming not available for multi-statement or complex queries"
603 .into(),
604 code: "ALOPEX-E021",
605 }))
606 }
607 }
608 }
609
610 pub fn execute_sql_streaming(&self, sql: &str) -> Result<SqlStreamingResult> {
643 let stmts = parse_sql(sql)?;
644 if stmts.is_empty() {
645 return Ok(SqlStreamingResult::Success);
646 }
647
648 if stmts.len() == 1 && matches!(stmts[0].kind, StatementKind::Select(_)) {
650 let stmt = &stmts[0];
651 let mode = TxnMode::ReadOnly;
652
653 let mut txn = self.store.begin(mode).map_err(Error::Core)?;
654 let mut overlay = CatalogOverlay::new();
655 let mut borrowed =
656 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(&mut txn, mode, &mut overlay);
657
658 let plan = {
659 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
660 let (_, overlay) = borrowed.split_parts();
661 plan_stmt(&*catalog, &*overlay, stmt)?
662 };
663
664 let (mut sql_txn, _overlay) = borrowed.split_parts();
665
666 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
667 let view = TxnCatalogView::new(&*catalog, _overlay);
668 let iter = execute_query_streaming(&mut sql_txn, &view, plan)
669 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
670
671 drop(catalog);
672 drop(borrowed);
673
674 txn.commit_self().map_err(Error::Core)?;
675
676 return Ok(SqlStreamingResult::Query(iter));
677 }
678
679 let result = self.execute_sql(sql)?;
681 match result {
682 alopex_sql::ExecutionResult::Success => Ok(SqlStreamingResult::Success),
683 alopex_sql::ExecutionResult::RowsAffected(n) => Ok(SqlStreamingResult::RowsAffected(n)),
684 alopex_sql::ExecutionResult::Query(qr) => {
685 use alopex_sql::executor::query::iterator::VecIterator;
687 use alopex_sql::executor::Row;
688 use alopex_sql::planner::typed_expr::Projection;
689
690 let column_names: Vec<String> = qr.columns.iter().map(|c| c.name.clone()).collect();
691 let schema: Vec<alopex_sql::catalog::ColumnMetadata> = qr
692 .columns
693 .iter()
694 .map(|c| alopex_sql::catalog::ColumnMetadata::new(&c.name, c.data_type.clone()))
695 .collect();
696 let rows: Vec<Row> = qr
697 .rows
698 .into_iter()
699 .enumerate()
700 .map(|(i, values)| Row::new(i as u64, values))
701 .collect();
702 let iter = VecIterator::new(rows, schema.clone());
703 let query_iter =
704 QueryRowIterator::new(Box::new(iter), Projection::All(column_names), schema);
705 Ok(SqlStreamingResult::Query(query_iter))
706 }
707 }
708 }
709}
710
711impl<'a> Transaction<'a> {
712 pub fn execute_sql(&mut self, sql: &str) -> Result<SqlResult> {
729 let stmts = parse_sql(sql)?;
730 if stmts.is_empty() {
731 return Ok(alopex_sql::ExecutionResult::Success);
732 }
733
734 if stmts.iter().any(stmt_requires_write) {
735 let mut cache = self
736 .db
737 .hnsw_cache
738 .write()
739 .expect("hnsw cache lock poisoned");
740 cache.clear();
741 let mut vector_cache = self
742 .db
743 .vector_cache
744 .write()
745 .expect("vector cache lock poisoned");
746 *vector_cache = None;
747 }
748
749 let store = self.db.store.clone();
750 let sql_catalog = self.db.sql_catalog.clone();
751
752 let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
753 let mode = txn.mode();
754
755 let mut borrowed =
756 TxnBridge::<alopex_core::kv::AnyKV>::wrap_external(txn, mode, &mut self.overlay);
757 let mut executor: Executor<_, _> = Executor::new(store, sql_catalog.clone());
758
759 let mut last = alopex_sql::ExecutionResult::Success;
760 for (statement_index, stmt) in stmts.iter().enumerate() {
761 let plan = {
762 let catalog = sql_catalog.read().expect("catalog lock poisoned");
763 let (_, overlay) = borrowed.split_parts();
764 plan_stmt(&*catalog, &*overlay, stmt)?
765 };
766
767 {
768 let catalog = sql_catalog.read().expect("catalog lock poisoned");
769 let (_, overlay) = borrowed.split_parts();
770 let view = TxnCatalogView::new(&*catalog, &*overlay);
771 self.db.record_routing(&view, stmt, statement_index);
772 }
773
774 last = executor
775 .execute_in_txn(plan, &mut borrowed)
776 .map_err(|e| Error::Sql(alopex_sql::SqlError::from(e)))?;
777 }
778
779 if stmts.iter().any(stmt_changes_catalog) {
780 self.catalog_modified = true;
781 }
782 Ok(last)
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use alopex_core::kv::decode_range_change;
790 use alopex_core::kv::RangeChangePayload;
791
792 #[test]
793 fn auto_commit_stages_sql_row_and_index_changes_before_visibility() {
794 let db = Database::open_in_memory().unwrap();
795 db.execute_sql("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);")
796 .unwrap();
797 db.execute_sql("CREATE INDEX idx_users_name ON users (name);")
798 .unwrap();
799 db.execute_sql("INSERT INTO users (id, name) VALUES (1, 'alice');")
800 .unwrap();
801
802 let mut reader = db.store.begin(TxnMode::ReadOnly).unwrap();
803 let records = reader
804 .scan_prefix(b"\x00alopex/range-change/")
805 .unwrap()
806 .filter_map(|(_, value)| decode_range_change(&value).ok())
807 .collect::<Vec<_>>();
808 assert_eq!(records.len(), 1);
809 assert!(records[0]
810 .payload
811 .iter()
812 .any(|payload| matches!(payload, RangeChangePayload::UpsertRow { .. })));
813 assert!(records[0]
814 .payload
815 .iter()
816 .any(|payload| matches!(payload, RangeChangePayload::UpsertIndex { .. })));
817 }
818
819 #[test]
820 fn explicit_sql_transaction_stages_journal_before_commit() {
821 let db = Database::open_in_memory().unwrap();
822 db.execute_sql("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);")
823 .unwrap();
824 let mut transaction = db.begin(TxnMode::ReadWrite).unwrap();
825 transaction
826 .execute_sql("INSERT INTO users (id, name) VALUES (1, 'alice');")
827 .unwrap();
828 transaction.commit().unwrap();
829
830 let records = db
831 .snapshot()
832 .into_iter()
833 .filter_map(|(_, value)| decode_range_change(&value).ok())
834 .collect::<Vec<_>>();
835 assert_eq!(records.len(), 1);
836 assert!(records[0]
837 .payload
838 .iter()
839 .any(|payload| matches!(payload, RangeChangePayload::UpsertRow { .. })));
840 }
841
842 #[test]
843 fn embedded_read_at_returns_retention_error_before_a_sql_session_exists() {
844 let db = Database::open_in_memory().unwrap();
845 let point = ReadAtPoint::new(1, 2, 3, 4);
846
847 assert!(matches!(
848 db.begin_read_at_sql(point),
849 Err(Error::ReadAt(alopex_core::ReadAtError::Unavailable { .. }))
850 ));
851 }
852}