Skip to main content

linkrs_server/graph_service/
session.rs

1use std::sync::Arc;
2
3use linkrs_api::api_core::SyncApi;
4#[cfg(feature = "vector")]
5use linkrs_api::api_core::VectorApi;
6use linkrs_metrics::{MetricType, StatsManager};
7use linkrs_query::query_manager::QueryManager;
8
9use crate::auth::Authenticator;
10use crate::permission::GOD_SPACE_ID;
11use crate::query::executor::streaming::transaction_scope::CancelReason;
12use crate::session::{SessionError, SessionResult};
13use crate::storage::{
14    StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
15};
16
17use super::GraphService;
18
19/// Detailed user entry combining stored account state with effective role.
20#[derive(Debug, Clone)]
21pub struct UserDetail {
22    pub username: String,
23    pub role: Option<String>,
24    pub status: String,
25    pub last_active: Option<String>,
26}
27
28fn format_last_active(millis: Option<i64>) -> Option<String> {
29    millis.and_then(|ts| chrono::DateTime::from_timestamp_millis(ts).map(|dt| dt.to_rfc3339()))
30}
31
32impl<
33        S: StorageClient
34            + StorageSchemaContextOps
35            + StorageSyncContextOps
36            + StorageOperationContextOps
37            + Clone
38            + 'static,
39    > GraphService<S>
40{
41    pub async fn authenticate(
42        &self,
43        username: &str,
44        password: &str,
45    ) -> Result<Arc<crate::session::ClientSession>, String> {
46        if username.is_empty() || password.is_empty() {
47            self.stats_manager
48                .add_value(MetricType::NumAuthFailedSessions);
49            return Err("User name or password cannot be empty".to_string());
50        }
51
52        if self.session_manager.is_out_of_connections().await {
53            self.stats_manager
54                .add_value(MetricType::NumAuthFailedSessions);
55            return Err("More than the maximum number of connections limit".to_string());
56        }
57
58        let auth = self.authenticator.clone();
59        let owned_user = username.to_string();
60        let owned_pass = password.to_string();
61        let auth_result =
62            tokio::task::spawn_blocking(move || auth.authenticate(&owned_user, &owned_pass))
63                .await
64                .map_err(|e| format!("authentication failure: blocking join failed: {}", e))?;
65
66        match auth_result {
67            Ok(_) => {
68                let _ = self.storage.update_last_login(username);
69                let session = self
70                    .session_manager
71                    .create_session(username.to_string(), "127.0.0.1".to_string())
72                    .await
73                    .map_err(|e| format!("Creating a session failed: {}", e))?;
74
75                Ok(session)
76            }
77            Err(e) => {
78                self.stats_manager
79                    .add_value(MetricType::NumAuthFailedSessions);
80                Err(format!("authentication failure: {}", e))
81            }
82        }
83    }
84
85    /// Password-policy settings snapshotted at startup.
86    pub fn security_config(&self) -> &crate::config::SecurityConfig {
87        &self.security_config
88    }
89
90    /// Validate a new plaintext password against the configured policy.
91    ///
92    /// Applies on every write path, including auth-disabled deployments:
93    /// disabling auth only skips the login step, never weak-password writes.
94    pub fn validate_new_password(&self, username: &str, password: &str) -> Result<(), String> {
95        if password.is_empty() {
96            return Err("password cannot be empty".to_string());
97        }
98        self.security_config
99            .password_policy
100            .validate_password(username, password)
101            .map_err(|reason| format!("password does not meet the password policy: {}", reason))?;
102        self.reject_reused_password(username, password)?;
103        Ok(())
104    }
105
106    /// Whether the account password is past the configured maximum age.
107    pub fn password_expired(&self, username: &str) -> bool {
108        let max_age_days = self.security_config.password_policy.max_age_days;
109        if max_age_days == 0 {
110            return false;
111        }
112        match self.storage.get_user(username) {
113            Some(user) => {
114                let now = chrono::Utc::now().timestamp_millis();
115                let age_millis = now.saturating_sub(user.password_changed_at);
116                age_millis > (max_age_days as i64).saturating_mul(86_400_000)
117            }
118            None => false,
119        }
120    }
121
122    /// Reject a new password that matches the current or a retained history hash.
123    fn reject_reused_password(&self, username: &str, password: &str) -> Result<(), String> {
124        if self.security_config.password_policy.history_size == 0 {
125            return Ok(());
126        }
127        let Some(user) = self.storage.get_user(username) else {
128            return Ok(());
129        };
130        if user.reuses_password(password) {
131            return Err("password must not reuse a recent password".to_string());
132        }
133        Ok(())
134    }
135
136    /// Session idle timeout driving expiry responses and reclamation.
137    pub fn session_idle_timeout(&self) -> std::time::Duration {
138        self.session_manager.idle_timeout()
139    }
140
141    /// Whether the login response should prompt a password change.
142    ///
143    /// True when forced rotation is configured and the default seed account
144    /// has never changed its password, or when the account password is past
145    /// the configured maximum age.
146    pub fn must_change_password(&self, username: &str) -> bool {
147        if self.is_auth_disabled() {
148            return false;
149        }
150        if self.password_expired(username) {
151            return true;
152        }
153        if !self.authenticator.config().force_change_default_password {
154            return false;
155        }
156        if username != self.authenticator.config().default_username {
157            return false;
158        }
159        match self.storage.get_user(username) {
160            Some(user) => user.password_changed_at <= user.created_at,
161            None => false,
162        }
163    }
164
165    /// Whether the account is currently disabled.
166    pub fn is_user_locked(&self, username: &str) -> bool {
167        self.storage
168            .get_user(username)
169            .map(|user| user.is_locked)
170            .unwrap_or(false)
171    }
172
173    /// Graceful shutdown of the shared execution infrastructure.
174    pub fn shutdown(&self) {
175        let cancelled = self.query_registry.cancel_all(CancelReason::Shutdown);
176        if !cancelled.is_empty() {
177            log::info!(
178                "Cancelled {} active query(s) during shutdown",
179                cancelled.len()
180            );
181        }
182        self.shared_scheduler.shutdown_shared();
183        log::info!("Shared query scheduler shut down");
184    }
185
186    pub async fn signout(&self, session_id: i64) {
187        if let Some(session) = self.session_manager.find_session(session_id) {
188            if let Some(space_name) = session.space_name() {
189                self.stats_manager
190                    .dec_space_metric(&space_name, MetricType::NumActiveQueries);
191            }
192        }
193        self.session_manager.remove_session(session_id).await;
194    }
195
196    pub fn get_session_manager(&self) -> &Arc<crate::session::GraphSessionManager> {
197        &self.session_manager
198    }
199
200    pub fn get_permission_manager(&self) -> &Arc<crate::permission::PermissionManager> {
201        &self.permission_manager
202    }
203
204    pub fn permission_checker(&self) -> crate::permission::PermissionChecker {
205        crate::permission::PermissionChecker::new(
206            (*self.permission_manager).clone(),
207            self.authenticator.config().clone(),
208        )
209    }
210
211    /// Whether authentication is globally disabled.
212    ///
213    /// Returns true when either `[auth].enable_authorize = false` or
214    /// `[bootstrap].single_user_mode = true`. Both flags cause the
215    /// middleware layer to skip session-id verification and fall through to a
216    /// default identity so purely-local deployments can operate without a
217    /// login round-trip.
218    pub fn is_auth_disabled(&self) -> bool {
219        !self.authenticator.config().enable_authorize || self.bootstrap_config.single_user_mode
220    }
221
222    /// Authenticator configuration reference.
223    pub fn auth_config(&self) -> &crate::config::AuthConfig {
224        self.authenticator.config()
225    }
226
227    /// Bootstrap configuration reference.
228    pub fn bootstrap_config(&self) -> &crate::config::BootstrapConfig {
229        &self.bootstrap_config
230    }
231
232    pub fn get_storage_space_id(&self, space_name: &str) -> Option<i64> {
233        self.storage
234            .get_space_id(space_name)
235            .map(|id| id as i64)
236            .ok()
237    }
238
239    pub fn get_stats_manager(&self) -> &Arc<StatsManager> {
240        &self.stats_manager
241    }
242
243    #[cfg(feature = "vector")]
244    pub fn vector_api(&self) -> Option<&Arc<VectorApi>> {
245        self.vector_api.as_ref()
246    }
247
248    pub fn sync_api(&self) -> Option<&Arc<SyncApi>> {
249        self.sync_api.as_ref()
250    }
251
252    /// Shared process-level query manager.
253    pub fn query_manager(&self) -> Arc<QueryManager> {
254        Arc::clone(&self.query_manager)
255    }
256
257    /// Obtain the session list (SHOW SESSIONS)
258    pub async fn list_sessions(&self) -> Vec<crate::session::SessionInfo> {
259        self.session_manager.list_sessions().await
260    }
261
262    /// Obtain detailed information about the specified session.
263    pub async fn get_session_info(&self, session_id: i64) -> Option<crate::session::SessionInfo> {
264        self.session_manager.get_session_info(session_id).await
265    }
266
267    /// Terminate the session (KILL SESSION)
268    pub async fn kill_session(&self, session_id: i64, current_user: &str) -> SessionResult<()> {
269        let is_admin = self.permission_manager.is_admin(current_user);
270
271        self.session_manager
272            .kill_session(session_id, current_user, is_admin)
273            .await
274    }
275
276    /// Terminate the query (KILL QUERY)
277    pub fn kill_query(&self, session_id: i64, query_id: u32) -> SessionResult<()> {
278        let session = self
279            .session_manager
280            .find_session(session_id)
281            .ok_or(SessionError::session_not_found(session_id))?;
282
283        match session.kill_query(query_id) {
284            Ok(()) => {
285                self.stats_manager.dec_value(MetricType::NumActiveQueries);
286                Ok(())
287            }
288            Err(e) => Err(SessionError::manager_error(e.to_string())),
289        }
290    }
291
292    fn require_admin(&self, caller: &str) -> Result<(), String> {
293        if self.permission_manager.is_admin(caller) {
294            Ok(())
295        } else {
296            Err("admin permission required".to_string())
297        }
298    }
299
300    fn enabled_admin_count_excluding(&self, exclude: &str) -> usize {
301        self.storage
302            .list_users()
303            .into_iter()
304            .filter(|name| name != exclude)
305            .filter(|name| self.permission_manager.is_admin(name))
306            .filter(|name| {
307                self.storage
308                    .get_user(name)
309                    .map(|user| !user.is_locked)
310                    .unwrap_or(false)
311            })
312            .count()
313    }
314
315    /// List users with display role, enablement status and last login time.
316    pub fn list_users_detailed(&self) -> Vec<UserDetail> {
317        let mut details: Vec<UserDetail> = self
318            .storage
319            .list_users()
320            .into_iter()
321            .map(|username| {
322                let stored = self.storage.get_user(&username);
323                let role = self
324                    .permission_manager
325                    .highest_role(&username)
326                    .map(|role| role.to_string());
327                let status = match stored.as_ref() {
328                    Some(user) if user.is_locked => "disabled".to_string(),
329                    _ => "enabled".to_string(),
330                };
331                let last_active = stored.and_then(|user| format_last_active(user.last_login_at));
332                UserDetail {
333                    username,
334                    role,
335                    status,
336                    last_active,
337                }
338            })
339            .collect();
340        details.sort_by(|a, b| a.username.cmp(&b.username));
341        details
342    }
343
344    /// Create a user through the admin channel.
345    pub fn admin_create_user(
346        &self,
347        caller: &str,
348        username: &str,
349        password: &str,
350    ) -> Result<(), String> {
351        self.require_admin(caller)?;
352        if username.is_empty() || password.is_empty() {
353            return Err("username and password cannot be empty".to_string());
354        }
355        if self.storage.user_exists(username) {
356            return Err(format!("user {} already exists", username));
357        }
358        self.validate_new_password(username, password)?;
359        let info = linkrs_core::types::UserInfo::new(username.to_string(), password.to_string())
360            .map_err(|e| e.to_string())?;
361        let mut handle = (*self.storage).clone();
362        handle.create_user(&info).map_err(|e| e.to_string())?;
363        Ok(())
364    }
365
366    /// Reset a password through the admin channel without old-password check.
367    ///
368    /// `except_session_id` keeps the caller's own session alive while every
369    /// other session of the target account is invalidated.
370    pub async fn admin_reset_password(
371        &self,
372        caller: &str,
373        caller_session_id: Option<i64>,
374        username: &str,
375        new_password: &str,
376    ) -> Result<(), String> {
377        self.require_admin(caller)?;
378        if new_password.is_empty() {
379            return Err("password cannot be empty".to_string());
380        }
381        if !self.storage.user_exists(username) {
382            return Err(format!("user {} not found", username));
383        }
384        self.validate_new_password(username, new_password)?;
385        let mut alter = linkrs_core::types::UserAlterInfo::new(username.to_string());
386        alter.new_password = Some(new_password.to_string());
387        alter.history_limit = self.security_config.password_policy.history_size;
388        let mut handle = (*self.storage).clone();
389        handle.alter_user(&alter).map_err(|e| e.to_string())?;
390        self.session_manager
391            .remove_sessions_by_username(username, caller_session_id)
392            .await;
393        Ok(())
394    }
395
396    /// Enable or disable a user; disabling evicts existing sessions.
397    pub async fn admin_set_user_enabled(
398        &self,
399        caller: &str,
400        caller_session_id: Option<i64>,
401        username: &str,
402        enabled: bool,
403    ) -> Result<(), String> {
404        self.require_admin(caller)?;
405        if username == caller && !enabled {
406            return Err("cannot disable self".to_string());
407        }
408        let stored = self
409            .storage
410            .get_user(username)
411            .ok_or_else(|| format!("user {} not found", username))?;
412        if stored.is_locked == !enabled {
413            return Ok(());
414        }
415        if !enabled
416            && self.permission_manager.is_admin(username)
417            && self.enabled_admin_count_excluding(username) == 0
418        {
419            return Err("cannot disable the last available admin".to_string());
420        }
421        let mut alter = linkrs_core::types::UserAlterInfo::new(username.to_string());
422        alter.is_locked = Some(!enabled);
423        let mut handle = (*self.storage).clone();
424        handle.alter_user(&alter).map_err(|e| e.to_string())?;
425        if !enabled {
426            self.session_manager
427                .remove_sessions_by_username(username, caller_session_id)
428                .await;
429        }
430        Ok(())
431    }
432
433    /// Drop a user and clean roles plus sessions.
434    pub async fn admin_drop_user(&self, caller: &str, username: &str) -> Result<(), String> {
435        self.require_admin(caller)?;
436        if username == caller {
437            return Err("cannot drop self".to_string());
438        }
439        if !self.storage.user_exists(username) {
440            return Err(format!("user {} not found", username));
441        }
442        if self.permission_manager.is_admin(username)
443            && self.enabled_admin_count_excluding(username) == 0
444        {
445            return Err("cannot drop the last available admin".to_string());
446        }
447        let mut handle = (*self.storage).clone();
448        handle.drop_user(username).map_err(|e| e.to_string())?;
449        self.permission_manager.remove_user(username);
450        self.session_manager
451            .remove_sessions_by_username(username, None)
452            .await;
453        Ok(())
454    }
455
456    fn resolve_space_id(&self, space: &str) -> Result<i64, String> {
457        let trimmed = space.trim();
458        if trimmed.is_empty()
459            || trimmed.eq_ignore_ascii_case("global")
460            || trimmed == GOD_SPACE_ID.to_string()
461        {
462            return Ok(GOD_SPACE_ID);
463        }
464        self.storage
465            .get_space_id(trimmed)
466            .map(|id| id as i64)
467            .map_err(|e| e.to_string())
468    }
469
470    /// Grant a role with dual write to storage and permission state.
471    pub fn admin_grant_role(
472        &self,
473        caller: &str,
474        username: &str,
475        space: &str,
476        role_name: &str,
477    ) -> Result<(), String> {
478        if caller == username {
479            return Err("cannot modify own role".to_string());
480        }
481        if !self.storage.user_exists(username) {
482            return Err(format!("user {} not found", username));
483        }
484        let role: linkrs_core::RoleType = role_name.parse().map_err(|e: String| e)?;
485        let space_id = self.resolve_space_id(space)?;
486        let operator_role = self
487            .permission_manager
488            .get_role(caller, space_id)
489            .or_else(|| self.permission_manager.get_role(caller, GOD_SPACE_ID))
490            .ok_or_else(|| "permission denied: only Admin or God can manage roles".to_string())?;
491        if !matches!(
492            operator_role,
493            linkrs_core::RoleType::God | linkrs_core::RoleType::Admin | linkrs_core::RoleType::Dba
494        ) {
495            return Err("permission denied: only Admin or God can manage roles".to_string());
496        }
497        if !operator_role.can_grant(role) {
498            return Err(format!("permission denied: cannot grant role {}", role));
499        }
500        let mut handle = (*self.storage).clone();
501        handle
502            .grant_role(username, space_id, role)
503            .map_err(|e| e.to_string())?;
504        self.permission_manager
505            .grant_role(username, space_id, role)
506            .map_err(|e| e.to_string())?;
507        Ok(())
508    }
509
510    /// Revoke a role with dual write to storage and permission state.
511    pub fn admin_revoke_role(
512        &self,
513        caller: &str,
514        username: &str,
515        space: &str,
516    ) -> Result<(), String> {
517        if caller == username {
518            return Err("cannot modify own role".to_string());
519        }
520        if !self.storage.user_exists(username) {
521            return Err(format!("user {} not found", username));
522        }
523        let space_id = self.resolve_space_id(space)?;
524        let operator_role = self
525            .permission_manager
526            .get_role(caller, space_id)
527            .or_else(|| self.permission_manager.get_role(caller, GOD_SPACE_ID))
528            .ok_or_else(|| "permission denied: only Admin or God can manage roles".to_string())?;
529        if !matches!(
530            operator_role,
531            linkrs_core::RoleType::God | linkrs_core::RoleType::Admin | linkrs_core::RoleType::Dba
532        ) {
533            return Err("permission denied: only Admin or God can manage roles".to_string());
534        }
535        let mut handle = (*self.storage).clone();
536        handle
537            .revoke_role(username, space_id)
538            .map_err(|e| e.to_string())?;
539        self.permission_manager
540            .revoke_role(username, space_id)
541            .map_err(|e| e.to_string())?;
542        Ok(())
543    }
544
545    /// Sync query-channel role writes into permission state.
546    ///
547    /// Storage remains the source of truth; this keeps the in-memory
548    /// permission map consistent when GRANT/REVOKE arrive via queries.
549    pub fn sync_role_from_storage(&self, username: &str) {
550        let entries = self.storage.list_user_roles(username);
551        if entries.is_empty() {
552            self.permission_manager.remove_user(username);
553            return;
554        }
555        for (space_id, role) in entries {
556            let _ = self.permission_manager.grant_role(username, space_id, role);
557        }
558    }
559
560    /// Serve SHOW SESSIONS from the live session registry.
561    ///
562    /// Columns: session_id, user_name, space_name, created_at,
563    /// last_active, active_queries, graph_addr. Times are unix epoch
564    /// seconds; an unset space or address renders as NULL.
565    pub(crate) async fn serve_show_sessions(
566        &self,
567    ) -> Result<linkrs_api::api_core::QueryResult, super::error::GraphServiceError> {
568        use linkrs_core::{NullType, Value};
569        let mut sessions = self.list_sessions().await;
570        sessions.sort_by_key(|info| info.session_id);
571        let rows = sessions
572            .iter()
573            .map(|info| {
574                vec![
575                    Value::BigInt(info.session_id),
576                    Value::string(info.user_name.clone()),
577                    info.space_name
578                        .clone()
579                        .map(Value::string)
580                        .unwrap_or(Value::Null(NullType::Null)),
581                    Value::BigInt(epoch_secs(info.create_time)),
582                    Value::BigInt(epoch_secs(info.last_access_time)),
583                    Value::BigInt(info.active_queries as i64),
584                    info.graph_addr
585                        .clone()
586                        .map(Value::string)
587                        .unwrap_or(Value::Null(NullType::Null)),
588                ]
589            })
590            .collect();
591        Ok(Self::inspection_result(
592            rows,
593            vec![
594                "session_id".to_string(),
595                "user_name".to_string(),
596                "space_name".to_string(),
597                "created_at".to_string(),
598                "last_active".to_string(),
599                "active_queries".to_string(),
600                "graph_addr".to_string(),
601            ],
602        ))
603    }
604
605    /// Serve SHOW QUERIES from tracked running queries.
606    ///
607    /// Columns: query_id, session_id, user_name, space_name, status,
608    /// duration_ms, query. The query_id values are the KILL QUERY targets.
609    /// Only executions registered through the service carry entries;
610    /// durations of running queries are measured live.
611    pub(crate) fn serve_show_queries(
612        &self,
613    ) -> Result<linkrs_api::api_core::QueryResult, super::error::GraphServiceError> {
614        use linkrs_core::{NullType, Value};
615        let mut running = self.query_manager.get_running_queries();
616        running.sort_by_key(|info| info.query_id);
617        let rows = running
618            .iter()
619            .map(|info| {
620                vec![
621                    Value::BigInt(info.query_id),
622                    Value::BigInt(info.session_id),
623                    Value::string(info.user_name.clone()),
624                    info.space_name
625                        .clone()
626                        .map(Value::string)
627                        .unwrap_or(Value::Null(NullType::Null)),
628                    Value::string(query_status_name(info.status)),
629                    Value::BigInt(live_duration_ms(info)),
630                    Value::string(info.query_text.clone()),
631                ]
632            })
633            .collect();
634        Ok(Self::inspection_result(
635            rows,
636            vec![
637                "query_id".to_string(),
638                "session_id".to_string(),
639                "user_name".to_string(),
640                "space_name".to_string(),
641                "status".to_string(),
642                "duration_ms".to_string(),
643                "query".to_string(),
644            ],
645        ))
646    }
647
648    /// Serve KILL QUERY against tracked query ids.
649    ///
650    /// The target session must exist and own a running query with the
651    /// given id; the kill marks the entry and cancels the shared
652    /// execution token, with a best-effort session-level cancel for
653    /// streaming executions under the same id.
654    pub(crate) fn serve_kill_query(
655        &self,
656        target_session_id: i64,
657        query_id: i64,
658    ) -> Result<linkrs_api::api_core::QueryResult, super::error::GraphServiceError> {
659        use linkrs_core::Value;
660        let target = self
661            .session_manager
662            .find_session(target_session_id)
663            .ok_or_else(|| {
664                super::error::GraphServiceError::new(format!(
665                    "SessionId[{}] does not exist",
666                    target_session_id
667                ))
668            })?;
669        let info = self.query_manager.get_query(query_id).ok_or_else(|| {
670            super::error::GraphServiceError::new(format!(
671                "ExecutionPlanId[{}] does not exist",
672                query_id
673            ))
674        })?;
675        if info.session_id != target_session_id {
676            return Err(super::error::GraphServiceError::new(format!(
677                "ExecutionPlanId[{}] does not exist in Session[{}]",
678                query_id, target_session_id
679            )));
680        }
681        if info.status != linkrs_query::query_manager::QueryStatus::Running {
682            return Err(super::error::GraphServiceError::new(format!(
683                "ExecutionPlanId[{}] is not running (status: {})",
684                query_id,
685                query_status_name(info.status)
686            )));
687        }
688        self.query_manager
689            .kill_query(query_id)
690            .map_err(|e| super::error::GraphServiceError::new(e.to_string()))?;
691        if let Ok(streaming_id) = u32::try_from(query_id) {
692            let _ = target.kill_query(streaming_id);
693        }
694        Ok(Self::inspection_result(
695            vec![vec![
696                Value::BigInt(target_session_id),
697                Value::BigInt(query_id),
698                Value::string("killed".to_string()),
699            ]],
700            vec![
701                "session_id".to_string(),
702                "query_id".to_string(),
703                "status".to_string(),
704            ],
705        ))
706    }
707
708    /// Assign the service-wide execution id used by the engine request,
709    /// the query tracker, and the session tracker alike.
710    pub(crate) fn assign_tracked_query_id(
711        &self,
712        session: &Arc<crate::session::ClientSession>,
713        username: &str,
714        stmt: &str,
715    ) -> Option<i64> {
716        let id = self
717            .next_query_id
718            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
719        let tracked: i64 = id as i64;
720        self.query_manager
721            .register_query_with_id(
722                tracked,
723                session.id(),
724                username.to_string(),
725                session.space().map(|space| space.name),
726                stmt.to_string(),
727            )
728            .ok()?;
729        Some(tracked)
730    }
731
732    /// Close a tracked execution and bound the retained finished history.
733    pub(crate) fn finish_tracked_query(&self, query_id: Option<i64>, success: bool) {
734        if let Some(tracked) = query_id {
735            if success {
736                let _ = self.query_manager.finish_query(tracked);
737            } else {
738                let _ = self.query_manager.fail_query(tracked);
739            }
740            self.query_manager.cleanup_finished_queries(1000);
741        }
742    }
743
744    /// Route session-inspection statements to the live registries.
745    ///
746    /// SHOW SESSIONS, SHOW QUERIES, and KILL QUERY never reach the query
747    /// engine: the engine has no session-registry access, so the service
748    /// answers them directly after the standard permission gate.
749    pub(crate) async fn execute_session_inspection(
750        &self,
751        session_id: i64,
752        stmt: &str,
753        parsed: crate::query::parser::ast::Stmt,
754    ) -> Result<linkrs_api::api_core::QueryResult, super::error::GraphServiceError> {
755        let session = self
756            .session_manager
757            .find_session(session_id)
758            .ok_or_else(|| {
759                super::error::GraphServiceError::new(format!("Invalid session ID: {}", session_id))
760            })?;
761        let space_id = session.space().map(|space| space.id as i64).unwrap_or(0);
762        let context = super::context::QueryExecutionContext {
763            stmt,
764            parsed_ast: None,
765            space_id,
766            parameters: None,
767            session_variables: None,
768        };
769        let username = session.user();
770        self.check_query_permission(&username, &context)?;
771        match parsed {
772            crate::query::parser::ast::Stmt::ShowSessions(_) => self.serve_show_sessions().await,
773            crate::query::parser::ast::Stmt::ShowQueries(_) => self.serve_show_queries(),
774            crate::query::parser::ast::Stmt::KillQuery(kill) => {
775                self.serve_kill_query(kill.session_id, kill.plan_id)
776            }
777            _ => Err(super::error::GraphServiceError::new(
778                "Statement is not a session inspection statement".to_string(),
779            )),
780        }
781    }
782
783    fn inspection_result(
784        rows: Vec<Vec<linkrs_core::Value>>,
785        columns: Vec<String>,
786    ) -> linkrs_api::api_core::QueryResult {
787        let returned = rows.len() as u64;
788        linkrs_api::api_core::QueryResult::new(
789            linkrs_query::executor::base::ExecutionResult::DataSet {
790                data: linkrs_core::DataSet::from_rows(rows, columns),
791            },
792            linkrs_api::api_core::types::ExecutionMetadata {
793                rows_returned: returned,
794                ..Default::default()
795            },
796        )
797    }
798}
799
800fn epoch_secs(time: std::time::SystemTime) -> i64 {
801    time.duration_since(std::time::UNIX_EPOCH)
802        .map(|elapsed| elapsed.as_secs() as i64)
803        .unwrap_or(0)
804}
805
806fn query_status_name(status: linkrs_query::query_manager::QueryStatus) -> String {
807    match status {
808        linkrs_query::query_manager::QueryStatus::Running => "running".to_string(),
809        linkrs_query::query_manager::QueryStatus::Finished => "finished".to_string(),
810        linkrs_query::query_manager::QueryStatus::Failed => "failed".to_string(),
811        linkrs_query::query_manager::QueryStatus::Killed => "killed".to_string(),
812    }
813}
814
815fn live_duration_ms(info: &linkrs_query::query_manager::QueryInfo) -> i64 {
816    if let Some(elapsed) = info.duration_ms {
817        return elapsed;
818    }
819    std::time::SystemTime::now()
820        .duration_since(info.start_time)
821        .map(|elapsed| elapsed.as_millis() as i64)
822        .unwrap_or(0)
823}