linkrs_server/graph_service/
query.rs1use 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 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 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 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 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 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 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 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 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 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 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}