Skip to main content

linkrs_server/graph_service/
query.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use log::warn;
5
6use linkrs_api::api_core::types::ConsistencyLevel;
7use linkrs_api::api_core::QueryResult;
8use linkrs_transaction::types::TransactionExecution;
9use linkrs_transaction::TransactionId;
10
11use super::context::QueryExecutionContext;
12use super::error::GraphServiceError;
13use super::GraphService;
14use crate::session::ClientSession;
15use crate::storage::{
16    StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
17};
18
19impl<
20        S: StorageClient
21            + StorageSchemaContextOps
22            + StorageSyncContextOps
23            + StorageOperationContextOps
24            + Clone
25            + 'static,
26    > GraphService<S>
27{
28    pub async fn execute(
29        &self,
30        session_id: i64,
31        stmt: &str,
32    ) -> Result<QueryResult, GraphServiceError> {
33        self.execute_inner(session_id, stmt, None, None, ConsistencyLevel::default())
34            .await
35    }
36
37    /// Execute a query with client-supplied query parameters (`@name`
38    /// references) and/or session variables (`$name` references).
39    pub async fn execute_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<QueryResult, GraphServiceError> {
46        self.execute_inner(
47            session_id,
48            stmt,
49            parameters,
50            session_variables,
51            ConsistencyLevel::default(),
52        )
53        .await
54    }
55
56    /// Execute a query with explicit consistency requirement.
57    pub async fn execute_with_consistency(
58        &self,
59        session_id: i64,
60        stmt: &str,
61        parameters: Option<HashMap<String, linkrs_core::Value>>,
62        session_variables: Option<HashMap<String, linkrs_core::Value>>,
63        consistency: ConsistencyLevel,
64    ) -> Result<QueryResult, GraphServiceError> {
65        self.execute_inner(session_id, stmt, parameters, session_variables, consistency)
66            .await
67    }
68
69    /// Unified materialized execution path. All public entry points delegate
70    /// here with either the default or an explicit consistency level, so
71    /// command classification, USE handling, and auto-commit stay in one place.
72    async fn execute_inner(
73        &self,
74        session_id: i64,
75        stmt: &str,
76        parameters: Option<HashMap<String, linkrs_core::Value>>,
77        session_variables: Option<HashMap<String, linkrs_core::Value>>,
78        consistency: ConsistencyLevel,
79    ) -> Result<QueryResult, GraphServiceError> {
80        let session = self
81            .session_manager
82            .find_session(session_id)
83            .ok_or_else(|| GraphServiceError::new(format!("Invalid session ID: {}", session_id)))?;
84
85        let space_id = session.space().map(|s| s.id as i64).unwrap_or(0);
86
87        if let Some(ref txn_manager) = self.transaction_manager {
88            txn_manager.cleanup_expired_transactions();
89        }
90
91        if let Some(parsed) = Self::parse_command(stmt)? {
92            let parsed_ast = parsed.ast;
93            let stmt_ast = parsed_ast.stmt();
94            let context = QueryExecutionContext {
95                stmt,
96                parsed_ast: Some(parsed_ast.clone()),
97                space_id,
98                parameters,
99                session_variables,
100            };
101            match stmt_ast {
102                crate::query::parser::ast::Stmt::AssignVariable(assign) => {
103                    return self.execute_variable_assignment(
104                        &session,
105                        parsed_ast.clone(),
106                        assign,
107                        &context,
108                        consistency,
109                    );
110                }
111                _ => {
112                    return self.execute_transaction_command(
113                        &session,
114                        stmt_ast,
115                        &context,
116                        consistency,
117                    );
118                }
119            }
120        }
121
122        let context = QueryExecutionContext {
123            stmt,
124            parsed_ast: None,
125            space_id,
126            parameters,
127            session_variables,
128        };
129        if let Some(parsed_stmt) = Self::parse_stmt(stmt) {
130            use crate::query::parser::ast::Stmt as ParsedStmt;
131            match parsed_stmt {
132                ParsedStmt::ShowSessions(_)
133                | ParsedStmt::ShowQueries(_)
134                | ParsedStmt::KillQuery(_) => {
135                    return self
136                        .execute_session_inspection(session_id, stmt, parsed_stmt)
137                        .await;
138                }
139                _ => {}
140            }
141        }
142        let parsed = Self::parse_stmt(stmt);
143        let mut result =
144            self.execute_query_with_permission(session_id, &context, consistency.clone());
145
146        if parsed
147            .as_ref()
148            .is_some_and(|parsed| Self::is_use_statement(parsed))
149        {
150            if let Ok(ref exec_result) = result {
151                if let Some(space_summary) = Self::extract_space_summary_from_result(exec_result) {
152                    session.set_space(space_summary);
153                }
154            }
155        }
156
157        self.finish_auto_commit(&session, &mut result);
158        if result.is_ok() {
159            let needs_alignment = parsed
160                .as_ref()
161                .is_some_and(|parsed| Self::is_user_management_write_stmt(parsed));
162            if needs_alignment {
163                self.reconcile_user_state(Some(session_id)).await;
164                self.evict_stale_user_sessions(&session, session_id, stmt)
165                    .await;
166            }
167        }
168        result
169    }
170
171    /// Enforce the password policy on query-channel password writes.
172    ///
173    /// Single hook on the execution path covering user creation, admin
174    /// alteration carrying a new password, and self-service password changes.
175    /// History reuse is checked against stored hashes. The caller has already
176    /// passed the permission gate by the time this runs.
177    pub(crate) fn enforce_new_password_policy(
178        &self,
179        caller: &str,
180        stmt: &crate::query::parser::ast::Stmt,
181    ) -> Result<(), GraphServiceError> {
182        use crate::query::parser::ast::Stmt as Parsed;
183        let invalid =
184            |reason: String| GraphServiceError::new(format!("invalid password: {}", reason));
185        match stmt {
186            Parsed::CreateUser(create) => self
187                .validate_new_password(&create.username, &create.password)
188                .map_err(invalid),
189            Parsed::AlterUser(alter) => match &alter.password {
190                Some(password) => self
191                    .validate_new_password(&alter.username, password)
192                    .map_err(invalid),
193                None => Ok(()),
194            },
195            Parsed::ChangePassword(change) => {
196                let target = change.username.as_deref().unwrap_or(caller);
197                self.validate_new_password(target, &change.new_password)
198                    .map_err(invalid)
199            }
200            _ => Ok(()),
201        }
202    }
203
204    /// Target account of a query-channel password write, if any.
205    ///
206    /// After a successful rotation or drop, every other session of the
207    /// target account is invalidated, matching the admin channel.
208    async fn evict_stale_user_sessions(
209        &self,
210        session: &Arc<ClientSession>,
211        session_id: i64,
212        stmt: &str,
213    ) {
214        let Some(parsed) = Self::parse_stmt(stmt) else {
215            return;
216        };
217        use crate::query::parser::ast::Stmt as Parsed;
218        let target = match parsed {
219            Parsed::AlterUser(alter) if alter.password.is_some() => Some(alter.username),
220            Parsed::ChangePassword(change) => {
221                Some(change.username.unwrap_or_else(|| session.user()))
222            }
223            Parsed::DropUser(drop) => Some(drop.username),
224            _ => None,
225        };
226        if let Some(target) = target {
227            self.session_manager
228                .remove_sessions_by_username(&target, Some(session_id))
229                .await;
230        }
231    }
232
233    /// Keep permission state and sessions consistent after query-channel
234    /// user writes. Storage is the source of truth.
235    async fn reconcile_user_state(&self, except_session_id: Option<i64>) {
236        let stored_users = self.storage.list_users();
237        let tracked: Vec<String> = self
238            .permission_manager
239            .list_all_users()
240            .into_iter()
241            .map(|(name, _)| name)
242            .collect();
243        for name in tracked {
244            if !stored_users.contains(&name) {
245                self.permission_manager.remove_user(&name);
246            }
247        }
248        for username in &stored_users {
249            self.sync_role_from_storage(username);
250            let locked = self
251                .storage
252                .get_user(username)
253                .map(|user| user.is_locked)
254                .unwrap_or(false);
255            if locked {
256                self.session_manager
257                    .remove_sessions_by_username(username, except_session_id)
258                    .await;
259            }
260        }
261    }
262
263    /// Commit the session-bound transaction when the session runs in
264    /// auto-commit mode. A commit failure replaces the original result.
265    pub(crate) fn finish_auto_commit(
266        &self,
267        session: &Arc<ClientSession>,
268        result: &mut Result<QueryResult, GraphServiceError>,
269    ) {
270        if result.is_ok() && session.is_auto_commit() {
271            if let Some(txn_id) = session.current_transaction() {
272                if let Some(ref txn_manager) = self.transaction_manager {
273                    match txn_manager.commit_transaction(txn_id) {
274                        Ok(()) => {
275                            session.unbind_transaction();
276                        }
277                        Err(e) => {
278                            warn!("Auto-commit failed for transaction {}: {}", txn_id, e);
279                            session.unbind_transaction();
280                            *result =
281                                Err(GraphServiceError::new(format!("Auto-commit failed: {}", e)));
282                        }
283                    }
284                }
285            }
286        }
287    }
288
289    pub(crate) fn execute_query_with_permission(
290        &self,
291        session_id: i64,
292        context: &QueryExecutionContext<'_>,
293        consistency: ConsistencyLevel,
294    ) -> Result<QueryResult, GraphServiceError> {
295        let session = self
296            .session_manager
297            .find_session(session_id)
298            .ok_or_else(|| GraphServiceError::new(format!("Invalid session ID: {}", session_id)))?;
299
300        session.charge();
301        let username = session.user();
302        self.check_query_permission(&username, context)?;
303
304        // Track the execution under the service-wide id so SHOW QUERIES
305        // lists it and KILL QUERY can target it; the same id travels in
306        // the engine request and backs the registry cancellation token.
307        let tracked_query_id = self.assign_tracked_query_id(&session, &username, context.stmt);
308
309        let mut statement_guard = None;
310        let execution = if let Some(txn_id) = session.current_transaction() {
311            if let Some(ref txn_manager) = self.transaction_manager {
312                match txn_manager.begin_statement(txn_id) {
313                    Ok((ctx, statement_start)) => {
314                        statement_guard = Some((txn_manager.clone(), ctx.clone(), statement_start));
315                        Some(
316                            txn_manager
317                                .create_execution(ctx.id, false)
318                                .map_err(|error| GraphServiceError::new(error.to_string()))?,
319                        )
320                    }
321                    Err(e) => {
322                        if e.is_timeout() {
323                            warn!(
324                                "Transaction {} exceeded a timeout before statement execution",
325                                txn_id
326                            );
327                        }
328                        return Err(GraphServiceError::new(e.to_string()));
329                    }
330                }
331            } else {
332                None
333            }
334        } else {
335            None
336        };
337
338        let mut result = self.run_query_plan(
339            &session,
340            context,
341            None,
342            execution,
343            consistency,
344            tracked_query_id.map(|tracked| tracked as u64),
345        );
346
347        if let Some((txn_manager, context, statement_start)) = statement_guard {
348            if let Err(error) = txn_manager.finish_statement(&context, statement_start) {
349                result = Err(GraphServiceError::new(error.to_string()));
350            }
351        }
352
353        self.finish_tracked_query(tracked_query_id, result.is_ok());
354
355        if result.is_err() {
356            self.cleanup_invalid_transaction(&session);
357        }
358
359        result
360    }
361
362    /// Drop a session transaction that can no longer execute after a failed query.
363    pub(crate) fn cleanup_invalid_transaction(&self, session: &Arc<ClientSession>) {
364        if let Some(txn_id) = session.current_transaction() {
365            if let Some(ref txn_manager) = self.transaction_manager {
366                if let Ok(ctx) = txn_manager.get_context(txn_id) {
367                    if !ctx.state().can_execute() {
368                        warn!(
369                            "Transaction {} is in invalid state {} after failed query, cleaning up",
370                            txn_id,
371                            ctx.state()
372                        );
373                        if let Err(e) = txn_manager.abort_transaction(txn_id) {
374                            warn!("Failed to rollback invalid transaction {}: {}", txn_id, e);
375                        }
376                        session.unbind_transaction();
377                        session.set_auto_commit(true);
378                        session.rollback_variables();
379                    }
380                }
381            }
382        }
383    }
384
385    /// Shared query execution segment for regular statements and transaction
386    /// commands: builds the `QueryRequest` and invokes the query API with the
387    /// resolved execution binding, converting the result.
388    pub(crate) fn run_query_plan(
389        &self,
390        session: &Arc<ClientSession>,
391        context: &QueryExecutionContext<'_>,
392        transaction_id: Option<TransactionId>,
393        execution: Option<TransactionExecution>,
394        consistency: ConsistencyLevel,
395        query_id: Option<u64>,
396    ) -> Result<QueryResult, GraphServiceError> {
397        let merged_session_variables = match context.session_variables {
398            Some(ref client_variables) => {
399                let mut merged = session.variables_snapshot();
400                merged.extend(client_variables.clone());
401                Some(merged)
402            }
403            None => Some(session.variables_snapshot()),
404        };
405        let query_request = linkrs_api::api_core::QueryRequest {
406            isolation_level: None,
407            space_id: session.space().map(|s| s.id),
408            space_name: session.space().map(|s| s.name),
409            auto_commit: session.is_auto_commit(),
410            transaction_id: transaction_id.or_else(|| session.current_transaction()),
411            parameters: context.parameters.clone(),
412            session_variables: merged_session_variables,
413            query_id,
414            parsed_statement: context.parsed_ast.clone(),
415            consistency,
416        };
417
418        let mut query_api = self.query_api.write();
419        if let Some(execution) = execution.as_ref() {
420            query_api
421                .execute_with_execution(context.stmt, query_request, execution)
422                .map_err(|e| GraphServiceError::from_core_error_with_query(e, context.stmt))
423        } else {
424            let execution_storage = self
425                .storage
426                .bind_auto_commit_context()
427                .map_err(|error| GraphServiceError::new(error.to_string()))?;
428            query_api
429                .execute_with_operation_storage(context.stmt, query_request, execution_storage)
430                .map_err(|e| GraphServiceError::from_core_error_with_query(e, context.stmt))
431        }
432    }
433}