linkrs_api/api_core/
session_command.rs1use crate::api_core::error::{CoreError, ExtendedErrorCode};
10use linkrs_query::parser::ast::Stmt;
11use linkrs_query::parser::{Parser, ParserResult};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SessionCommand {
18 Begin,
19 Commit,
20 Rollback,
21 Savepoint,
22 ReleaseSavepoint,
23 AssignVariable,
24 Extension,
25}
26
27#[derive(Debug, Clone)]
30pub struct CommandParseError {
31 pub message: String,
32 pub offset: Option<usize>,
33 pub position: Option<linkrs_core::types::Position>,
34}
35
36impl std::fmt::Display for CommandParseError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "Parse error: {}", self.message)
39 }
40}
41
42impl From<CommandParseError> for CoreError {
45 fn from(error: CommandParseError) -> Self {
46 match (error.position, error.offset) {
47 (Some(position), offset) => CoreError::detailed_query_error_with_position(
48 error.message,
49 ExtendedErrorCode::SyntaxError,
50 offset,
51 Some(position),
52 ),
53 (None, Some(offset)) => CoreError::detailed_query_error(
54 error.message,
55 ExtendedErrorCode::SyntaxError,
56 Some(offset),
57 ),
58 (None, None) => CoreError::QueryExecutionFailed(error.message),
59 }
60 }
61}
62
63pub fn is_command_like(text: &str) -> bool {
67 let upper = text.trim().to_uppercase();
68 upper == "BEGIN"
69 || upper.starts_with("BEGIN ")
70 || upper.starts_with("COMMIT")
71 || upper.starts_with("ROLLBACK")
72 || upper.starts_with("SAVEPOINT")
73 || upper.starts_with("RELEASE SAVEPOINT")
74 || upper == "LET"
75 || upper.starts_with("LET ")
76 || upper.starts_with("LOAD EXTENSION")
77 || upper.starts_with("INSTALL EXTENSION")
78 || upper.starts_with("UPDATE EXTENSION")
79 || upper.starts_with("UNINSTALL EXTENSION")
80}
81
82pub fn classify_session_command(
90 text: &str,
91) -> Result<Option<(ParserResult, SessionCommand)>, CommandParseError> {
92 if !is_command_like(text) {
93 return Ok(None);
94 }
95 let mut parser = Parser::new(text);
96 match parser.parse() {
97 Ok(result) if !parser.has_errors() => match result.ast.stmt() {
98 Stmt::BeginTransaction(_) => Ok(Some((result, SessionCommand::Begin))),
99 Stmt::CommitTransaction(_) => Ok(Some((result, SessionCommand::Commit))),
100 Stmt::RollbackTransaction(_) => Ok(Some((result, SessionCommand::Rollback))),
101 Stmt::Savepoint(_) => Ok(Some((result, SessionCommand::Savepoint))),
102 Stmt::ReleaseSavepoint(_) => Ok(Some((result, SessionCommand::ReleaseSavepoint))),
103 Stmt::AssignVariable(_) => Ok(Some((result, SessionCommand::AssignVariable))),
104 Stmt::Extension(_) => Ok(Some((result, SessionCommand::Extension))),
105 _ => Ok(None),
106 },
107 Ok(_) => Ok(None),
108 Err(_) => match parser.errors().iter().next() {
109 Some(first) => Err(CommandParseError {
110 message: first.message.to_string(),
111 offset: first.offset,
112 position: first.position.is_valid().then_some(first.position),
113 }),
114 None => Ok(None),
115 },
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn kind_of(text: &str) -> Option<SessionCommand> {
124 classify_session_command(text)
125 .ok()
126 .flatten()
127 .map(|(_, k)| k)
128 }
129
130 #[test]
131 fn classifies_every_transaction_and_session_command() {
132 assert_eq!(kind_of("BEGIN"), Some(SessionCommand::Begin));
133 assert_eq!(kind_of("BEGIN TRANSACTION"), Some(SessionCommand::Begin));
134 assert_eq!(kind_of("BEGIN READ ONLY"), Some(SessionCommand::Begin));
135 assert_eq!(kind_of("COMMIT"), Some(SessionCommand::Commit));
136 assert_eq!(kind_of("ROLLBACK"), Some(SessionCommand::Rollback));
137 assert_eq!(kind_of("ROLLBACK TO sp1"), Some(SessionCommand::Rollback));
138 assert_eq!(kind_of("SAVEPOINT sp1"), Some(SessionCommand::Savepoint));
139 assert_eq!(
140 kind_of("RELEASE SAVEPOINT sp1"),
141 Some(SessionCommand::ReleaseSavepoint)
142 );
143 assert_eq!(kind_of("LET $x = 1"), Some(SessionCommand::AssignVariable));
144 assert_eq!(
145 kind_of("LOAD EXTENSION '/tmp/a.so'"),
146 Some(SessionCommand::Extension)
147 );
148 assert_eq!(
149 kind_of("INSTALL EXTENSION my_udf FROM '/tmp/a.so'"),
150 Some(SessionCommand::Extension)
151 );
152 assert_eq!(
153 kind_of("UNINSTALL EXTENSION my_udf"),
154 Some(SessionCommand::Extension)
155 );
156 assert_eq!(
157 kind_of("UPDATE EXTENSION my_udf"),
158 Some(SessionCommand::Extension)
159 );
160 }
161
162 #[test]
163 fn regular_statements_and_non_commands_pass_through() {
164 assert_eq!(kind_of("MATCH (n) RETURN n"), None);
165 assert_eq!(kind_of("BEGINNING OF TEXT"), None);
166 assert_eq!(kind_of(""), None);
167 assert_eq!(kind_of("COMMIT; MATCH (n) RETURN n"), None);
169 }
170
171 #[test]
172 fn malformed_command_reports_first_error_with_position() {
173 let error = classify_session_command("BEGIN").expect("bare BEGIN parses");
174 assert_eq!(error.map(|(_, kind)| kind), Some(SessionCommand::Begin));
175
176 let error = classify_session_command("SAVEPOINT").expect_err("savepoint needs a name");
177 assert!(!error.message.is_empty());
178 assert!(error.offset.is_some() || error.position.is_some());
179 }
180}