systemprompt_database/services/
executor.rs1use super::database::Database;
7use super::provider::DatabaseProvider;
8use crate::error::{DatabaseResult, RepositoryError};
9use crate::models::QueryResult;
10
11#[derive(Debug, Copy, Clone)]
12pub struct SqlExecutor;
13
14enum SplitState {
15 Normal,
16 SingleQuote,
17 DollarQuote(String),
18 LineComment,
19 BlockComment(u32),
20}
21
22fn dollar_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
23 debug_assert_eq!(bytes[start], b'$');
24 let mut j = start + 1;
25 while j < bytes.len() {
26 let c = bytes[j];
27 if c == b'$' {
28 return Some(j);
29 }
30 if !(c.is_ascii_alphanumeric() || c == b'_') {
31 return None;
32 }
33 j += 1;
34 }
35 None
36}
37
38struct Splitter<'a> {
39 sql: &'a str,
40 bytes: &'a [u8],
41 i: usize,
42 start: usize,
43 has_content: bool,
44 statements: Vec<String>,
45}
46
47impl<'a> Splitter<'a> {
48 const fn new(sql: &'a str) -> Self {
49 Self {
50 sql,
51 bytes: sql.as_bytes(),
52 i: 0,
53 start: 0,
54 has_content: false,
55 statements: Vec::new(),
56 }
57 }
58
59 fn emit(&mut self, end: usize) {
60 if self.has_content {
61 let stmt = self.sql[self.start..end].trim();
62 if !stmt.is_empty() {
63 self.statements.push(stmt.to_owned());
64 }
65 }
66 self.has_content = false;
67 }
68
69 fn step_normal(&mut self) -> SplitState {
70 match self.bytes[self.i] {
71 b'\'' => {
72 self.has_content = true;
73 self.i += 1;
74 SplitState::SingleQuote
75 },
76 b'-' if self.bytes.get(self.i + 1) == Some(&b'-') => {
77 self.i += 2;
78 SplitState::LineComment
79 },
80 b'/' if self.bytes.get(self.i + 1) == Some(&b'*') => {
81 self.i += 2;
82 SplitState::BlockComment(1)
83 },
84 b'$' => {
85 self.has_content = true;
86 if let Some(tag_end) = dollar_tag_end(self.bytes, self.i) {
87 let tag = self.sql[self.i..=tag_end].to_string();
88 self.i = tag_end + 1;
89 SplitState::DollarQuote(tag)
90 } else {
91 self.i += 1;
92 SplitState::Normal
93 }
94 },
95 b';' => {
96 self.emit(self.i);
97 self.i += 1;
98 self.start = self.i;
99 SplitState::Normal
100 },
101 b => {
102 if !b.is_ascii_whitespace() {
103 self.has_content = true;
104 }
105 self.i += 1;
106 SplitState::Normal
107 },
108 }
109 }
110
111 fn step_single_quote(&mut self) -> SplitState {
112 if self.bytes[self.i] == b'\'' {
113 if self.bytes.get(self.i + 1) == Some(&b'\'') {
114 self.i += 2;
115 SplitState::SingleQuote
116 } else {
117 self.i += 1;
118 SplitState::Normal
119 }
120 } else {
121 self.i += 1;
122 SplitState::SingleQuote
123 }
124 }
125
126 fn step_dollar_quote(&mut self, tag: String) -> SplitState {
127 let tag_bytes = tag.as_bytes();
128 if self.i + tag_bytes.len() <= self.bytes.len()
129 && self.bytes[self.i..self.i + tag_bytes.len()] == *tag_bytes
130 {
131 self.i += tag_bytes.len();
132 SplitState::Normal
133 } else {
134 self.i += 1;
135 SplitState::DollarQuote(tag)
136 }
137 }
138
139 const fn step_line_comment(&mut self) -> SplitState {
140 let next = if self.bytes[self.i] == b'\n' {
141 SplitState::Normal
142 } else {
143 SplitState::LineComment
144 };
145 self.i += 1;
146 next
147 }
148
149 fn step_block_comment(&mut self, depth: u32) -> SplitState {
150 if self.bytes[self.i] == b'/' && self.bytes.get(self.i + 1) == Some(&b'*') {
151 self.i += 2;
152 SplitState::BlockComment(depth + 1)
153 } else if self.bytes[self.i] == b'*' && self.bytes.get(self.i + 1) == Some(&b'/') {
154 self.i += 2;
155 if depth == 1 {
156 SplitState::Normal
157 } else {
158 SplitState::BlockComment(depth - 1)
159 }
160 } else {
161 self.i += 1;
162 SplitState::BlockComment(depth)
163 }
164 }
165
166 fn run(mut self) -> DatabaseResult<Vec<String>> {
167 let mut state = SplitState::Normal;
168 while self.i < self.bytes.len() {
169 state = match state {
170 SplitState::Normal => self.step_normal(),
171 SplitState::SingleQuote => self.step_single_quote(),
172 SplitState::DollarQuote(tag) => self.step_dollar_quote(tag),
173 SplitState::LineComment => self.step_line_comment(),
174 SplitState::BlockComment(depth) => self.step_block_comment(depth),
175 };
176 }
177
178 match state {
179 SplitState::Normal | SplitState::LineComment => {
180 let end = self.sql.len();
181 self.emit(end);
182 Ok(self.statements)
183 },
184 SplitState::SingleQuote => Err(RepositoryError::Internal(
185 "Unterminated string literal in SQL".into(),
186 )),
187 SplitState::DollarQuote(tag) => Err(RepositoryError::Internal(format!(
188 "Unterminated dollar-quoted string: {tag}"
189 ))),
190 SplitState::BlockComment(_) => Err(RepositoryError::Internal(
191 "Unterminated block comment in SQL".into(),
192 )),
193 }
194 }
195}
196
197impl SqlExecutor {
198 pub async fn execute_statements(db: &Database, sql: &str) -> DatabaseResult<()> {
199 db.execute_batch(sql).await.map_err(|e| {
200 RepositoryError::Internal(format!("Failed to execute SQL statements: {e}"))
201 })
202 }
203
204 pub async fn execute_statements_parsed(
205 db: &dyn DatabaseProvider,
206 sql: &str,
207 ) -> DatabaseResult<()> {
208 let statements = Self::parse_sql_statements(sql)?;
209
210 for statement in statements {
211 db.execute_raw(&statement).await.map_err(|e| {
212 RepositoryError::Internal(format!(
213 "Failed to execute SQL statement: {statement}: {e}"
214 ))
215 })?;
216 }
217
218 Ok(())
219 }
220
221 pub fn parse_sql_statements(sql: &str) -> DatabaseResult<Vec<String>> {
222 Splitter::new(sql).run()
223 }
224
225 pub async fn execute_query(db: &Database, query: &str) -> DatabaseResult<QueryResult> {
226 db.query_raw(&query)
227 .await
228 .map_err(|e| RepositoryError::QueryExecution(Box::new(e)))
229 }
230
231 pub async fn execute_file(db: &Database, file_path: &str) -> DatabaseResult<()> {
232 let sql = std::fs::read_to_string(file_path).map_err(|e| {
233 RepositoryError::Internal(format!("Failed to read SQL file: {file_path}: {e}"))
234 })?;
235 Self::execute_statements(db, &sql).await
236 }
237
238 pub async fn table_exists(db: &Database, table_name: &str) -> DatabaseResult<bool> {
239 let result = db
240 .query_raw_with(
241 &"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = \
242 'public' AND table_name = $1) as exists",
243 &[&table_name],
244 )
245 .await?;
246
247 result
248 .first()
249 .and_then(|row| row.get("exists"))
250 .and_then(serde_json::Value::as_bool)
251 .ok_or_else(|| RepositoryError::Internal("Failed to check table existence".to_owned()))
252 }
253
254 pub async fn column_exists(
255 db: &Database,
256 table_name: &str,
257 column_name: &str,
258 ) -> DatabaseResult<bool> {
259 let result = db
260 .query_raw_with(
261 &"SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = \
262 'public' AND table_name = $1 AND column_name = $2) as exists",
263 &[&table_name, &column_name],
264 )
265 .await?;
266
267 result
268 .first()
269 .and_then(|row| row.get("exists"))
270 .and_then(serde_json::Value::as_bool)
271 .ok_or_else(|| RepositoryError::Internal("Failed to check column existence".to_owned()))
272 }
273}