Skip to main content

alopex_server/http/
sql.rs

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    /// Legacy single-result fields. They contain the last statement result so
64    /// existing clients can migrate to `results` without an abrupt break.
65    #[serde(flatten)]
66    pub last_result: SqlResultResponse,
67    /// Results in input statement order.
68    pub results: Vec<SqlResultResponse>,
69    #[serde(skip_serializing_if = "Vec::is_empty")]
70    pub routing_diagnostics: Vec<RoutingDiagnostics>,
71}
72
73/// Explicit remote-read entry point. The coordinator registration is kept
74/// separate from legacy `/sql`, so an unsupported distributed request can
75/// never silently execute as a local SQL query.
76#[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
180/// Reject an explicit distributed-read request when this server has no
181/// registered fenced range-read coordinator. This route intentionally does
182/// not call the legacy local SQL route: capability absence is observable and
183/// classified before a query can produce local rows.
184pub 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
210/// Stream an already prepared distributed result. A coordinator registers the
211/// execution before dispatch and calls `publish_prepared` only after every
212/// range has supplied its P2.10 cleanup acknowledgement through P2.11.
213pub 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        // Dropping an unconsumed lease dispatches the same cancellation and
234        // cleanup callbacks as an explicit HTTP cancellation request.
235        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
271/// Deliver an idempotent cancellation to the coordinator and every registered
272/// range peer. Ownership is verified against the authenticated HTTP profile.
273pub 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            // The lease Drop implementation invokes the cancellation registry
325            // and releases any still-active peer work exactly once.
326            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
376/// SQL を非ストリーミングで実行する共有経路。
377///
378/// HTTP `/sql` と gRPC `ExecuteSql` (issue #25) の両方から呼ばれる。
379/// SQL の実行セマンティクス (タイムアウト・コミット/ロールバック・
380/// 監査ログ・カタログ同期・メトリクス) はこの関数に集約する。
381pub(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}