Skip to main content

linkrs_server/graph_service/
streaming.rs

1use std::collections::HashMap;
2use std::sync::atomic::Ordering;
3use std::sync::Arc;
4
5use crate::query::executor::streaming::StreamingQueryResult;
6use crate::session::ClientSession;
7use crate::storage::{
8    StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
9};
10
11use super::error::GraphServiceError;
12use super::GraphService;
13
14impl<
15        S: StorageClient
16            + StorageSchemaContextOps
17            + StorageSyncContextOps
18            + StorageOperationContextOps
19            + Clone
20            + 'static,
21    > GraphService<S>
22{
23    /// Execute a query and return a [`StreamingQueryResult`] for chunk-at-a-time consumption.
24    pub async fn execute_stream(
25        &self,
26        session_id: i64,
27        stmt: &str,
28    ) -> Result<StreamingQueryResult, GraphServiceError> {
29        let session = self
30            .session_manager
31            .find_session(session_id)
32            .ok_or_else(|| GraphServiceError::new(format!("Invalid session ID: {}", session_id)))?;
33        let snapshot = session.variables_snapshot();
34        self.build_streaming_result(&session, stmt, None, Some(snapshot))
35            .await
36    }
37
38    /// Execute a query with caller-supplied bindings and return a streaming handle.
39    pub async fn execute_stream_with_params(
40        &self,
41        session_id: i64,
42        stmt: &str,
43        parameters: Option<HashMap<String, linkrs_core::Value>>,
44        session_variables: Option<HashMap<String, linkrs_core::Value>>,
45    ) -> Result<StreamingQueryResult, GraphServiceError> {
46        let session = self
47            .session_manager
48            .find_session(session_id)
49            .ok_or_else(|| GraphServiceError::new(format!("Invalid session ID: {}", session_id)))?;
50        self.build_streaming_result(&session, stmt, parameters, session_variables)
51            .await
52    }
53
54    /// Shared streaming setup behind [`execute_stream`](Self::execute_stream) and
55    /// [`execute_stream_with_params`](Self::execute_stream_with_params).
56    pub(crate) async fn build_streaming_result(
57        &self,
58        session: &Arc<ClientSession>,
59        stmt: &str,
60        parameters: Option<HashMap<String, linkrs_core::Value>>,
61        session_variables: Option<HashMap<String, linkrs_core::Value>>,
62    ) -> Result<StreamingQueryResult, GraphServiceError> {
63        let session_id = session.id();
64        if Self::is_config_statement(stmt) {
65            return Err(GraphServiceError::new(
66                "UPDATE CONFIGS and SHOW CONFIGS are not supported on streaming or export endpoints; use the unary query endpoint instead",
67            ));
68        }
69        match Self::parse_command(stmt) {
70            Err(parse_error) => return Err(parse_error),
71            Ok(Some(_)) => {
72                return self
73                    .execute_with_params(session_id, stmt, parameters, session_variables)
74                    .await
75                    .map(|result| StreamingQueryResult::from_execution_result(result.execution));
76            }
77            Ok(None) => {}
78        }
79
80        // Session inspection (SHOW SESSIONS / SHOW QUERIES / KILL QUERY)
81        // mutates no session state but answers from the live registries.
82        // It runs on the materialized path, whose single implementation
83        // serves all three from the session and query managers, and comes
84        // back as a one-chunk stream; the streaming request below snapshots
85        // execution state up front, so routing inspection through it would
86        // bypass the registry lookup.
87        if Self::parse_stmt(stmt).is_some_and(|parsed| {
88            Self::is_use_statement(&parsed) || Self::is_session_inspection(&parsed)
89        }) {
90            return self
91                .execute_with_params(session_id, stmt, parameters, session_variables)
92                .await
93                .map(|result| StreamingQueryResult::from_execution_result(result.execution));
94        }
95
96        // Regular statements run the same permission gate as the materialized
97        // path, so a statement admitted by one entry can never be rejected by
98        // the other. Commands above already re-enter that gate through
99        // `execute_with_params`.
100        let permission_context = super::context::QueryExecutionContext {
101            stmt,
102            parsed_ast: None,
103            space_id: session.space().map(|s| s.id as i64).unwrap_or(0),
104            parameters: None,
105            session_variables: None,
106        };
107        self.check_query_permission(&session.user(), &permission_context)?;
108
109        let query_id = self.next_query_id.fetch_add(1, Ordering::Relaxed) as u32;
110        // Streaming executions share the service-wide id lane with unary
111        // ones, so SHOW QUERIES rows stay valid KILL QUERY targets.
112        let _ = self.query_manager.register_query_with_id(
113            query_id as i64,
114            session.id(),
115            session.user(),
116            session.space().map(|space| space.name),
117            stmt.to_string(),
118        );
119        let query_request = linkrs_api::api_core::QueryRequest {
120            isolation_level: None,
121            space_id: session.space().map(|s| s.id),
122            space_name: session.space().map(|s| s.name),
123            auto_commit: session.is_auto_commit(),
124            transaction_id: session.current_transaction(),
125            parameters,
126            session_variables,
127            query_id: Some(query_id as u64),
128            parsed_statement: None,
129            consistency: Default::default(),
130        };
131
132        let mut query_api = self.query_api.write();
133        let result = if let Some(txn_id) = session.current_transaction() {
134            let manager = self
135                .transaction_manager
136                .as_ref()
137                .ok_or_else(|| GraphServiceError::new("Transaction manager is not configured"))?;
138            manager
139                .refresh_statement_snapshot(txn_id)
140                .map_err(|error| GraphServiceError::new(error.to_string()))?;
141            let execution = manager
142                .create_execution(txn_id, false)
143                .map_err(|error| GraphServiceError::new(error.to_string()))?;
144            query_api
145                .execute_stream_with_execution(stmt, query_request, &execution)
146                .map_err(|e| GraphServiceError::from_core_error_with_query(e, stmt))?
147        } else {
148            let execution_storage = self
149                .storage
150                .bind_auto_commit_context()
151                .map_err(|error| GraphServiceError::new(error.to_string()))?;
152            query_api
153                .execute_stream_with_operation_storage(stmt, query_request, execution_storage)
154                .map_err(|e| GraphServiceError::from_core_error_with_query(e, stmt))?
155        };
156
157        result.runtime().assign_query_id(query_id as u64);
158        result
159            .runtime()
160            .set_progress_identity(session_id, query_id as i64);
161        result
162            .runtime()
163            .set_progress_rows_interval(self.progress_rows_interval);
164        session.register_streaming_query(query_id, stmt.to_string(), result.runtime_downgrade());
165
166        let session_clone = session.clone();
167        let tracked_manager = Arc::clone(&self.query_manager);
168        let tracked_id = query_id as i64;
169        result.set_on_drop(Box::new(move || {
170            session_clone.unregister_streaming_query(query_id);
171            if tracked_manager.get_query(tracked_id).is_some_and(|info| {
172                info.status == crate::query::query_manager::QueryStatus::Running
173            }) {
174                let _ = tracked_manager.finish_query(tracked_id);
175            }
176            tracked_manager.cleanup_finished_queries(1000);
177        }));
178
179        Ok(result)
180    }
181}