1use std::collections::HashMap;
4use std::io::Read as _;
5use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
6use std::time::Duration;
7
8use chrono::{DateTime, TimeDelta, Utc};
9use serde_json::{json, Value};
10
11use crate::utils::secret::Secret;
12
13use super::error::{Error, Result};
14use super::row::{Column, Row};
15use super::transport::{request_id, Transport};
16
17const POLL_INTERVAL: Duration = Duration::from_secs(3);
19
20pub(crate) struct LoginTokens {
22 pub session_token: Secret,
24 pub master_token: Secret,
26 pub session_validity_secs: i64,
28 pub master_validity_secs: i64,
30}
31
32#[derive(Clone, Debug)]
34struct Tokens {
35 session_token: Secret,
36 master_token: Secret,
37 session_expires_at: DateTime<Utc>,
38 master_expires_at: DateTime<Utc>,
39}
40
41#[derive(Clone, Debug)]
50struct InFlight {
51 request_id: String,
52 sql: String,
53 session_token: Secret,
54}
55
56pub struct SnowflakeSession {
63 transport: Arc<Transport>,
64 tokens: StdMutex<Tokens>,
65 in_flight: Arc<StdMutex<Option<InFlight>>>,
70 query_timeout: Duration,
72}
73
74#[derive(Clone)]
83pub struct AbortHandle {
84 transport: Arc<Transport>,
85 in_flight: Arc<StdMutex<Option<InFlight>>>,
86}
87
88impl AbortHandle {
89 pub async fn abort(&self) -> Result<bool> {
101 let Some(target) = self.snapshot() else {
102 return Ok(false);
103 };
104 let rid = request_id();
105 let body = json!({ "sqlText": target.sql, "requestId": target.request_id });
106 match self
107 .transport
108 .post(
109 "queries/v1/abort-request",
110 &[("requestId", rid.as_str())],
111 &body,
112 Some(target.session_token.expose_secret()),
113 )
114 .await
115 {
116 Ok(_) => Ok(true),
117 Err(Error::Server { .. }) => Ok(false),
120 Err(e) => Err(e),
121 }
122 }
123
124 fn snapshot(&self) -> Option<InFlight> {
125 self.in_flight
126 .lock()
127 .unwrap_or_else(std::sync::PoisonError::into_inner)
128 .clone()
129 }
130
131 #[cfg(test)]
134 #[must_use]
135 pub(crate) fn noop_for_test() -> Self {
136 use url::Url;
137 let base = Url::parse("http://127.0.0.1/").unwrap_or_else(|_| unreachable!());
138 Self {
139 transport: Arc::new(
140 Transport::with_base_url(base, Duration::from_secs(1))
141 .unwrap_or_else(|_| unreachable!()),
142 ),
143 in_flight: Arc::new(StdMutex::new(None)),
144 }
145 }
146}
147
148impl SnowflakeSession {
149 pub(crate) fn new(
151 transport: Arc<Transport>,
152 tokens: LoginTokens,
153 query_timeout: Duration,
154 ) -> Self {
155 let now = Utc::now();
156 Self {
157 transport,
158 tokens: StdMutex::new(Tokens {
159 session_token: tokens.session_token,
160 master_token: tokens.master_token,
161 session_expires_at: now + TimeDelta::seconds(tokens.session_validity_secs),
162 master_expires_at: now + TimeDelta::seconds(tokens.master_validity_secs),
163 }),
164 in_flight: Arc::new(StdMutex::new(None)),
165 query_timeout,
166 }
167 }
168
169 #[must_use]
175 pub fn abort_handle(&self) -> AbortHandle {
176 AbortHandle {
177 transport: Arc::clone(&self.transport),
178 in_flight: Arc::clone(&self.in_flight),
179 }
180 }
181
182 #[must_use]
184 pub fn session_expiring_within(&self, within: TimeDelta) -> bool {
185 Utc::now() + within >= self.lock().session_expires_at
186 }
187
188 #[must_use]
191 pub fn master_expires_at(&self) -> DateTime<Utc> {
192 self.lock().master_expires_at
193 }
194
195 pub async fn query(&self, sql: &str) -> Result<Vec<Row>> {
202 let token = self.lock().session_token.clone();
203 let rid = request_id();
204 let body = json!({ "sqlText": sql });
205 self.set_in_flight(InFlight {
208 request_id: rid.clone(),
209 sql: sql.to_string(),
210 session_token: token.clone(),
211 });
212 let result = self
216 .transport
217 .post_statement(
218 &[("requestId", rid.as_str())],
219 &body,
220 token.expose_secret(),
221 self.query_timeout,
222 POLL_INTERVAL,
223 )
224 .await;
225 self.clear_in_flight();
226 let data = result?;
227 self.rows_from_data(&data).await
228 }
229
230 pub async fn query_multi(&self, sql: &str) -> Result<Vec<Vec<Row>>> {
244 if count_statements(sql) <= 1 {
247 return Ok(vec![self.query(sql).await?]);
248 }
249
250 let token = self.lock().session_token.clone();
251 let rid = request_id();
252 let body = json!({ "sqlText": sql, "parameters": { "MULTI_STATEMENT_COUNT": 0 } });
256 self.set_in_flight(InFlight {
261 request_id: rid.clone(),
262 sql: sql.to_string(),
263 session_token: token.clone(),
264 });
265 let result = self
266 .transport
267 .post_statement(
268 &[("requestId", rid.as_str())],
269 &body,
270 token.expose_secret(),
271 self.query_timeout,
272 POLL_INTERVAL,
273 )
274 .await;
275 self.clear_in_flight();
276 let data = result?;
277
278 let Some(ids) = data
282 .get("resultIds")
283 .and_then(Value::as_str)
284 .filter(|s| !s.trim().is_empty())
285 else {
286 return Ok(vec![self.rows_from_data(&data).await?]);
287 };
288
289 let mut results = Vec::new();
290 for id in ids.split(',').map(str::trim).filter(|s| !s.is_empty()) {
291 let child = self
292 .transport
293 .get_statement_result(id, token.expose_secret(), self.query_timeout, POLL_INTERVAL)
294 .await?;
295 results.push(self.rows_from_data(&child).await?);
296 }
297 Ok(results)
298 }
299
300 async fn rows_from_data(&self, data: &Value) -> Result<Vec<Row>> {
303 let (columns, index, mut rows) = parse_result(data)?;
304 if let Some(chunks) = data.get("chunks").and_then(Value::as_array) {
305 let headers = parse_chunk_headers(data);
306 for chunk in chunks {
307 if let Some(url) = chunk.get("url").and_then(Value::as_str) {
308 let bytes = self.transport.get_bytes(url, &headers).await?;
309 rows.extend(decode_chunk_rows(&bytes, &columns, &index)?);
310 }
311 }
312 }
313 Ok(rows)
314 }
315
316 pub async fn renew(&self) -> Result<()> {
323 let (old_session, master) = {
324 let tokens = self.lock();
325 (tokens.session_token.clone(), tokens.master_token.clone())
326 };
327 let rid = request_id();
328 let body =
329 json!({ "oldSessionToken": old_session.expose_secret(), "requestType": "RENEW" });
330 let data = self
331 .transport
332 .post(
333 "session/token-request",
334 &[("requestId", rid.as_str())],
335 &body,
336 Some(master.expose_secret()),
337 )
338 .await?;
339
340 let new_session = data
341 .get("sessionToken")
342 .or_else(|| data.get("token"))
343 .and_then(Value::as_str)
344 .ok_or_else(|| Error::Protocol("token-request returned no sessionToken".into()))?
345 .to_string();
346 let st_validity = data
347 .get("validityInSecondsST")
348 .or_else(|| data.get("validityInSeconds"))
349 .and_then(Value::as_i64)
350 .unwrap_or(3600);
351
352 let mut tokens = self.lock();
353 tokens.session_token = new_session.into();
354 tokens.session_expires_at = Utc::now() + TimeDelta::seconds(st_validity);
355 if let Some(master_token) = data.get("masterToken").and_then(Value::as_str) {
356 tokens.master_token = master_token.into();
357 }
358 if let Some(mt_validity) = data.get("validityInSecondsMT").and_then(Value::as_i64) {
359 tokens.master_expires_at = Utc::now() + TimeDelta::seconds(mt_validity);
360 }
361 Ok(())
362 }
363
364 pub async fn heartbeat(&self) -> Result<()> {
371 let token = self.lock().session_token.clone();
372 let rid = request_id();
373 self.transport
374 .post(
375 "session/heartbeat",
376 &[("requestId", rid.as_str())],
377 &json!({}),
378 Some(token.expose_secret()),
379 )
380 .await?;
381 Ok(())
384 }
385
386 pub async fn close(&self) -> Result<()> {
392 let token = self.lock().session_token.clone();
393 let rid = request_id();
394 self.transport
395 .post(
396 "session",
397 &[("delete", "true"), ("requestId", rid.as_str())],
398 &json!({}),
399 Some(token.expose_secret()),
400 )
401 .await?;
402 Ok(())
403 }
404
405 fn lock(&self) -> MutexGuard<'_, Tokens> {
406 self.tokens
407 .lock()
408 .unwrap_or_else(std::sync::PoisonError::into_inner)
409 }
410
411 fn set_in_flight(&self, in_flight: InFlight) {
412 *self
413 .in_flight
414 .lock()
415 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(in_flight);
416 }
417
418 fn clear_in_flight(&self) {
419 *self
420 .in_flight
421 .lock()
422 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
423 }
424}
425
426type ParsedResult = (Arc<Vec<Column>>, Arc<HashMap<String, usize>>, Vec<Row>);
428
429fn parse_result(data: &Value) -> Result<ParsedResult> {
432 if let Some(format) = data.get("queryResultFormat").and_then(Value::as_str) {
433 if !format.eq_ignore_ascii_case("json") {
434 return Err(Error::Unsupported(format!(
435 "result format '{format}' (only JSON is supported)"
436 )));
437 }
438 }
439
440 let rowtype = data
441 .get("rowtype")
442 .and_then(Value::as_array)
443 .ok_or_else(|| Error::Protocol("query response missing rowtype".into()))?;
444 let columns: Arc<Vec<Column>> = Arc::new(rowtype.iter().map(parse_column).collect());
445
446 let mut index = HashMap::new();
447 for (i, col) in columns.iter().enumerate() {
448 index.entry(col.name.to_ascii_uppercase()).or_insert(i);
449 }
450 let index = Arc::new(index);
451
452 let rows = data
453 .get("rowset")
454 .and_then(Value::as_array)
455 .map(|rows| {
456 rows.iter()
457 .map(|row| row_from_cells(row, &columns, &index))
458 .collect()
459 })
460 .unwrap_or_default();
461 Ok((columns, index, rows))
462}
463
464fn row_from_cells(
466 row: &Value,
467 columns: &Arc<Vec<Column>>,
468 index: &Arc<HashMap<String, usize>>,
469) -> Row {
470 let cells = row
471 .as_array()
472 .map(|cells| cells.iter().map(cell_to_string).collect())
473 .unwrap_or_default();
474 Row::new(cells, Arc::clone(columns), Arc::clone(index))
475}
476
477fn parse_chunk_headers(data: &Value) -> Vec<(String, String)> {
479 data.get("chunkHeaders")
480 .and_then(Value::as_object)
481 .map(|headers| {
482 headers
483 .iter()
484 .filter_map(|(k, v)| v.as_str().map(|v| (k.clone(), v.to_string())))
485 .collect()
486 })
487 .unwrap_or_default()
488}
489
490fn decode_chunk_rows(
492 bytes: &[u8],
493 columns: &Arc<Vec<Column>>,
494 index: &Arc<HashMap<String, usize>>,
495) -> Result<Vec<Row>> {
496 let json = gunzip_if_needed(bytes)?;
497 let mut framed = Vec::with_capacity(json.len() + 2);
501 framed.push(b'[');
502 framed.extend_from_slice(&json);
503 framed.push(b']');
504 let rows: Vec<Value> = serde_json::from_slice(&framed)
505 .map_err(|e| Error::Protocol(format!("invalid result chunk JSON: {e}")))?;
506 Ok(rows
507 .iter()
508 .map(|row| row_from_cells(row, columns, index))
509 .collect())
510}
511
512fn gunzip_if_needed(bytes: &[u8]) -> Result<Vec<u8>> {
514 if bytes.starts_with(&[0x1f, 0x8b]) {
515 let mut decoder = flate2::read::GzDecoder::new(bytes);
516 let mut out = Vec::new();
517 decoder.read_to_end(&mut out).map_err(Error::Io)?;
518 Ok(out)
519 } else {
520 Ok(bytes.to_vec())
521 }
522}
523
524fn parse_column(value: &Value) -> Column {
526 Column {
527 name: value
528 .get("name")
529 .and_then(Value::as_str)
530 .unwrap_or_default()
531 .to_string(),
532 ty: value
533 .get("type")
534 .and_then(Value::as_str)
535 .unwrap_or_default()
536 .to_ascii_lowercase(),
537 nullable: value
538 .get("nullable")
539 .and_then(Value::as_bool)
540 .unwrap_or(true),
541 length: value.get("length").and_then(Value::as_i64),
542 precision: value.get("precision").and_then(Value::as_i64),
543 scale: value.get("scale").and_then(Value::as_i64),
544 }
545}
546
547fn cell_to_string(value: &Value) -> Option<String> {
549 match value {
550 Value::Null => None,
551 Value::String(s) => Some(s.clone()),
552 other => Some(other.to_string()),
553 }
554}
555
556#[derive(PartialEq, Eq)]
558enum ScanState {
559 Normal,
561 Single,
563 Double,
565 Line,
567 Block,
569 Dollar,
571}
572
573fn count_statements(sql: &str) -> usize {
583 let bytes = sql.as_bytes();
584 let mut state = ScanState::Normal;
585 let mut count = 0usize;
586 let mut has_content = false;
589 let mut i = 0;
590 while i < bytes.len() {
591 let c = bytes[i];
592 let next = bytes.get(i + 1).copied();
593 match state {
594 ScanState::Normal => match c {
595 b'\'' => {
596 state = ScanState::Single;
597 has_content = true;
598 }
599 b'"' => {
600 state = ScanState::Double;
601 has_content = true;
602 }
603 b'-' if next == Some(b'-') => {
604 state = ScanState::Line;
605 i += 1;
606 }
607 b'/' if next == Some(b'*') => {
608 state = ScanState::Block;
609 i += 1;
610 }
611 b'$' if next == Some(b'$') => {
612 state = ScanState::Dollar;
613 has_content = true;
614 i += 1;
615 }
616 b';' => {
617 if has_content {
618 count += 1;
619 }
620 has_content = false;
621 }
622 _ => {
623 if !c.is_ascii_whitespace() {
624 has_content = true;
625 }
626 }
627 },
628 ScanState::Single => match c {
629 b'\'' => {
630 if next == Some(b'\'') {
631 i += 1; } else {
633 state = ScanState::Normal;
634 }
635 }
636 b'\\' => i += 1, _ => {}
638 },
639 ScanState::Double => {
640 if c == b'"' {
641 if next == Some(b'"') {
642 i += 1; } else {
644 state = ScanState::Normal;
645 }
646 }
647 }
648 ScanState::Line => {
649 if c == b'\n' {
650 state = ScanState::Normal;
651 }
652 }
653 ScanState::Block => {
654 if c == b'*' && next == Some(b'/') {
655 state = ScanState::Normal;
656 i += 1;
657 }
658 }
659 ScanState::Dollar => {
660 if c == b'$' && next == Some(b'$') {
661 state = ScanState::Normal;
662 i += 1;
663 }
664 }
665 }
666 i += 1;
667 }
668 if has_content {
669 count += 1;
670 }
671 count
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used, clippy::expect_used)]
676mod tests {
677 use super::*;
678 use crate::snowflake::client::row::rows_to_payload;
679 use url::Url;
680 use wiremock::matchers::{method, path};
681 use wiremock::{Mock, MockServer, ResponseTemplate};
682
683 fn live_session(server: &MockServer, session_validity_secs: i64) -> SnowflakeSession {
686 let base = Url::parse(&server.uri()).unwrap().join("/").unwrap();
687 let transport = Arc::new(Transport::with_base_url(base, Duration::from_secs(5)).unwrap());
688 SnowflakeSession::new(
689 transport,
690 LoginTokens {
691 session_token: "sess".into(),
692 master_token: "mast".into(),
693 session_validity_secs,
694 master_validity_secs: 14_400,
695 },
696 Duration::from_secs(5),
697 )
698 }
699
700 #[test]
701 fn token_accessors_reflect_validities() {
702 let base = Url::parse("https://acct.example/").unwrap();
704 let transport = Arc::new(Transport::with_base_url(base, Duration::from_secs(5)).unwrap());
705 let session = SnowflakeSession::new(
706 transport,
707 LoginTokens {
708 session_token: "s".into(),
709 master_token: "m".into(),
710 session_validity_secs: 3600,
711 master_validity_secs: 14_400,
712 },
713 Duration::from_secs(5),
714 );
715 assert!(session.master_expires_at() > Utc::now());
716 assert!(!session.session_expiring_within(TimeDelta::seconds(60)));
717 assert!(session.session_expiring_within(TimeDelta::seconds(7200)));
718 }
719
720 #[test]
721 fn tokens_debug_redacts_secrets() {
722 let tokens = Tokens {
723 session_token: "sekret-session-token".into(),
724 master_token: "sekret-master-token".into(),
725 session_expires_at: Utc::now(),
726 master_expires_at: Utc::now(),
727 };
728 let debug = format!("{tokens:?}");
730 assert!(
731 !debug.contains("sekret-session-token"),
732 "leaked session token: {debug}"
733 );
734 assert!(
735 !debug.contains("sekret-master-token"),
736 "leaked master token: {debug}"
737 );
738 assert!(debug.contains("session_token: <redacted>"));
739 assert!(debug.contains("master_token: <redacted>"));
740 }
741
742 #[tokio::test]
743 async fn query_returns_inline_rows() {
744 let server = MockServer::start().await;
745 Mock::given(method("POST"))
746 .and(path("/queries/v1/query-request"))
747 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
748 "success": true,
749 "data": {
750 "queryResultFormat": "json",
751 "rowtype": [{ "name": "N", "type": "fixed", "precision": 38, "scale": 0 }],
752 "rowset": [["1"], ["2"]],
753 }
754 })))
755 .mount(&server)
756 .await;
757
758 let rows = live_session(&server, 3600).query("SELECT 1").await.unwrap();
759 assert_eq!(rows.len(), 2);
760 }
761
762 #[tokio::test]
763 async fn query_downloads_and_appends_external_chunks() {
764 let server = MockServer::start().await;
765 let chunk_url = format!("{}/chunk0", server.uri());
766 Mock::given(method("POST"))
767 .and(path("/queries/v1/query-request"))
768 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
769 "success": true,
770 "data": {
771 "queryResultFormat": "json",
772 "rowtype": [{ "name": "N", "type": "text" }],
773 "rowset": [["a"]],
774 "chunks": [{ "url": chunk_url }],
775 }
776 })))
777 .mount(&server)
778 .await;
779 Mock::given(method("GET"))
781 .and(path("/chunk0"))
782 .respond_with(ResponseTemplate::new(200).set_body_bytes(br#"["b"],["c"]"#.to_vec()))
783 .mount(&server)
784 .await;
785
786 let rows = live_session(&server, 3600).query("SELECT 1").await.unwrap();
787 assert_eq!(rows.len(), 3, "1 inline + 2 chunked");
788 }
789
790 #[tokio::test]
791 async fn query_surfaces_session_expired() {
792 let server = MockServer::start().await;
793 Mock::given(method("POST"))
794 .and(path("/queries/v1/query-request"))
795 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
796 "success": false, "code": "390112", "message": "expired", "data": {}
797 })))
798 .mount(&server)
799 .await;
800
801 let err = live_session(&server, 3600)
802 .query("SELECT 1")
803 .await
804 .unwrap_err();
805 assert!(matches!(err, Error::SessionExpired));
806 }
807
808 #[tokio::test]
809 async fn abort_cancels_the_in_flight_statement_by_request_id() {
810 let server = MockServer::start().await;
811 Mock::given(method("POST"))
814 .and(path("/queries/v1/query-request"))
815 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
816 "success": true, "code": "333333", "data": { "getResultUrl": "/poll/1" }
817 })))
818 .mount(&server)
819 .await;
820 Mock::given(method("GET"))
821 .and(path("/poll/1"))
822 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
823 "success": true, "code": "333333", "data": {}
824 })))
825 .mount(&server)
826 .await;
827 Mock::given(method("POST"))
828 .and(path("/queries/v1/abort-request"))
829 .respond_with(
830 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
831 )
832 .mount(&server)
833 .await;
834
835 let session = Arc::new(live_session(&server, 3600));
836 let handle = session.abort_handle();
837
838 assert!(
840 !handle.abort().await.unwrap(),
841 "idle session has nothing to abort"
842 );
843
844 let query = {
846 let session = Arc::clone(&session);
847 tokio::spawn(async move { session.query("SELECT LONG").await })
848 };
849 tokio::time::sleep(Duration::from_millis(100)).await;
851 assert!(
852 handle.abort().await.unwrap(),
853 "a running statement is aborted"
854 );
855 query.abort();
856
857 let requests = server.received_requests().await.unwrap();
860 let query_rid = requests
861 .iter()
862 .find(|r| r.url.path() == "/queries/v1/query-request")
863 .and_then(|r| {
864 r.url
865 .query_pairs()
866 .find(|(k, _)| k == "requestId")
867 .map(|(_, v)| v.into_owned())
868 })
869 .expect("the query recorded its requestId");
870 let abort = requests
871 .iter()
872 .find(|r| r.url.path() == "/queries/v1/abort-request")
873 .expect("an abort request was sent");
874 let abort_rid = abort
875 .url
876 .query_pairs()
877 .find(|(k, _)| k == "requestId")
878 .map(|(_, v)| v.into_owned())
879 .unwrap_or_default();
880 let body: Value = serde_json::from_slice(&abort.body).unwrap();
881 assert_eq!(body["sqlText"], "SELECT LONG");
882 assert_eq!(body["requestId"], query_rid, "body targets the query's id");
883 assert_ne!(
884 abort_rid, query_rid,
885 "the abort call uses its own requestId"
886 );
887 }
888
889 #[tokio::test]
890 async fn abort_reports_nothing_to_cancel_on_a_server_rejection() {
891 let server = MockServer::start().await;
892 Mock::given(method("POST"))
893 .and(path("/queries/v1/query-request"))
894 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
895 "success": true, "code": "333333", "data": { "getResultUrl": "/poll/1" }
896 })))
897 .mount(&server)
898 .await;
899 Mock::given(method("GET"))
900 .and(path("/poll/1"))
901 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
902 "success": true, "code": "333333", "data": {}
903 })))
904 .mount(&server)
905 .await;
906 Mock::given(method("POST"))
908 .and(path("/queries/v1/abort-request"))
909 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
910 "success": false, "code": "000603", "message": "query not found"
911 })))
912 .mount(&server)
913 .await;
914
915 let session = Arc::new(live_session(&server, 3600));
916 let handle = session.abort_handle();
917 let query = {
918 let session = Arc::clone(&session);
919 tokio::spawn(async move { session.query("SELECT LONG").await })
920 };
921 tokio::time::sleep(Duration::from_millis(100)).await;
922 assert!(!handle.abort().await.unwrap());
924 query.abort();
925 }
926
927 #[tokio::test]
928 async fn renew_swaps_in_a_fresh_session_token() {
929 let server = MockServer::start().await;
930 Mock::given(method("POST"))
931 .and(path("/session/token-request"))
932 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
933 "success": true,
934 "data": { "sessionToken": "new-sess", "validityInSecondsST": 3600 }
935 })))
936 .mount(&server)
937 .await;
938
939 let session = live_session(&server, 1);
941 assert!(session.session_expiring_within(TimeDelta::seconds(120)));
942 session.renew().await.unwrap();
943 assert!(!session.session_expiring_within(TimeDelta::seconds(120)));
944 }
945
946 #[tokio::test]
947 async fn renew_errors_when_response_lacks_a_token() {
948 let server = MockServer::start().await;
949 Mock::given(method("POST"))
950 .and(path("/session/token-request"))
951 .respond_with(
952 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
953 )
954 .mount(&server)
955 .await;
956
957 let err = live_session(&server, 3600).renew().await.unwrap_err();
958 assert!(matches!(err, Error::Protocol(_)));
959 }
960
961 #[tokio::test]
962 async fn heartbeat_and_close_post_successfully() {
963 let server = MockServer::start().await;
964 Mock::given(method("POST"))
965 .and(path("/session/heartbeat"))
966 .respond_with(
967 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
968 )
969 .mount(&server)
970 .await;
971 Mock::given(method("POST"))
972 .and(path("/session"))
973 .respond_with(
974 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
975 )
976 .mount(&server)
977 .await;
978
979 let session = live_session(&server, 3600);
980 session.heartbeat().await.unwrap();
981 session.close().await.unwrap();
982 }
983
984 #[test]
985 fn parse_result_builds_columns_and_inline_rows() {
986 let data = json!({
987 "queryResultFormat": "json",
988 "rowtype": [
989 { "name": "ID", "type": "fixed", "nullable": false, "precision": 38, "scale": 0 },
990 { "name": "NAME", "type": "text", "nullable": true, "length": 16_777_216 },
991 ],
992 "rowset": [["1", "alice"], ["2", null]],
993 });
994 let (_columns, _index, rows) = parse_result(&data).unwrap();
995 assert_eq!(rows.len(), 2);
996 let payload = rows_to_payload(&rows);
997 assert_eq!(payload["columns"][0]["name"], "ID");
998 assert_eq!(payload["columns"][0]["type"], "fixed(38,0)");
999 assert_eq!(payload["rows"][0]["ID"], 1);
1000 assert_eq!(payload["rows"][0]["NAME"], "alice");
1001 assert_eq!(payload["rows"][1]["NAME"], Value::Null);
1002 }
1003
1004 #[test]
1005 fn parse_result_rejects_arrow_but_allows_chunked() {
1006 let arrow = json!({ "queryResultFormat": "arrow", "rowtype": [], "rowset": [] });
1007 assert!(matches!(parse_result(&arrow), Err(Error::Unsupported(_))));
1008 let chunked = json!({
1011 "queryResultFormat": "json",
1012 "rowtype": [{ "name": "C", "type": "text" }],
1013 "rowset": [["a"]],
1014 "chunks": [{ "url": "https://example/chunk0" }],
1015 });
1016 let (_c, _i, rows) = parse_result(&chunked).unwrap();
1017 assert_eq!(rows.len(), 1);
1018 }
1019
1020 #[test]
1021 fn parse_result_requires_rowtype() {
1022 assert!(matches!(
1023 parse_result(&json!({ "rowset": [] })),
1024 Err(Error::Protocol(_))
1025 ));
1026 }
1027
1028 #[test]
1029 fn decode_chunk_rows_handles_gzip_and_plain_json() {
1030 use std::io::Write as _;
1031 let (columns, index, _rows) = parse_result(&json!({
1032 "queryResultFormat": "json",
1033 "rowtype": [
1034 { "name": "ID", "type": "fixed", "precision": 38, "scale": 0 },
1035 { "name": "NAME", "type": "text" },
1036 ],
1037 "rowset": [],
1038 }))
1039 .unwrap();
1040
1041 let chunk_json = br#"["3","carol"],["4",null]"#;
1044
1045 let rows = decode_chunk_rows(chunk_json, &columns, &index).unwrap();
1047 assert_eq!(rows.len(), 2);
1048 assert_eq!(rows[0].to_json_object().get("NAME"), Some(&json!("carol")));
1049 assert_eq!(rows[1].to_json_object().get("NAME"), Some(&Value::Null));
1050
1051 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
1053 encoder.write_all(chunk_json).unwrap();
1054 let gzipped = encoder.finish().unwrap();
1055 let rows = decode_chunk_rows(&gzipped, &columns, &index).unwrap();
1056 assert_eq!(rows.len(), 2);
1057 assert_eq!(rows[0].to_json_object().get("ID"), Some(&json!(3)));
1058 }
1059
1060 #[test]
1061 fn parse_chunk_headers_reads_string_pairs() {
1062 let data = json!({ "chunkHeaders": { "x-amz-server-side-encryption": "AES256" } });
1063 let headers = parse_chunk_headers(&data);
1064 assert_eq!(
1065 headers,
1066 vec![(
1067 "x-amz-server-side-encryption".to_string(),
1068 "AES256".to_string()
1069 )]
1070 );
1071 }
1072
1073 #[test]
1074 fn count_statements_counts_top_level_separators() {
1075 assert_eq!(count_statements(""), 0);
1076 assert_eq!(count_statements(" \n "), 0);
1077 assert_eq!(count_statements(";;;"), 0, "empty statements don't count");
1078 assert_eq!(count_statements("SELECT 1"), 1);
1079 assert_eq!(count_statements("SELECT 1;"), 1, "trailing ; is not a stmt");
1080 assert_eq!(count_statements("SELECT 1; SELECT 2"), 2);
1081 assert_eq!(count_statements("SELECT 1; SELECT 2;"), 2);
1082 assert_eq!(
1083 count_statements("USE WAREHOUSE WH; USE ROLE R; SELECT 1"),
1084 3
1085 );
1086 }
1087
1088 #[test]
1089 fn count_statements_ignores_separators_in_literals_and_comments() {
1090 assert_eq!(count_statements("SELECT ';'"), 1);
1092 assert_eq!(count_statements("SELECT ';'; SELECT 2"), 2);
1093 assert_eq!(count_statements("SELECT 'it''s; ok'"), 1);
1095 assert_eq!(count_statements("SELECT 'it\\'s; ok'; SELECT 2"), 2);
1096 assert_eq!(count_statements("SELECT 1 AS \"a;b\""), 1);
1098 assert_eq!(count_statements("SELECT 1 -- a; b\n; SELECT 2"), 2);
1100 assert_eq!(count_statements("SELECT 1 /* a; b */; SELECT 2"), 2);
1101 assert_eq!(
1103 count_statements("CREATE PROC p() AS $$ BEGIN a; b; END $$; SELECT 1"),
1104 2
1105 );
1106 }
1107
1108 #[tokio::test]
1109 async fn query_multi_single_statement_returns_one_result_set() {
1110 let server = MockServer::start().await;
1111 Mock::given(method("POST"))
1112 .and(path("/queries/v1/query-request"))
1113 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1114 "success": true,
1115 "data": {
1116 "queryResultFormat": "json",
1117 "rowtype": [{ "name": "N", "type": "fixed", "precision": 38, "scale": 0 }],
1118 "rowset": [["1"], ["2"]],
1119 }
1120 })))
1121 .mount(&server)
1122 .await;
1123
1124 let results = live_session(&server, 3600)
1125 .query_multi("SELECT 1")
1126 .await
1127 .unwrap();
1128 assert_eq!(results.len(), 1, "one statement → one result set");
1129 assert_eq!(results[0].len(), 2);
1130 }
1131
1132 #[tokio::test]
1133 async fn query_multi_follows_result_ids_for_each_statement() {
1134 let server = MockServer::start().await;
1135 Mock::given(method("POST"))
1137 .and(path("/queries/v1/query-request"))
1138 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1139 "success": true,
1140 "data": { "resultIds": "id-a,id-b", "resultTypes": "1,1" }
1141 })))
1142 .mount(&server)
1143 .await;
1144 Mock::given(method("GET"))
1145 .and(path("/queries/id-a/result"))
1146 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1147 "success": true,
1148 "data": {
1149 "queryResultFormat": "json",
1150 "rowtype": [{ "name": "A", "type": "text" }],
1151 "rowset": [["x"]],
1152 }
1153 })))
1154 .mount(&server)
1155 .await;
1156 Mock::given(method("GET"))
1157 .and(path("/queries/id-b/result"))
1158 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1159 "success": true,
1160 "data": {
1161 "queryResultFormat": "json",
1162 "rowtype": [{ "name": "B", "type": "fixed", "precision": 38, "scale": 0 }],
1163 "rowset": [["1"], ["2"]],
1164 }
1165 })))
1166 .mount(&server)
1167 .await;
1168
1169 let results = live_session(&server, 3600)
1170 .query_multi("USE ROLE R; SELECT 2")
1171 .await
1172 .unwrap();
1173 assert_eq!(results.len(), 2, "two statements → two result sets");
1174 assert_eq!(results[0].len(), 1);
1175 assert_eq!(results[1].len(), 2);
1176 }
1177}