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 if e.code() == Some(&tokio_postgres::error::SqlState::SYNTAX_ERROR) {
466 let outcome = run_multi_statement(client, sql, page_size).await?;
467 return Ok((outcome, None));
468 }
469 return Err(PgError::Driver(e));
470 }
471
472 let fetch_sql = format!("FETCH FORWARD {} FROM {}", page_size, cursor_id);
474 let stream = match client.simple_query(&fetch_sql).await {
475 Ok(s) => s,
476 Err(e) => {
477 let _ = client.batch_execute("ROLLBACK").await;
478 return Err(PgError::Driver(e));
479 }
480 };
481
482 let (rows, fetched) = collect_rows(stream, columns.len());
483
484 if fetched < page_size {
488 let _ = client
489 .batch_execute(&format!("CLOSE {}; COMMIT", cursor_id))
490 .await;
491 return Ok((
492 ExecutionOutcome {
493 columns,
494 rows,
495 rows_affected: Some(fetched as u64),
496 cursor_id: None,
497 },
498 None,
499 ));
500 }
501
502 let active = ActiveCursor {
505 cursor_id: cursor_id.clone(),
506 column_count: columns.len(),
507 };
508 Ok((
509 ExecutionOutcome {
510 columns,
511 rows,
512 rows_affected: Some(fetched as u64),
513 cursor_id: Some(cursor_id),
514 },
515 Some(active),
516 ))
517}
518
519pub async fn fetch_page(
521 client: &Client,
522 cursor: &ActiveCursor,
523 count: usize,
524) -> Result<PageResult, PgError> {
525 let sql = format!("FETCH FORWARD {} FROM {}", count, cursor.cursor_id);
526 let stream = client.simple_query(&sql).await.map_err(PgError::Driver)?;
527 let (rows, fetched) = collect_rows(stream, cursor.column_count);
528 Ok(PageResult {
529 rows,
530 has_more: fetched == count,
531 })
532}
533
534pub async fn close_query(client: &Client, cursor: &ActiveCursor) {
538 let sql = format!("CLOSE {}; COMMIT", cursor.cursor_id);
539 if let Err(e) = client.batch_execute(&sql).await {
540 tracing::debug!(
541 cursor = %cursor.cursor_id,
542 error = %e,
543 "cursor close failed; session likely already broken"
544 );
545 let _ = client.batch_execute("ROLLBACK").await;
547 }
548}
549
550fn collect_rows(
554 stream: Vec<SimpleQueryMessage>,
555 width: usize,
556) -> (Vec<Vec<Option<String>>>, usize) {
557 let mut rows: Vec<Vec<Option<String>>> = Vec::new();
558 let mut fetched = 0usize;
559 for msg in stream {
560 match msg {
561 SimpleQueryMessage::Row(row) => {
562 let mut values: Vec<Option<String>> = Vec::with_capacity(width);
563 for idx in 0..width {
564 values.push(row.get(idx).map(str::to_string));
565 }
566 rows.push(values);
567 }
568 SimpleQueryMessage::CommandComplete(n) => {
569 fetched = n as usize;
570 }
571 _ => {}
572 }
573 }
574 (rows, fetched)
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use proptest::prelude::*;
581
582 #[test]
583 fn execution_outcome_round_trips() {
584 let out = ExecutionOutcome {
585 columns: vec![ColumnMeta {
586 name: "id".to_string(),
587 type_oid: 23, type_name: "int4".to_string(),
589 }],
590 rows: vec![vec![Some("1".to_string())], vec![None]],
591 rows_affected: Some(2),
592 cursor_id: Some("c_abc".to_string()),
593 };
594 let json = serde_json::to_string(&out).expect("serialize");
595 let back: ExecutionOutcome = serde_json::from_str(&json).expect("deserialize");
596 assert_eq!(back.columns.len(), 1);
597 assert_eq!(back.columns[0].type_oid, 23);
598 assert_eq!(back.columns[0].type_name, "int4");
599 assert_eq!(back.rows.len(), 2);
600 assert_eq!(back.cursor_id.as_deref(), Some("c_abc"));
601 }
602
603 #[test]
604 fn page_result_round_trips() {
605 let p = PageResult {
606 rows: vec![vec![Some("x".into())]],
607 has_more: false,
608 };
609 let json = serde_json::to_string(&p).expect("serialize");
610 let back: PageResult = serde_json::from_str(&json).expect("deserialize");
611 assert_eq!(back.rows.len(), 1);
612 assert!(!back.has_more);
613 }
614
615 #[test]
616 fn multi_statement_detector_handles_common_cases() {
617 assert!(!is_multi_statement("SELECT 1"));
619 assert!(!is_multi_statement("SELECT 1;"));
621 assert!(!is_multi_statement("SELECT 1;\n"));
622 assert!(!is_multi_statement("SELECT 1;\n \n"));
623 assert!(is_multi_statement("SET x = 1; SELECT 1"));
625 assert!(is_multi_statement("BEGIN; UPDATE t SET v=1; COMMIT;"));
626 }
627
628 #[test]
629 fn multi_statement_detector_ignores_semicolons_in_strings() {
630 assert!(!is_multi_statement("SELECT 'hello; world'"));
632 assert!(!is_multi_statement("INSERT INTO t VALUES ('a;b;c')"));
633 assert!(!is_multi_statement("SELECT 'it''s; fine'"));
635 assert!(is_multi_statement("SELECT 'a;b'; SELECT 1"));
637 }
638
639 #[test]
640 fn multi_statement_detector_ignores_semicolons_in_identifiers() {
641 assert!(!is_multi_statement("SELECT \"weird;name\" FROM t"));
643 assert!(is_multi_statement("SELECT \"col\"; SELECT 1"));
644 }
645
646 #[test]
647 fn multi_statement_detector_ignores_semicolons_in_comments() {
648 assert!(!is_multi_statement("SELECT 1 -- ; comment\n"));
650 assert!(!is_multi_statement("SELECT 1 /* ; */ FROM t"));
652 assert!(!is_multi_statement("/* ; */ SELECT 1"));
653 assert!(!is_multi_statement("SELECT 1; -- trailing comment\n"));
654 assert!(is_multi_statement("SELECT 1 -- ;\n; SELECT 2"));
656 }
657
658 #[test]
659 fn multi_statement_detector_ignores_semicolons_in_dollar_quotes() {
660 assert!(!is_multi_statement("SELECT $$hello; world$$"));
661 assert!(!is_multi_statement("SELECT $tag$hello; world$tag$"));
662 assert!(is_multi_statement("SELECT $$hello; world$$; SELECT 1"));
663 }
664
665 #[test]
666 fn smart_split_returns_preamble_and_main() {
667 let (pre, main) = split_at_last_statement("SET x = 1; SELECT * FROM t").expect("split");
668 assert_eq!(pre, "SET x = 1;");
669 assert_eq!(main, "SELECT * FROM t");
670 }
671
672 #[test]
673 fn smart_split_handles_multiple_preamble_statements() {
674 let (pre, main) = split_at_last_statement("SET x = 1; SET y = 2; SELECT 1").expect("split");
675 assert_eq!(pre, "SET x = 1; SET y = 2;");
676 assert_eq!(main, "SELECT 1");
677 }
678
679 #[test]
680 fn smart_split_returns_none_when_no_main_statement_after_delimiter() {
681 assert!(split_at_last_statement("SET x = 1; SELECT 1;").is_none());
684 assert!(split_at_last_statement("SET x = 1;").is_none());
685 assert!(split_at_last_statement("SET x = 1; -- trailing\n").is_none());
687 }
688
689 #[test]
690 fn smart_split_ignores_in_string_semicolons() {
691 let (pre, main) = split_at_last_statement("SET x = 'a;b'; SELECT 1").expect("split");
692 assert_eq!(pre, "SET x = 'a;b';");
693 assert_eq!(main, "SELECT 1");
694 }
695
696 #[test]
697 fn smart_split_ignores_dollar_quoted_semicolons() {
698 let (pre, main) =
699 split_at_last_statement("SELECT $body$a;b$body$; SELECT 1").expect("split");
700 assert_eq!(pre, "SELECT $body$a;b$body$;");
701 assert_eq!(main, "SELECT 1");
702
703 let function = "CREATE FUNCTION f() RETURNS int AS $$ BEGIN; RETURN 1; END; $$ LANGUAGE plpgsql; SELECT f()";
704 let (pre, main) = split_at_last_statement(function).expect("split");
705 assert_eq!(
706 pre,
707 "CREATE FUNCTION f() RETURNS int AS $$ BEGIN; RETURN 1; END; $$ LANGUAGE plpgsql;"
708 );
709 assert_eq!(main, "SELECT f()");
710 }
711
712 #[test]
713 fn smart_split_returns_none_for_single_statement() {
714 assert!(split_at_last_statement("SELECT 1").is_none());
715 assert!(split_at_last_statement("SELECT 1;").is_none());
716 }
717
718 proptest! {
719 #[test]
720 fn dollar_quoted_semicolons_do_not_create_false_splits(body in "[A-Za-z0-9_ ;,()\\n]{0,128}") {
721 let sql = format!("SELECT $$ {body} $$");
722 prop_assert!(!is_multi_statement(&sql));
723 prop_assert!(split_at_last_statement(&sql).is_none());
724 }
725
726 #[test]
727 fn trailing_comment_after_semicolon_is_still_single_statement(comment in "[A-Za-z0-9_ ;,()]{0,128}") {
728 let sql = format!("SELECT 1; -- {comment}");
729 prop_assert!(!is_multi_statement(&sql));
730 prop_assert!(split_at_last_statement(&sql).is_none());
731 }
732 }
733}