1use std::convert::Infallible;
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4
5use alopex_cluster::{
6 CatalogTableRef, CatalogTableSnapshot, PlacementCatalog, PlanId, QueryRouter,
7 QueryRoutingRequest, QueryTableReference, QueryTableReferenceAccess, QueryTableReferenceSource,
8 RequestId, RoutingDecisionKind, RoutingDiagnostics, TableLifecycleEffect, TableRef,
9};
10use alopex_core::kv::async_adapter::AsyncKVTransactionAdapter;
11use alopex_core::kv::{KVStore, KVTransaction};
12use alopex_core::storage::format::bincode_config;
13use alopex_core::types::TxnMode;
14use alopex_sql::catalog::persistent::{PersistedTableMeta, TableFqn, TABLES_PREFIX};
15use alopex_sql::catalog::TableMetadata;
16use alopex_sql::planner::{
17 LogicalPlan, PlannedStatement, TableReference, TableReferenceAccess, TableReferenceSource,
18};
19use alopex_sql::storage::async_storage::AsyncTxnBridge;
20use alopex_sql::storage::AsyncSqlTransaction;
21use alopex_sql::AlopexDialect;
22use axum::extract::{Extension, Path};
23use axum::response::{IntoResponse, Response};
24use axum::Json;
25use bincode::Options;
26use futures::StreamExt;
27use serde::{Deserialize, Serialize};
28use tokio::sync::mpsc;
29use tokio_stream::wrappers::ReceiverStream;
30
31use crate::error::{Result, ServerError};
32use crate::http::{error_response, json_response, RequestContext};
33use crate::ops::distributed_read::{
34 PreparedReadLease, ReadCancellation, ReadExecutionOutcome, ReadExecutionSummary,
35};
36use crate::ops::memory::MemoryControlPolicy;
37use crate::server::ServerState;
38use crate::session::{CatalogRollbackEffect, SessionId, TxnHandle};
39
40#[derive(Debug, Deserialize)]
41pub struct SqlRequest {
42 pub sql: String,
43 pub session_id: Option<String>,
44 #[serde(default)]
45 pub streaming: bool,
46}
47
48#[derive(Debug, Clone, Serialize)]
49pub struct ColumnInfoResponse {
50 pub name: String,
51 pub data_type: String,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct SqlResultResponse {
56 pub columns: Vec<ColumnInfoResponse>,
57 pub rows: Vec<Vec<alopex_sql::storage::SqlValue>>,
58 pub affected_rows: Option<u64>,
59}
60
61#[derive(Debug, Serialize)]
62pub struct SqlResponse {
63 #[serde(flatten)]
66 pub last_result: SqlResultResponse,
67 pub results: Vec<SqlResultResponse>,
69 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub routing_diagnostics: Vec<RoutingDiagnostics>,
71}
72
73#[derive(Debug, Deserialize)]
77pub struct DistributedReadRequest {
78 pub sql: String,
79 pub session_id: Option<String>,
80 #[serde(default)]
81 pub read_mode: Option<String>,
82}
83
84#[derive(Debug, Serialize)]
85#[serde(tag = "type", rename_all = "snake_case")]
86enum DistributedReadStreamItem {
87 Prepared {
88 execution_id: RequestId,
89 columns: Vec<ColumnInfoResponse>,
90 },
91 Row {
92 values: Vec<alopex_sql::storage::SqlValue>,
93 },
94 Terminal {
95 summary: ReadExecutionSummary,
96 },
97}
98
99#[derive(Debug, Serialize)]
100struct DistributedReadCancelResponse {
101 #[serde(flatten)]
102 cancellation: ReadCancellation,
103}
104
105#[derive(Debug, Serialize)]
106struct StreamItem {
107 row: Option<Vec<alopex_sql::storage::SqlValue>>,
108 error: Option<StreamError>,
109 done: bool,
110}
111
112#[derive(Debug, Serialize)]
113struct StreamError {
114 code: String,
115 message: String,
116 correlation_id: String,
117}
118
119type AsyncTxn = AsyncTxnBridge<'static, AsyncKVTransactionAdapter>;
120
121enum StreamSource {
122 Txn(AsyncTxn),
123 Handle(TxnHandle),
124}
125
126struct RoutingPlan {
127 planned: Vec<PlannedStatement>,
128 diagnostics: Vec<RoutingDiagnostics>,
129}
130
131#[derive(Clone)]
132struct TableLifecycleState {
133 table_ref: TableRef,
134 table_id: u32,
135 table: TableMetadata,
136}
137
138enum TableLifecycleCandidate {
139 Created {
140 table_name: String,
141 before: Option<TableLifecycleState>,
142 },
143 Dropped {
144 table_name: String,
145 before: Option<TableLifecycleState>,
146 },
147 CreateIndex {
148 index_name: String,
149 index_existed_before: bool,
150 },
151 DropIndex {
152 index_name: String,
153 before: Option<TableLifecycleState>,
154 },
155}
156
157pub async fn handle(
158 Extension(state): Extension<Arc<ServerState>>,
159 Extension(ctx): Extension<RequestContext>,
160 Json(request): Json<SqlRequest>,
161) -> Response {
162 if request.sql.trim().is_empty() {
163 return error_response(
164 ServerError::BadRequest("sql must not be empty".into()),
165 &ctx,
166 );
167 }
168
169 if request.streaming {
170 return stream_response(state, request, &ctx);
171 }
172
173 let result = execute_non_streaming(state.clone(), &request, &ctx).await;
174 match result {
175 Ok(response) => json_response(response, state.config.max_response_size, &ctx),
176 Err(err) => error_response(err, &ctx),
177 }
178}
179
180pub async fn begin_distributed_read(
185 Extension(state): Extension<Arc<ServerState>>,
186 Extension(ctx): Extension<RequestContext>,
187 Json(request): Json<DistributedReadRequest>,
188) -> Response {
189 if request.sql.trim().is_empty() {
190 return error_response(
191 ServerError::BadRequest("sql must not be empty".into()),
192 &ctx,
193 );
194 }
195 if let Err(error) =
196 crate::http::session::distributed_read_owner(&state, &ctx, request.session_id.as_deref())
197 .await
198 {
199 return error_response(error, &ctx);
200 }
201 let requested_mode = request.read_mode.as_deref().unwrap_or("inherit");
202 error_response(
203 ServerError::CapabilityUnavailable(format!(
204 "distributed read mode '{requested_mode}' has no registered fenced range-read coordinator; the SQL was not executed locally"
205 )),
206 &ctx,
207 )
208}
209
210pub async fn stream_distributed_read(
214 Extension(state): Extension<Arc<ServerState>>,
215 Extension(ctx): Extension<RequestContext>,
216 Path(execution_id): Path<String>,
217) -> Response {
218 if execution_id.trim().is_empty() {
219 return error_response(
220 ServerError::BadRequest("distributed read execution id must not be empty".into()),
221 &ctx,
222 );
223 }
224 let execution_id = RequestId::new(execution_id);
225 let lease = match state
226 .distributed_read_registry
227 .open_prepared(&execution_id, ctx.actor.as_deref())
228 {
229 Ok(lease) => lease,
230 Err(error) => return error_response(error, &ctx),
231 };
232 if let Err(error) = preflight_distributed_stream(&lease, state.config.max_response_size) {
233 return error_response(error, &ctx);
236 }
237
238 let prepared = DistributedReadStreamItem::Prepared {
239 execution_id: execution_id.clone(),
240 columns: lease
241 .columns()
242 .iter()
243 .map(|column| ColumnInfoResponse {
244 name: column.name.clone(),
245 data_type: type_to_string(&column.data_type),
246 })
247 .collect(),
248 };
249 let (sender, receiver) = mpsc::channel(32);
250 tokio::spawn(async move {
251 if sender.send(prepared).await.is_err() {
252 return;
253 }
254 stream_prepared_distributed_rows(lease, sender).await;
255 });
256
257 let stream = ReceiverStream::new(receiver).map(|item| {
258 let json = serde_json::to_string(&item).unwrap_or_else(|_| {
259 "{\"type\":\"terminal\",\"summary\":{\"outcome\":\"terminal_failure\",\"reason\":\"response serialization failed\"}}".to_string()
260 });
261 Ok::<axum::body::Bytes, Infallible>(axum::body::Bytes::from(json + "\n"))
262 });
263 let body = axum::body::Body::from_stream(stream);
264 axum::response::Response::builder()
265 .status(axum::http::StatusCode::OK)
266 .header(axum::http::header::CONTENT_TYPE, "application/jsonl")
267 .body(body)
268 .unwrap_or_else(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
269}
270
271pub async fn cancel_distributed_read(
274 Extension(state): Extension<Arc<ServerState>>,
275 Extension(ctx): Extension<RequestContext>,
276 Path(execution_id): Path<String>,
277) -> Response {
278 if execution_id.trim().is_empty() {
279 return error_response(
280 ServerError::BadRequest("distributed read execution id must not be empty".into()),
281 &ctx,
282 );
283 }
284 let execution_id = RequestId::new(execution_id);
285 match state
286 .distributed_read_registry
287 .cancel(&execution_id, ctx.actor.as_deref())
288 {
289 Ok(cancellation) => json_response(
290 DistributedReadCancelResponse { cancellation },
291 state.config.max_response_size,
292 &ctx,
293 ),
294 Err(error) => error_response(error, &ctx),
295 }
296}
297
298async fn stream_prepared_distributed_rows(
299 mut lease: PreparedReadLease,
300 sender: mpsc::Sender<DistributedReadStreamItem>,
301) {
302 loop {
303 let summary = lease.summary();
304 if summary.outcome != ReadExecutionOutcome::Success {
305 let _ = sender
306 .send(DistributedReadStreamItem::Terminal { summary })
307 .await;
308 lease.finish();
309 return;
310 }
311 let Some(values) = lease.next_row() else {
312 let summary = lease.summary();
313 let _ = sender
314 .send(DistributedReadStreamItem::Terminal { summary })
315 .await;
316 lease.finish();
317 return;
318 };
319 if sender
320 .send(DistributedReadStreamItem::Row { values })
321 .await
322 .is_err()
323 {
324 return;
327 }
328 }
329}
330
331fn preflight_distributed_stream(lease: &PreparedReadLease, max_size: usize) -> Result<()> {
332 let header = DistributedReadStreamItem::Prepared {
333 execution_id: lease.summary().execution_id,
334 columns: lease
335 .columns()
336 .iter()
337 .map(|column| ColumnInfoResponse {
338 name: column.name.clone(),
339 data_type: type_to_string(&column.data_type),
340 })
341 .collect(),
342 };
343 let mut total = serialized_jsonl_size(&header)?;
344 let mut preview = lease.preview_stream().ok_or_else(|| {
345 ServerError::Conflict("distributed read result has already been released".into())
346 })?;
347 while let Some(values) = preview.next_row() {
348 total = total.saturating_add(serialized_jsonl_size(&DistributedReadStreamItem::Row {
349 values,
350 })?);
351 if total > max_size {
352 return Err(ServerError::PayloadTooLarge(
353 "response size exceeds limit".into(),
354 ));
355 }
356 }
357 total = total.saturating_add(serialized_jsonl_size(
358 &DistributedReadStreamItem::Terminal {
359 summary: lease.summary(),
360 },
361 )?);
362 if total > max_size {
363 return Err(ServerError::PayloadTooLarge(
364 "response size exceeds limit".into(),
365 ));
366 }
367 Ok(())
368}
369
370fn serialized_jsonl_size<T: Serialize>(item: &T) -> Result<usize> {
371 serde_json::to_vec(item)
372 .map(|encoded| encoded.len().saturating_add(1))
373 .map_err(|error| ServerError::Internal(error.to_string()))
374}
375
376pub(crate) async fn execute_non_streaming(
382 state: Arc<ServerState>,
383 request: &SqlRequest,
384 ctx: &RequestContext,
385) -> Result<SqlResponse> {
386 let start = Instant::now();
387 let sql = request.sql.as_str();
388 let is_ddl = is_ddl(sql);
389 if is_write_sql(sql) {
390 state.lifecycle_state.check_write_allowed()?;
391 }
392
393 let exec_result: Result<(
394 Vec<alopex_sql::executor::ExecutionResult>,
395 Vec<RoutingDiagnostics>,
396 )> = async {
397 if let Some(session_id) = &request.session_id {
398 let session_id = session_id
399 .parse::<SessionId>()
400 .map_err(|_| ServerError::BadRequest("invalid session_id".into()))?;
401 execute_session_statements_with_routing(
402 &state,
403 &session_id,
404 sql,
405 &ctx.correlation_id,
406 state.config.query_timeout,
407 )
408 .await
409 } else {
410 execute_non_session_statements_with_routing(
411 &state,
412 sql,
413 &ctx.correlation_id,
414 state.config.query_timeout,
415 )
416 .await
417 }
418 }
419 .await;
420 let exec_result = match exec_result {
421 Ok(result) => result,
422 Err(err) => {
423 state.metrics.record_query(start.elapsed(), false);
424 return Err(err);
425 }
426 };
427
428 if state.config.audit_log_enabled && is_ddl {
429 state
430 .audit
431 .log_ddl(sql, ctx.actor.as_deref(), &ctx.correlation_id);
432 }
433
434 if is_ddl && request.session_id.is_none() {
435 sync_catalog_to_store(&state)?;
436 }
437
438 state.metrics.record_query(start.elapsed(), true);
439
440 Ok(map_execution_results(exec_result.0, exec_result.1))
441}
442
443pub(crate) async fn execute_session_statements_with_routing(
444 state: &ServerState,
445 session_id: &SessionId,
446 sql: &str,
447 correlation_id: &str,
448 timeout: Duration,
449) -> Result<(
450 Vec<alopex_sql::executor::ExecutionResult>,
451 Vec<RoutingDiagnostics>,
452)> {
453 let handle = state.session_manager.get_transaction(session_id).await?;
454 let routing_plan = route_session_sql(state, &handle, sql, correlation_id).await?;
455 if let Some(diagnostic) = future_distributed_diagnostic(&routing_plan.diagnostics) {
456 return Err(future_distributed_error(diagnostic));
457 }
458 let lifecycle_candidates = table_lifecycle_candidates(state, &routing_plan.planned)?;
459 let result = tokio::time::timeout(timeout, handle.execute_multi(sql))
460 .await
461 .map_err(|_| ServerError::Timeout("query timeout".into()))?
462 .map_err(|err| ServerError::Sql(err.into()))?;
463 let (lifecycle_effects, catalog_rollback_effects) =
464 statement_effects_after_execution(state, lifecycle_candidates)?;
465 handle
466 .buffer_table_lifecycle_effects(lifecycle_effects)
467 .await;
468 handle
469 .buffer_catalog_rollback_effects(catalog_rollback_effects)
470 .await;
471 Ok((result, routing_plan.diagnostics))
472}
473
474pub(crate) async fn execute_session_statement_with_routing(
475 state: &ServerState,
476 session_id: &SessionId,
477 sql: &str,
478 correlation_id: &str,
479 timeout: Duration,
480) -> Result<(
481 alopex_sql::executor::ExecutionResult,
482 Vec<RoutingDiagnostics>,
483)> {
484 let (mut results, diagnostics) =
485 execute_session_statements_with_routing(state, session_id, sql, correlation_id, timeout)
486 .await?;
487 let result = results
488 .pop()
489 .ok_or_else(|| ServerError::BadRequest("sql must not be empty".into()))?;
490 Ok((result, diagnostics))
491}
492
493pub(crate) async fn execute_non_session_statements_with_routing(
494 state: &ServerState,
495 sql: &str,
496 correlation_id: &str,
497 timeout: Duration,
498) -> Result<(
499 Vec<alopex_sql::executor::ExecutionResult>,
500 Vec<RoutingDiagnostics>,
501)> {
502 let mut txn = state.begin_sql_txn().await?;
503 let routing_plan = match route_non_session_sql(state, &txn, sql, correlation_id).await {
504 Ok(plan) => plan,
505 Err(err) => {
506 let _ = txn.async_rollback().await;
507 return Err(err);
508 }
509 };
510 if let Some(diagnostic) = future_distributed_diagnostic(&routing_plan.diagnostics) {
511 let _ = txn.async_rollback().await;
512 return Err(future_distributed_error(diagnostic));
513 }
514 let lifecycle_candidates = table_lifecycle_candidates(state, &routing_plan.planned)?;
515 let fut = match tokio::time::timeout(timeout, txn.async_execute_multi(sql)).await {
516 Ok(result) => result,
517 Err(_) => {
518 let _ = txn.async_rollback().await;
519 return Err(ServerError::Timeout("query timeout".into()));
520 }
521 };
522 match fut {
523 Ok(result) => {
524 let (lifecycle_effects, _) =
525 statement_effects_after_execution(state, lifecycle_candidates)?;
526 txn.async_commit()
527 .await
528 .map_err(|err| ServerError::Sql(err.into()))?;
529 state.apply_table_lifecycle_effects(lifecycle_effects)?;
530 Ok((result, routing_plan.diagnostics))
531 }
532 Err(err) => {
533 let _ = txn.async_rollback().await;
534 Err(ServerError::Sql(err.into()))
535 }
536 }
537}
538
539pub(crate) async fn execute_non_session_statement_with_routing(
540 state: &ServerState,
541 sql: &str,
542 correlation_id: &str,
543 timeout: Duration,
544) -> Result<(
545 alopex_sql::executor::ExecutionResult,
546 Vec<RoutingDiagnostics>,
547)> {
548 let (mut results, diagnostics) =
549 execute_non_session_statements_with_routing(state, sql, correlation_id, timeout).await?;
550 let result = results
551 .pop()
552 .ok_or_else(|| ServerError::BadRequest("sql must not be empty".into()))?;
553 Ok((result, diagnostics))
554}
555
556pub(crate) async fn route_session_statement_for_execution(
557 state: &ServerState,
558 handle: &TxnHandle,
559 sql: &str,
560 correlation_id: &str,
561) -> Result<Vec<RoutingDiagnostics>> {
562 let routing_plan = route_session_sql(state, handle, sql, correlation_id).await?;
563 if let Some(diagnostic) = future_distributed_diagnostic(&routing_plan.diagnostics) {
564 return Err(future_distributed_error(diagnostic));
565 }
566 Ok(routing_plan.diagnostics)
567}
568
569async fn route_non_session_sql(
570 state: &ServerState,
571 txn: &AsyncTxn,
572 sql: &str,
573 correlation_id: &str,
574) -> Result<RoutingPlan> {
575 let planned = txn
576 .async_plan_for_routing(sql)
577 .await
578 .map_err(|err| ServerError::Sql(err.into()))?;
579 route_planned_sql(state, planned, correlation_id)
580}
581
582async fn route_session_sql(
583 state: &ServerState,
584 handle: &TxnHandle,
585 sql: &str,
586 correlation_id: &str,
587) -> Result<RoutingPlan> {
588 let planned = handle
589 .plan_for_routing(sql)
590 .await
591 .map_err(|err| ServerError::Sql(err.into()))?;
592 route_planned_sql(state, planned, correlation_id)
593}
594
595fn route_planned_sql(
596 state: &ServerState,
597 planned: Vec<PlannedStatement>,
598 correlation_id: &str,
599) -> Result<RoutingPlan> {
600 let cluster_snapshot = state.cluster_status_snapshot()?;
601 let placement_catalog = PlacementCatalog::from_view(cluster_snapshot.placement);
602 let membership = cluster_snapshot.membership;
603 let catalog_snapshot = catalog_table_snapshot(state, cluster_snapshot.identity.update_epoch)?;
604 let router = QueryRouter::new(&placement_catalog, &membership);
605
606 let mut diagnostics = Vec::with_capacity(planned.len());
607 for (index, statement) in planned.iter().enumerate() {
608 let plan_id = PlanId::new(format!("{correlation_id}:{index}"));
609 let request = query_routing_request(plan_id, statement, &catalog_snapshot);
610 diagnostics.push(router.route(request));
611 }
612 Ok(RoutingPlan {
613 planned,
614 diagnostics,
615 })
616}
617
618fn future_distributed_diagnostic(
619 diagnostics: &[RoutingDiagnostics],
620) -> Option<&RoutingDiagnostics> {
621 diagnostics.iter().find(|diagnostic| {
622 diagnostic.decision == RoutingDecisionKind::FutureDistributedExecutionRequired
623 })
624}
625
626fn future_distributed_error(diagnostic: &RoutingDiagnostics) -> ServerError {
627 ServerError::FutureDistributedExecutionRequired(format!(
628 "routing decision {:?} for plan {}: {:?}",
629 diagnostic.decision,
630 diagnostic.plan_id.as_str(),
631 diagnostic.reason
632 ))
633}
634
635fn query_routing_request(
636 plan_id: PlanId,
637 statement: &PlannedStatement,
638 catalog_snapshot: &CatalogTableSnapshot,
639) -> QueryRoutingRequest {
640 let table_references = statement
641 .table_references()
642 .iter()
643 .map(|reference| query_table_reference(reference, catalog_snapshot))
644 .collect();
645
646 QueryRoutingRequest::new(plan_id, catalog_snapshot.clone(), table_references)
647}
648
649fn query_table_reference(
650 reference: &TableReference,
651 catalog_snapshot: &CatalogTableSnapshot,
652) -> QueryTableReference {
653 QueryTableReference::new(
654 table_ref_for_reference(&reference.table_name, catalog_snapshot),
655 query_table_reference_access(reference.access),
656 query_table_reference_source(reference.source),
657 )
658}
659
660fn query_table_reference_access(access: TableReferenceAccess) -> QueryTableReferenceAccess {
661 match access {
662 TableReferenceAccess::Read => QueryTableReferenceAccess::Read,
663 TableReferenceAccess::Write => QueryTableReferenceAccess::Write,
664 TableReferenceAccess::Create => QueryTableReferenceAccess::Create,
665 TableReferenceAccess::Drop => QueryTableReferenceAccess::Drop,
666 TableReferenceAccess::Metadata => QueryTableReferenceAccess::Metadata,
667 }
668}
669
670fn query_table_reference_source(source: TableReferenceSource) -> QueryTableReferenceSource {
671 match source {
672 TableReferenceSource::TopLevelPlanTableName => {
673 QueryTableReferenceSource::TopLevelPlanTableName
674 }
675 TableReferenceSource::LogicalPlanScan => QueryTableReferenceSource::LogicalPlanScan,
676 TableReferenceSource::LogicalPlanMutationTarget => {
677 QueryTableReferenceSource::LogicalPlanMutationTarget
678 }
679 TableReferenceSource::LogicalPlanDdlTarget => {
680 QueryTableReferenceSource::LogicalPlanDdlTarget
681 }
682 TableReferenceSource::LogicalPlanIndexTarget => {
683 QueryTableReferenceSource::LogicalPlanIndexTarget
684 }
685 TableReferenceSource::TypedExprSubquery => QueryTableReferenceSource::TypedExprSubquery,
686 }
687}
688
689fn catalog_table_snapshot(state: &ServerState, update_epoch: u64) -> Result<CatalogTableSnapshot> {
690 let guard = state
691 .catalog
692 .read()
693 .map_err(|_| ServerError::Internal("catalog lock poisoned".into()))?;
694 let tables = guard
695 .list_tables()
696 .iter()
697 .map(|table| CatalogTableRef::new(table_fqn_string(table), table.table_id))
698 .collect();
699 Ok(CatalogTableSnapshot::from_tables(update_epoch, tables))
700}
701
702fn table_ref_for_reference(table_name: &str, snapshot: &CatalogTableSnapshot) -> TableRef {
703 snapshot
704 .tables
705 .iter()
706 .find(|table| {
707 table.table_ref.as_str() == table_name
708 || table.table_ref.as_str().rsplit('.').next() == Some(table_name)
709 })
710 .map(|table| table.table_ref.clone())
711 .unwrap_or_else(|| TableRef::new(default_table_ref(table_name)))
712}
713
714fn table_fqn_string(table: &TableMetadata) -> String {
715 let fqn = TableFqn::from(table);
716 format!("{}.{}.{}", fqn.catalog, fqn.namespace, fqn.table)
717}
718
719fn table_lifecycle_candidates(
720 state: &ServerState,
721 planned: &[PlannedStatement],
722) -> Result<Vec<TableLifecycleCandidate>> {
723 let mut candidates = Vec::new();
724 for statement in planned {
725 match &statement.plan {
726 LogicalPlan::CreateTable { table, .. } => {
727 candidates.push(TableLifecycleCandidate::Created {
728 table_name: table.name.clone(),
729 before: table_lifecycle_state(state, &table.name)?,
730 });
731 }
732 LogicalPlan::DropTable { name, .. } => {
733 candidates.push(TableLifecycleCandidate::Dropped {
734 table_name: name.clone(),
735 before: table_lifecycle_state(state, name)?,
736 });
737 }
738 LogicalPlan::CreateIndex { index, .. } => {
739 candidates.push(TableLifecycleCandidate::CreateIndex {
740 index_name: index.name.clone(),
741 index_existed_before: index_exists(state, &index.name)?,
742 });
743 }
744 LogicalPlan::DropIndex { name, .. } => {
745 candidates.push(TableLifecycleCandidate::DropIndex {
746 index_name: name.clone(),
747 before: index_table_lifecycle_state(state, name)?,
748 });
749 }
750 _ => {}
751 }
752 }
753 Ok(candidates)
754}
755
756fn statement_effects_after_execution(
757 state: &ServerState,
758 candidates: Vec<TableLifecycleCandidate>,
759) -> Result<(Vec<TableLifecycleEffect>, Vec<CatalogRollbackEffect>)> {
760 let mut lifecycle_effects = Vec::new();
761 let mut catalog_rollback_effects = Vec::new();
762 for candidate in candidates {
763 match candidate {
764 TableLifecycleCandidate::Created { table_name, before } => {
765 let after = table_lifecycle_state(state, &table_name)?;
766 if let Some(after) = after {
767 let changed = match before.as_ref() {
768 Some(before) => before.table_id != after.table_id,
769 None => true,
770 };
771 if changed {
772 lifecycle_effects.push(TableLifecycleEffect::Created {
773 table_ref: after.table_ref,
774 table_id: after.table_id,
775 });
776 catalog_rollback_effects.push(CatalogRollbackEffect::DropTable {
777 table_name: after.table.name,
778 });
779 }
780 }
781 }
782 TableLifecycleCandidate::Dropped { table_name, before } => {
783 let after = table_lifecycle_state(state, &table_name)?;
784 if let Some(before) = before {
785 let changed = match after.as_ref() {
786 Some(after) => after.table_id != before.table_id,
787 None => true,
788 };
789 if changed {
790 lifecycle_effects.push(TableLifecycleEffect::Dropped {
791 table_ref: before.table_ref.clone(),
792 table_id: before.table_id,
793 });
794 catalog_rollback_effects.push(CatalogRollbackEffect::CreateTable {
795 table: Box::new(before.table),
796 });
797 }
798 }
799 }
800 TableLifecycleCandidate::CreateIndex {
801 index_name,
802 index_existed_before,
803 } => {
804 if !index_existed_before {
805 if let Some(after) = index_table_lifecycle_state(state, &index_name)? {
806 lifecycle_effects.push(TableLifecycleEffect::SchemaChanged {
807 table_ref: after.table_ref,
808 table_id: after.table_id,
809 });
810 }
811 }
812 }
813 TableLifecycleCandidate::DropIndex { index_name, before } => {
814 if let Some(before) = before {
815 if !index_exists(state, &index_name)? {
816 lifecycle_effects.push(TableLifecycleEffect::SchemaChanged {
817 table_ref: before.table_ref,
818 table_id: before.table_id,
819 });
820 }
821 }
822 }
823 }
824 }
825 Ok((lifecycle_effects, catalog_rollback_effects))
826}
827
828fn table_lifecycle_state(
829 state: &ServerState,
830 table_name: &str,
831) -> Result<Option<TableLifecycleState>> {
832 let guard = state
833 .catalog
834 .read()
835 .map_err(|_| ServerError::Internal("catalog lock poisoned".into()))?;
836 Ok(guard
837 .get_table(table_name)
838 .map(|table| TableLifecycleState {
839 table_ref: TableRef::new(table_fqn_string(table)),
840 table_id: table.table_id,
841 table: table.clone(),
842 }))
843}
844
845fn index_table_lifecycle_state(
846 state: &ServerState,
847 index_name: &str,
848) -> Result<Option<TableLifecycleState>> {
849 let guard = state
850 .catalog
851 .read()
852 .map_err(|_| ServerError::Internal("catalog lock poisoned".into()))?;
853 let Some(index) = guard.get_index(index_name) else {
854 return Ok(None);
855 };
856 Ok(guard
857 .get_table(&index.table)
858 .map(|table| TableLifecycleState {
859 table_ref: TableRef::new(table_fqn_string(table)),
860 table_id: table.table_id,
861 table: table.clone(),
862 }))
863}
864
865fn index_exists(state: &ServerState, index_name: &str) -> Result<bool> {
866 let guard = state
867 .catalog
868 .read()
869 .map_err(|_| ServerError::Internal("catalog lock poisoned".into()))?;
870 Ok(guard.get_index(index_name).is_some())
871}
872
873fn default_table_ref(table_name: &str) -> String {
874 if table_name.matches('.').count() >= 2 {
875 table_name.to_string()
876 } else {
877 format!("default.default.{table_name}")
878 }
879}
880
881pub(crate) fn sync_catalog_to_store(state: &ServerState) -> Result<()> {
882 let guard = state
883 .catalog
884 .read()
885 .map_err(|_| ServerError::Internal("catalog lock poisoned".into()))?;
886 let tables = guard.list_tables();
887 let mut txn = state.store.begin(TxnMode::ReadWrite)?;
888 delete_prefix(&mut txn, TABLES_PREFIX)?;
889 for table in tables {
890 let persisted = PersistedTableMeta::from(&table);
891 let value = bincode_config()
892 .serialize(&persisted)
893 .map_err(|err| ServerError::Internal(err.to_string()))?;
894 txn.put(
895 table_key(&table.catalog_name, &table.namespace_name, &table.name),
896 value,
897 )?;
898 }
899 txn.commit_self()?;
900 Ok(())
901}
902
903fn delete_prefix<'a, T: KVTransaction<'a>>(txn: &mut T, prefix: &[u8]) -> Result<()> {
904 let mut keys = Vec::new();
905 for (key, _) in txn.scan_prefix(prefix)? {
906 keys.push(key);
907 }
908 for key in keys {
909 txn.delete(key)?;
910 }
911 Ok(())
912}
913
914fn table_key(catalog_name: &str, namespace_name: &str, table_name: &str) -> Vec<u8> {
915 let mut key = TABLES_PREFIX.to_vec();
916 key.extend_from_slice(catalog_name.as_bytes());
917 key.push(b'/');
918 key.extend_from_slice(namespace_name.as_bytes());
919 key.push(b'/');
920 key.extend_from_slice(table_name.as_bytes());
921 key
922}
923
924fn stream_response(state: Arc<ServerState>, request: SqlRequest, ctx: &RequestContext) -> Response {
925 if is_write_sql(&request.sql) {
926 if let Err(err) = state.lifecycle_state.check_write_allowed() {
927 return error_response(err, ctx);
928 }
929 }
930 let (sender, receiver) = mpsc::channel(32);
931 let sql = request.sql.clone();
932 let correlation_id = ctx.correlation_id.clone();
933 let max_response_size = state.config.max_response_size;
934 let timeout = state.config.query_timeout;
935 let memory_policy = MemoryControlPolicy::from_env();
936 let metrics = state.metrics.clone();
937 let mut audit = None;
938 if state.config.audit_log_enabled && is_ddl(&sql) {
939 audit = Some(state.audit.clone());
940 }
941
942 let session_id = request.session_id.clone();
943 let state_clone = state.clone();
944 let memory_policy = memory_policy.clone();
945 tokio::spawn(async move {
946 let start = Instant::now();
947 let mut bytes_sent = 0usize;
948 let mut success = true;
949 let mut source = match session_id {
950 Some(id) => {
951 let parsed = match id.parse::<SessionId>() {
952 Ok(id) => id,
953 Err(_) => {
954 let _ = sender
955 .send(stream_item_error(
956 ServerError::BadRequest("invalid session_id".into()),
957 &correlation_id,
958 ))
959 .await;
960 return;
961 }
962 };
963 match state_clone.session_manager.get_transaction(&parsed).await {
964 Ok(handle) => {
965 match route_session_statement_for_execution(
966 &state_clone,
967 &handle,
968 &sql,
969 &correlation_id,
970 )
971 .await
972 {
973 Ok(_) => {}
974 Err(err) => {
975 let _ = sender.send(stream_item_error(err, &correlation_id)).await;
976 return;
977 }
978 }
979 StreamSource::Handle(handle)
980 }
981 Err(err) => {
982 let _ = sender.send(stream_item_error(err, &correlation_id)).await;
983 return;
984 }
985 }
986 }
987 None => match state_clone.begin_sql_txn().await {
988 Ok(txn) => StreamSource::Txn(txn),
989 Err(err) => {
990 let _ = sender.send(stream_item_error(err, &correlation_id)).await;
991 return;
992 }
993 },
994 };
995
996 let mut stream = match &mut source {
997 StreamSource::Handle(handle) => handle.query(&sql),
998 StreamSource::Txn(txn) => txn.async_query(&sql),
999 };
1000 let deadline = start + timeout;
1001 loop {
1002 let remaining = deadline.saturating_duration_since(Instant::now());
1003 if remaining.is_zero() {
1004 let _ = sender
1005 .send(stream_item_error(
1006 ServerError::Timeout("query timeout".into()),
1007 &correlation_id,
1008 ))
1009 .await;
1010 success = false;
1011 break;
1012 }
1013
1014 tokio::select! {
1015 _ = sender.closed() => {
1016 success = false;
1017 break;
1018 }
1019 item = tokio::time::timeout(remaining, stream.next()) => {
1020 let next = match item {
1021 Ok(value) => value,
1022 Err(_) => {
1023 let _ = sender
1024 .send(stream_item_error(
1025 ServerError::Timeout("query timeout".into()),
1026 &correlation_id,
1027 ))
1028 .await;
1029 success = false;
1030 break;
1031 }
1032 };
1033
1034 match next {
1035 Some(Ok(row)) => {
1036 let item = StreamItem {
1037 row: Some(row.values),
1038 error: None,
1039 done: false,
1040 };
1041 match serde_json::to_vec(&item) {
1042 Ok(bytes) => {
1043 bytes_sent += bytes.len();
1044 if let Err(err) =
1045 memory_policy.enforce_output_bytes(bytes_sent as u64)
1046 {
1047 let _ = sender
1048 .send(stream_item_error(err, &correlation_id))
1049 .await;
1050 success = false;
1051 break;
1052 }
1053 if bytes_sent > max_response_size {
1054 let _ = sender
1055 .send(stream_item_error(
1056 ServerError::PayloadTooLarge(
1057 "response size exceeds limit".into(),
1058 ),
1059 &correlation_id,
1060 ))
1061 .await;
1062 success = false;
1063 break;
1064 }
1065 }
1066 Err(err) => {
1067 let _ = sender
1068 .send(stream_item_error(
1069 ServerError::Internal(err.to_string()),
1070 &correlation_id,
1071 ))
1072 .await;
1073 success = false;
1074 break;
1075 }
1076 }
1077 match sender.try_send(item) {
1078 Ok(()) => {}
1079 Err(mpsc::error::TrySendError::Full(item)) => {
1080 metrics.record_backpressure();
1081 if sender.send(item).await.is_err() {
1082 success = false;
1083 break;
1084 }
1085 }
1086 Err(mpsc::error::TrySendError::Closed(_)) => {
1087 success = false;
1088 break;
1089 }
1090 }
1091 }
1092 Some(Err(err)) => {
1093 let _ = sender
1094 .send(stream_item_error(
1095 ServerError::Sql(err.into()),
1096 &correlation_id,
1097 ))
1098 .await;
1099 success = false;
1100 break;
1101 }
1102 None => break,
1103 }
1104 }
1105 }
1106 }
1107
1108 drop(stream);
1109 if let StreamSource::Txn(txn) = source {
1110 let _ = txn.async_rollback().await;
1111 }
1112 if let Some(logger) = audit {
1113 logger.log_ddl(&sql, None, &correlation_id);
1114 }
1115 metrics.record_query(start.elapsed(), success);
1116 let _ = sender
1117 .send(StreamItem {
1118 row: None,
1119 error: None,
1120 done: true,
1121 })
1122 .await;
1123 });
1124
1125 let stream = ReceiverStream::new(receiver).map(|item| {
1126 let json = serde_json::to_string(&item).unwrap_or_else(|_| "{}".to_string());
1127 Ok::<axum::body::Bytes, Infallible>(axum::body::Bytes::from(json + "\n"))
1128 });
1129
1130 let body = axum::body::Body::from_stream(stream);
1131 axum::response::Response::builder()
1132 .status(axum::http::StatusCode::OK)
1133 .header(axum::http::header::CONTENT_TYPE, "application/jsonl")
1134 .body(body)
1135 .unwrap_or_else(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
1136}
1137
1138fn stream_item_error(err: ServerError, correlation_id: &str) -> StreamItem {
1139 StreamItem {
1140 row: None,
1141 error: Some(StreamError {
1142 code: err.error_code(),
1143 message: err.to_string(),
1144 correlation_id: correlation_id.to_string(),
1145 }),
1146 done: false,
1147 }
1148}
1149
1150fn map_execution_results(
1151 exec_results: Vec<alopex_sql::executor::ExecutionResult>,
1152 routing_diagnostics: Vec<RoutingDiagnostics>,
1153) -> SqlResponse {
1154 let results: Vec<SqlResultResponse> =
1155 exec_results.into_iter().map(map_execution_result).collect();
1156 let last_result = results
1157 .last()
1158 .cloned()
1159 .unwrap_or_else(|| SqlResultResponse {
1160 columns: Vec::new(),
1161 rows: Vec::new(),
1162 affected_rows: None,
1163 });
1164 SqlResponse {
1165 last_result,
1166 results,
1167 routing_diagnostics,
1168 }
1169}
1170
1171fn map_execution_result(exec_result: alopex_sql::executor::ExecutionResult) -> SqlResultResponse {
1172 match exec_result {
1173 alopex_sql::executor::ExecutionResult::Query(query) => SqlResultResponse {
1174 columns: query
1175 .columns
1176 .into_iter()
1177 .map(|col| ColumnInfoResponse {
1178 name: col.name,
1179 data_type: type_to_string(&col.data_type),
1180 })
1181 .collect(),
1182 rows: query.rows,
1183 affected_rows: None,
1184 },
1185 alopex_sql::executor::ExecutionResult::RowsAffected(rows) => SqlResultResponse {
1186 columns: Vec::new(),
1187 rows: Vec::new(),
1188 affected_rows: Some(rows),
1189 },
1190 alopex_sql::executor::ExecutionResult::Success => SqlResultResponse {
1191 columns: Vec::new(),
1192 rows: Vec::new(),
1193 affected_rows: None,
1194 },
1195 }
1196}
1197
1198fn type_to_string(data_type: &alopex_sql::planner::ResolvedType) -> String {
1199 match data_type {
1200 alopex_sql::planner::ResolvedType::Integer => "INTEGER".to_string(),
1201 alopex_sql::planner::ResolvedType::BigInt => "BIGINT".to_string(),
1202 alopex_sql::planner::ResolvedType::Float => "FLOAT".to_string(),
1203 alopex_sql::planner::ResolvedType::Double => "DOUBLE".to_string(),
1204 alopex_sql::planner::ResolvedType::Text => "TEXT".to_string(),
1205 alopex_sql::planner::ResolvedType::Blob => "BLOB".to_string(),
1206 alopex_sql::planner::ResolvedType::Boolean => "BOOLEAN".to_string(),
1207 alopex_sql::planner::ResolvedType::Timestamp => "TIMESTAMP".to_string(),
1208 alopex_sql::planner::ResolvedType::Vector { dimension, metric } => {
1209 format!("VECTOR({dimension}, {metric:?})")
1210 }
1211 alopex_sql::planner::ResolvedType::Null => "NULL".to_string(),
1212 }
1213}
1214
1215fn is_ddl(sql: &str) -> bool {
1216 let Ok(statements) = alopex_sql::parser::Parser::parse_sql(&AlopexDialect, sql) else {
1217 return false;
1218 };
1219 statements.iter().any(|stmt| match &stmt.kind {
1220 alopex_sql::ast::StatementKind::CreateTable(_)
1221 | alopex_sql::ast::StatementKind::DropTable(_)
1222 | alopex_sql::ast::StatementKind::CreateIndex(_)
1223 | alopex_sql::ast::StatementKind::DropIndex(_) => true,
1224 alopex_sql::ast::StatementKind::Select(_)
1225 | alopex_sql::ast::StatementKind::Insert(_)
1226 | alopex_sql::ast::StatementKind::Update(_)
1227 | alopex_sql::ast::StatementKind::Delete(_)
1228 | alopex_sql::ast::StatementKind::Pragma { .. } => false,
1229 })
1230}
1231
1232fn is_write_sql(sql: &str) -> bool {
1233 let Ok(statements) = alopex_sql::parser::Parser::parse_sql(&AlopexDialect, sql) else {
1234 return false;
1235 };
1236 statements
1237 .iter()
1238 .any(|stmt| !matches!(stmt.kind, alopex_sql::ast::StatementKind::Select(_)))
1239}