1use serde::{Deserialize, Serialize};
31use tokio_postgres::{Client, SimpleQueryMessage};
32use uuid::Uuid;
33
34use crate::PgError;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ColumnMeta {
38 pub name: String,
39 pub type_oid: u32,
46 pub type_name: String,
51}
52
53#[derive(Debug, Clone)]
57pub struct ActiveCursor {
58 pub cursor_id: String,
59 pub column_count: usize,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ExecutionOutcome {
64 pub columns: Vec<ColumnMeta>,
66 pub rows: Vec<Vec<Option<String>>>,
69 pub rows_affected: Option<u64>,
72 pub cursor_id: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PageResult {
80 pub rows: Vec<Vec<Option<String>>>,
81 pub has_more: bool,
84}
85
86pub(crate) fn is_multi_statement(sql: &str) -> bool {
98 top_level_semicolons(sql)
99 .into_iter()
100 .any(|idx| !is_effectively_empty(&sql[idx + 1..]))
101}
102
103pub(crate) fn split_at_last_statement(sql: &str) -> Option<(&str, &str)> {
114 let split = top_level_semicolons(sql).into_iter().last()?;
115 let main = &sql[split + 1..];
116 if is_effectively_empty(main) {
117 return None;
121 }
122 let main = main.trim();
123 let preamble = &sql[..split + 1]; if is_multi_statement(main) {
128 return None;
129 }
130 Some((preamble, main))
131}
132
133fn top_level_semicolons(sql: &str) -> Vec<usize> {
134 enum LexState<'a> {
135 Normal,
136 SingleQuote,
137 DoubleQuote,
138 LineComment,
139 BlockComment,
140 DollarQuote(&'a str),
141 }
142
143 let bytes = sql.as_bytes();
144 let mut positions = Vec::new();
145 let mut i = 0usize;
146 let mut state = LexState::Normal;
147 while i < bytes.len() {
148 let c = bytes[i];
149 match state {
150 LexState::Normal => match c {
151 b'\'' => state = LexState::SingleQuote,
152 b'"' => state = LexState::DoubleQuote,
153 b'-' if i + 1 < bytes.len() && bytes[i + 1] == b'-' => {
154 state = LexState::LineComment;
155 i += 1;
156 }
157 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
158 state = LexState::BlockComment;
159 i += 1;
160 }
161 b'$' => {
162 if let Some(delimiter) = dollar_quote_delimiter_at(sql, i) {
163 state = LexState::DollarQuote(delimiter);
164 i += delimiter.len() - 1;
165 }
166 }
167 b';' => positions.push(i),
168 _ => {}
169 },
170 LexState::SingleQuote => {
171 if c == b'\'' {
172 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
173 i += 1;
174 } else {
175 state = LexState::Normal;
176 }
177 }
178 }
179 LexState::DoubleQuote => {
180 if c == b'"' {
181 if i + 1 < bytes.len() && bytes[i + 1] == b'"' {
182 i += 1;
183 } else {
184 state = LexState::Normal;
185 }
186 }
187 }
188 LexState::LineComment => {
189 if c == b'\n' {
190 state = LexState::Normal;
191 }
192 }
193 LexState::BlockComment => {
194 if c == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
195 state = LexState::Normal;
196 i += 1;
197 }
198 }
199 LexState::DollarQuote(delimiter) => {
200 if sql[i..].starts_with(delimiter) {
201 state = LexState::Normal;
202 i += delimiter.len() - 1;
203 }
204 }
205 }
206 i += 1;
207 }
208 positions
209}
210
211fn dollar_quote_delimiter_at(sql: &str, start: usize) -> Option<&str> {
212 let bytes = sql.as_bytes();
213 if bytes.get(start) != Some(&b'$') {
214 return None;
215 }
216 let mut end = start + 1;
217 match bytes.get(end) {
218 Some(b'$') => return Some(&sql[start..=end]),
219 Some(b) if b.is_ascii_alphabetic() || *b == b'_' => end += 1,
220 _ => return None,
221 }
222 while let Some(b) = bytes.get(end)
223 && (b.is_ascii_alphanumeric() || *b == b'_')
224 {
225 end += 1;
226 }
227 if bytes.get(end) == Some(&b'$') {
228 Some(&sql[start..=end])
229 } else {
230 None
231 }
232}
233
234fn is_effectively_empty(sql: &str) -> bool {
238 enum LexState {
239 Normal,
240 LineComment,
241 BlockComment,
242 }
243 let bytes = sql.as_bytes();
244 let mut i = 0usize;
245 let mut state = LexState::Normal;
246 while i < bytes.len() {
247 let c = bytes[i];
248 match state {
249 LexState::Normal => match c {
250 b' ' | b'\t' | b'\n' | b'\r' => {}
251 b'-' if i + 1 < bytes.len() && bytes[i + 1] == b'-' => {
252 state = LexState::LineComment;
253 i += 1;
254 }
255 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
256 state = LexState::BlockComment;
257 i += 1;
258 }
259 _ => return false,
260 },
261 LexState::LineComment => {
262 if c == b'\n' {
263 state = LexState::Normal;
264 }
265 }
266 LexState::BlockComment => {
267 if c == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
268 state = LexState::Normal;
269 i += 1;
270 }
271 }
272 }
273 i += 1;
274 }
275 true
276}
277
278async fn run_multi_statement(
286 client: &Client,
287 sql: &str,
288 page_size: usize,
289) -> Result<ExecutionOutcome, PgError> {
290 let stream = client.simple_query(sql).await.map_err(PgError::Driver)?;
291
292 let mut current_columns: Vec<ColumnMeta> = vec![];
293 let mut current_rows: Vec<Vec<Option<String>>> = vec![];
294 let mut last_command_complete: Option<u64> = None;
295
296 for msg in stream {
297 match msg {
298 SimpleQueryMessage::RowDescription(desc) => {
299 current_columns = desc
309 .iter()
310 .map(|c| ColumnMeta {
311 name: c.name().to_string(),
312 type_oid: 0,
313 type_name: String::new(),
314 })
315 .collect();
316 current_rows.clear();
317 }
318 SimpleQueryMessage::Row(row) => {
319 if current_rows.len() >= page_size {
320 continue;
324 }
325 let width = current_columns.len();
326 let mut cells = Vec::with_capacity(width);
327 for idx in 0..width {
328 cells.push(row.get(idx).map(str::to_string));
329 }
330 current_rows.push(cells);
331 }
332 SimpleQueryMessage::CommandComplete(n) => {
333 last_command_complete = Some(n);
334 }
335 _ => {}
336 }
337 }
338
339 Ok(ExecutionOutcome {
340 columns: current_columns,
341 rows: current_rows,
342 rows_affected: last_command_complete,
343 cursor_id: None,
344 })
345}
346
347pub async fn open_query(
358 client: &Client,
359 sql: &str,
360 page_size: usize,
361 previous: Option<ActiveCursor>,
362) -> Result<(ExecutionOutcome, Option<ActiveCursor>), PgError> {
363 if let Some(prev) = previous.as_ref() {
368 let cleanup = format!("CLOSE {}; COMMIT", prev.cursor_id);
369 if let Err(e) = client.batch_execute(&cleanup).await {
370 tracing::debug!(
371 cursor = %prev.cursor_id,
372 error = %e,
373 "previous cursor cleanup failed; continuing with ROLLBACK"
374 );
375 let _ = client.batch_execute("ROLLBACK").await;
377 }
378 }
379
380 if is_multi_statement(sql) {
392 if let Some((preamble, main)) = split_at_last_statement(sql) {
393 client
397 .batch_execute(preamble)
398 .await
399 .map_err(PgError::Driver)?;
400 return Box::pin(open_query(client, main, page_size, None)).await;
404 }
405 let outcome = run_multi_statement(client, sql, page_size).await?;
406 return Ok((outcome, None));
407 }
408
409 let stmt = client.prepare(sql).await.map_err(PgError::Driver)?;
422 let columns: Vec<ColumnMeta> = stmt
423 .columns()
424 .iter()
425 .map(|c| ColumnMeta {
426 name: c.name().to_string(),
427 type_oid: c.type_().oid(),
428 type_name: c.type_().name().to_string(),
429 })
430 .collect();
431
432 if columns.is_empty() {
433 let stream = client.simple_query(sql).await.map_err(PgError::Driver)?;
435 let rows_affected = stream.into_iter().find_map(|m| match m {
436 SimpleQueryMessage::CommandComplete(n) => Some(n),
437 _ => None,
438 });
439 return Ok((
440 ExecutionOutcome {
441 columns: vec![],
442 rows: vec![],
443 rows_affected,
444 cursor_id: None,
445 },
446 None,
447 ));
448 }
449
450 let cursor_id = format!("c_{}", Uuid::new_v4().simple());
451
452 let begin = format!("BEGIN; DECLARE {} NO SCROLL CURSOR FOR {}", cursor_id, sql);
456 if let Err(e) = client.batch_execute(&begin).await {
457 let _ = client.batch_execute("ROLLBACK").await;
459 return Err(PgError::Driver(e));
460 }
461
462 let fetch_sql = format!("FETCH FORWARD {} FROM {}", page_size, cursor_id);
464 let stream = match client.simple_query(&fetch_sql).await {
465 Ok(s) => s,
466 Err(e) => {
467 let _ = client.batch_execute("ROLLBACK").await;
468 return Err(PgError::Driver(e));
469 }
470 };
471
472 let (rows, fetched) = collect_rows(stream, columns.len());
473
474 if fetched < page_size {
478 let _ = client
479 .batch_execute(&format!("CLOSE {}; COMMIT", cursor_id))
480 .await;
481 return Ok((
482 ExecutionOutcome {
483 columns,
484 rows,
485 rows_affected: Some(fetched as u64),
486 cursor_id: None,
487 },
488 None,
489 ));
490 }
491
492 let active = ActiveCursor {
495 cursor_id: cursor_id.clone(),
496 column_count: columns.len(),
497 };
498 Ok((
499 ExecutionOutcome {
500 columns,
501 rows,
502 rows_affected: Some(fetched as u64),
503 cursor_id: Some(cursor_id),
504 },
505 Some(active),
506 ))
507}
508
509pub async fn fetch_page(
511 client: &Client,
512 cursor: &ActiveCursor,
513 count: usize,
514) -> Result<PageResult, PgError> {
515 let sql = format!("FETCH FORWARD {} FROM {}", count, cursor.cursor_id);
516 let stream = client.simple_query(&sql).await.map_err(PgError::Driver)?;
517 let (rows, fetched) = collect_rows(stream, cursor.column_count);
518 Ok(PageResult {
519 rows,
520 has_more: fetched == count,
521 })
522}
523
524pub async fn close_query(client: &Client, cursor: &ActiveCursor) {
528 let sql = format!("CLOSE {}; COMMIT", cursor.cursor_id);
529 if let Err(e) = client.batch_execute(&sql).await {
530 tracing::debug!(
531 cursor = %cursor.cursor_id,
532 error = %e,
533 "cursor close failed; session likely already broken"
534 );
535 let _ = client.batch_execute("ROLLBACK").await;
537 }
538}
539
540fn collect_rows(
544 stream: Vec<SimpleQueryMessage>,
545 width: usize,
546) -> (Vec<Vec<Option<String>>>, usize) {
547 let mut rows: Vec<Vec<Option<String>>> = Vec::new();
548 let mut fetched = 0usize;
549 for msg in stream {
550 match msg {
551 SimpleQueryMessage::Row(row) => {
552 let mut values: Vec<Option<String>> = Vec::with_capacity(width);
553 for idx in 0..width {
554 values.push(row.get(idx).map(str::to_string));
555 }
556 rows.push(values);
557 }
558 SimpleQueryMessage::CommandComplete(n) => {
559 fetched = n as usize;
560 }
561 _ => {}
562 }
563 }
564 (rows, fetched)
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use proptest::prelude::*;
571
572 #[test]
573 fn execution_outcome_round_trips() {
574 let out = ExecutionOutcome {
575 columns: vec![ColumnMeta {
576 name: "id".to_string(),
577 type_oid: 23, type_name: "int4".to_string(),
579 }],
580 rows: vec![vec![Some("1".to_string())], vec![None]],
581 rows_affected: Some(2),
582 cursor_id: Some("c_abc".to_string()),
583 };
584 let json = serde_json::to_string(&out).expect("serialize");
585 let back: ExecutionOutcome = serde_json::from_str(&json).expect("deserialize");
586 assert_eq!(back.columns.len(), 1);
587 assert_eq!(back.columns[0].type_oid, 23);
588 assert_eq!(back.columns[0].type_name, "int4");
589 assert_eq!(back.rows.len(), 2);
590 assert_eq!(back.cursor_id.as_deref(), Some("c_abc"));
591 }
592
593 #[test]
594 fn page_result_round_trips() {
595 let p = PageResult {
596 rows: vec![vec![Some("x".into())]],
597 has_more: false,
598 };
599 let json = serde_json::to_string(&p).expect("serialize");
600 let back: PageResult = serde_json::from_str(&json).expect("deserialize");
601 assert_eq!(back.rows.len(), 1);
602 assert!(!back.has_more);
603 }
604
605 #[test]
606 fn multi_statement_detector_handles_common_cases() {
607 assert!(!is_multi_statement("SELECT 1"));
609 assert!(!is_multi_statement("SELECT 1;"));
611 assert!(!is_multi_statement("SELECT 1;\n"));
612 assert!(!is_multi_statement("SELECT 1;\n \n"));
613 assert!(is_multi_statement("SET x = 1; SELECT 1"));
615 assert!(is_multi_statement("BEGIN; UPDATE t SET v=1; COMMIT;"));
616 }
617
618 #[test]
619 fn multi_statement_detector_ignores_semicolons_in_strings() {
620 assert!(!is_multi_statement("SELECT 'hello; world'"));
622 assert!(!is_multi_statement("INSERT INTO t VALUES ('a;b;c')"));
623 assert!(!is_multi_statement("SELECT 'it''s; fine'"));
625 assert!(is_multi_statement("SELECT 'a;b'; SELECT 1"));
627 }
628
629 #[test]
630 fn multi_statement_detector_ignores_semicolons_in_identifiers() {
631 assert!(!is_multi_statement("SELECT \"weird;name\" FROM t"));
633 assert!(is_multi_statement("SELECT \"col\"; SELECT 1"));
634 }
635
636 #[test]
637 fn multi_statement_detector_ignores_semicolons_in_comments() {
638 assert!(!is_multi_statement("SELECT 1 -- ; comment\n"));
640 assert!(!is_multi_statement("SELECT 1 /* ; */ FROM t"));
642 assert!(!is_multi_statement("/* ; */ SELECT 1"));
643 assert!(!is_multi_statement("SELECT 1; -- trailing comment\n"));
644 assert!(is_multi_statement("SELECT 1 -- ;\n; SELECT 2"));
646 }
647
648 #[test]
649 fn multi_statement_detector_ignores_semicolons_in_dollar_quotes() {
650 assert!(!is_multi_statement("SELECT $$hello; world$$"));
651 assert!(!is_multi_statement("SELECT $tag$hello; world$tag$"));
652 assert!(is_multi_statement("SELECT $$hello; world$$; SELECT 1"));
653 }
654
655 #[test]
656 fn smart_split_returns_preamble_and_main() {
657 let (pre, main) = split_at_last_statement("SET x = 1; SELECT * FROM t").expect("split");
658 assert_eq!(pre, "SET x = 1;");
659 assert_eq!(main, "SELECT * FROM t");
660 }
661
662 #[test]
663 fn smart_split_handles_multiple_preamble_statements() {
664 let (pre, main) = split_at_last_statement("SET x = 1; SET y = 2; SELECT 1").expect("split");
665 assert_eq!(pre, "SET x = 1; SET y = 2;");
666 assert_eq!(main, "SELECT 1");
667 }
668
669 #[test]
670 fn smart_split_returns_none_when_no_main_statement_after_delimiter() {
671 assert!(split_at_last_statement("SET x = 1; SELECT 1;").is_none());
674 assert!(split_at_last_statement("SET x = 1;").is_none());
675 assert!(split_at_last_statement("SET x = 1; -- trailing\n").is_none());
677 }
678
679 #[test]
680 fn smart_split_ignores_in_string_semicolons() {
681 let (pre, main) = split_at_last_statement("SET x = 'a;b'; SELECT 1").expect("split");
682 assert_eq!(pre, "SET x = 'a;b';");
683 assert_eq!(main, "SELECT 1");
684 }
685
686 #[test]
687 fn smart_split_ignores_dollar_quoted_semicolons() {
688 let (pre, main) =
689 split_at_last_statement("SELECT $body$a;b$body$; SELECT 1").expect("split");
690 assert_eq!(pre, "SELECT $body$a;b$body$;");
691 assert_eq!(main, "SELECT 1");
692
693 let function = "CREATE FUNCTION f() RETURNS int AS $$ BEGIN; RETURN 1; END; $$ LANGUAGE plpgsql; SELECT f()";
694 let (pre, main) = split_at_last_statement(function).expect("split");
695 assert_eq!(
696 pre,
697 "CREATE FUNCTION f() RETURNS int AS $$ BEGIN; RETURN 1; END; $$ LANGUAGE plpgsql;"
698 );
699 assert_eq!(main, "SELECT f()");
700 }
701
702 #[test]
703 fn smart_split_returns_none_for_single_statement() {
704 assert!(split_at_last_statement("SELECT 1").is_none());
705 assert!(split_at_last_statement("SELECT 1;").is_none());
706 }
707
708 proptest! {
709 #[test]
710 fn dollar_quoted_semicolons_do_not_create_false_splits(body in "[A-Za-z0-9_ ;,()\\n]{0,128}") {
711 let sql = format!("SELECT $$ {body} $$");
712 prop_assert!(!is_multi_statement(&sql));
713 prop_assert!(split_at_last_statement(&sql).is_none());
714 }
715
716 #[test]
717 fn trailing_comment_after_semicolon_is_still_single_statement(comment in "[A-Za-z0-9_ ;,()]{0,128}") {
718 let sql = format!("SELECT 1; -- {comment}");
719 prop_assert!(!is_multi_statement(&sql));
720 prop_assert!(split_at_last_statement(&sql).is_none());
721 }
722 }
723}