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 super::error::{Error, Result};
12use super::row::{Column, Row};
13use super::transport::{request_id, Transport};
14
15const POLL_INTERVAL: Duration = Duration::from_secs(3);
17
18pub(crate) struct LoginTokens {
20 pub session_token: String,
22 pub master_token: String,
24 pub session_validity_secs: i64,
26 pub master_validity_secs: i64,
28}
29
30#[derive(Clone, Debug)]
32struct Tokens {
33 session_token: String,
34 master_token: String,
35 session_expires_at: DateTime<Utc>,
36 master_expires_at: DateTime<Utc>,
37}
38
39pub struct SnowflakeSession {
46 transport: Arc<Transport>,
47 tokens: StdMutex<Tokens>,
48 query_timeout: Duration,
50}
51
52impl SnowflakeSession {
53 pub(crate) fn new(
55 transport: Arc<Transport>,
56 tokens: LoginTokens,
57 query_timeout: Duration,
58 ) -> Self {
59 let now = Utc::now();
60 Self {
61 transport,
62 tokens: StdMutex::new(Tokens {
63 session_token: tokens.session_token,
64 master_token: tokens.master_token,
65 session_expires_at: now + TimeDelta::seconds(tokens.session_validity_secs),
66 master_expires_at: now + TimeDelta::seconds(tokens.master_validity_secs),
67 }),
68 query_timeout,
69 }
70 }
71
72 #[must_use]
74 pub fn session_expiring_within(&self, within: TimeDelta) -> bool {
75 Utc::now() + within >= self.lock().session_expires_at
76 }
77
78 #[must_use]
81 pub fn master_expires_at(&self) -> DateTime<Utc> {
82 self.lock().master_expires_at
83 }
84
85 pub async fn query(&self, sql: &str) -> Result<Vec<Row>> {
92 let token = self.lock().session_token.clone();
93 let rid = request_id();
94 let body = json!({ "sqlText": sql });
95 let data = self
99 .transport
100 .post_statement(
101 &[("requestId", rid.as_str())],
102 &body,
103 &token,
104 self.query_timeout,
105 POLL_INTERVAL,
106 )
107 .await?;
108
109 let (columns, index, mut rows) = parse_result(&data)?;
110 if let Some(chunks) = data.get("chunks").and_then(Value::as_array) {
113 let headers = parse_chunk_headers(&data);
114 for chunk in chunks {
115 if let Some(url) = chunk.get("url").and_then(Value::as_str) {
116 let bytes = self.transport.get_bytes(url, &headers).await?;
117 rows.extend(decode_chunk_rows(&bytes, &columns, &index)?);
118 }
119 }
120 }
121 Ok(rows)
122 }
123
124 pub async fn renew(&self) -> Result<()> {
131 let (old_session, master) = {
132 let tokens = self.lock();
133 (tokens.session_token.clone(), tokens.master_token.clone())
134 };
135 let rid = request_id();
136 let body = json!({ "oldSessionToken": old_session, "requestType": "RENEW" });
137 let data = self
138 .transport
139 .post(
140 "session/token-request",
141 &[("requestId", rid.as_str())],
142 &body,
143 Some(&master),
144 )
145 .await?;
146
147 let new_session = data
148 .get("sessionToken")
149 .or_else(|| data.get("token"))
150 .and_then(Value::as_str)
151 .ok_or_else(|| Error::Protocol("token-request returned no sessionToken".into()))?
152 .to_string();
153 let st_validity = data
154 .get("validityInSecondsST")
155 .or_else(|| data.get("validityInSeconds"))
156 .and_then(Value::as_i64)
157 .unwrap_or(3600);
158
159 let mut tokens = self.lock();
160 tokens.session_token = new_session;
161 tokens.session_expires_at = Utc::now() + TimeDelta::seconds(st_validity);
162 if let Some(master_token) = data.get("masterToken").and_then(Value::as_str) {
163 tokens.master_token = master_token.to_string();
164 }
165 if let Some(mt_validity) = data.get("validityInSecondsMT").and_then(Value::as_i64) {
166 tokens.master_expires_at = Utc::now() + TimeDelta::seconds(mt_validity);
167 }
168 Ok(())
169 }
170
171 pub async fn heartbeat(&self) -> Result<()> {
178 let token = self.lock().session_token.clone();
179 let rid = request_id();
180 self.transport
181 .post(
182 "session/heartbeat",
183 &[("requestId", rid.as_str())],
184 &json!({}),
185 Some(&token),
186 )
187 .await?;
188 Ok(())
191 }
192
193 pub async fn close(&self) -> Result<()> {
199 let token = self.lock().session_token.clone();
200 let rid = request_id();
201 self.transport
202 .post(
203 "session",
204 &[("delete", "true"), ("requestId", rid.as_str())],
205 &json!({}),
206 Some(&token),
207 )
208 .await?;
209 Ok(())
210 }
211
212 fn lock(&self) -> MutexGuard<'_, Tokens> {
213 self.tokens
214 .lock()
215 .unwrap_or_else(std::sync::PoisonError::into_inner)
216 }
217}
218
219type ParsedResult = (Arc<Vec<Column>>, Arc<HashMap<String, usize>>, Vec<Row>);
221
222fn parse_result(data: &Value) -> Result<ParsedResult> {
225 if let Some(format) = data.get("queryResultFormat").and_then(Value::as_str) {
226 if !format.eq_ignore_ascii_case("json") {
227 return Err(Error::Unsupported(format!(
228 "result format '{format}' (only JSON is supported)"
229 )));
230 }
231 }
232
233 let rowtype = data
234 .get("rowtype")
235 .and_then(Value::as_array)
236 .ok_or_else(|| Error::Protocol("query response missing rowtype".into()))?;
237 let columns: Arc<Vec<Column>> = Arc::new(rowtype.iter().map(parse_column).collect());
238
239 let mut index = HashMap::new();
240 for (i, col) in columns.iter().enumerate() {
241 index.entry(col.name.to_ascii_uppercase()).or_insert(i);
242 }
243 let index = Arc::new(index);
244
245 let rows = data
246 .get("rowset")
247 .and_then(Value::as_array)
248 .map(|rows| {
249 rows.iter()
250 .map(|row| row_from_cells(row, &columns, &index))
251 .collect()
252 })
253 .unwrap_or_default();
254 Ok((columns, index, rows))
255}
256
257fn row_from_cells(
259 row: &Value,
260 columns: &Arc<Vec<Column>>,
261 index: &Arc<HashMap<String, usize>>,
262) -> Row {
263 let cells = row
264 .as_array()
265 .map(|cells| cells.iter().map(cell_to_string).collect())
266 .unwrap_or_default();
267 Row::new(cells, Arc::clone(columns), Arc::clone(index))
268}
269
270fn parse_chunk_headers(data: &Value) -> Vec<(String, String)> {
272 data.get("chunkHeaders")
273 .and_then(Value::as_object)
274 .map(|headers| {
275 headers
276 .iter()
277 .filter_map(|(k, v)| v.as_str().map(|v| (k.clone(), v.to_string())))
278 .collect()
279 })
280 .unwrap_or_default()
281}
282
283fn decode_chunk_rows(
285 bytes: &[u8],
286 columns: &Arc<Vec<Column>>,
287 index: &Arc<HashMap<String, usize>>,
288) -> Result<Vec<Row>> {
289 let json = gunzip_if_needed(bytes)?;
290 let mut framed = Vec::with_capacity(json.len() + 2);
294 framed.push(b'[');
295 framed.extend_from_slice(&json);
296 framed.push(b']');
297 let rows: Vec<Value> = serde_json::from_slice(&framed)
298 .map_err(|e| Error::Protocol(format!("invalid result chunk JSON: {e}")))?;
299 Ok(rows
300 .iter()
301 .map(|row| row_from_cells(row, columns, index))
302 .collect())
303}
304
305fn gunzip_if_needed(bytes: &[u8]) -> Result<Vec<u8>> {
307 if bytes.starts_with(&[0x1f, 0x8b]) {
308 let mut decoder = flate2::read::GzDecoder::new(bytes);
309 let mut out = Vec::new();
310 decoder.read_to_end(&mut out).map_err(Error::Io)?;
311 Ok(out)
312 } else {
313 Ok(bytes.to_vec())
314 }
315}
316
317fn parse_column(value: &Value) -> Column {
319 Column {
320 name: value
321 .get("name")
322 .and_then(Value::as_str)
323 .unwrap_or_default()
324 .to_string(),
325 ty: value
326 .get("type")
327 .and_then(Value::as_str)
328 .unwrap_or_default()
329 .to_ascii_lowercase(),
330 nullable: value
331 .get("nullable")
332 .and_then(Value::as_bool)
333 .unwrap_or(true),
334 length: value.get("length").and_then(Value::as_i64),
335 precision: value.get("precision").and_then(Value::as_i64),
336 scale: value.get("scale").and_then(Value::as_i64),
337 }
338}
339
340fn cell_to_string(value: &Value) -> Option<String> {
342 match value {
343 Value::Null => None,
344 Value::String(s) => Some(s.clone()),
345 other => Some(other.to_string()),
346 }
347}
348
349#[cfg(test)]
350#[allow(clippy::unwrap_used, clippy::expect_used)]
351mod tests {
352 use super::*;
353 use crate::snowflake::client::row::rows_to_payload;
354 use url::Url;
355 use wiremock::matchers::{method, path};
356 use wiremock::{Mock, MockServer, ResponseTemplate};
357
358 fn live_session(server: &MockServer, session_validity_secs: i64) -> SnowflakeSession {
361 let base = Url::parse(&server.uri()).unwrap().join("/").unwrap();
362 let transport = Arc::new(Transport::with_base_url(base, Duration::from_secs(5)).unwrap());
363 SnowflakeSession::new(
364 transport,
365 LoginTokens {
366 session_token: "sess".to_string(),
367 master_token: "mast".to_string(),
368 session_validity_secs,
369 master_validity_secs: 14_400,
370 },
371 Duration::from_secs(5),
372 )
373 }
374
375 #[test]
376 fn token_accessors_reflect_validities() {
377 let base = Url::parse("https://acct.example/").unwrap();
379 let transport = Arc::new(Transport::with_base_url(base, Duration::from_secs(5)).unwrap());
380 let session = SnowflakeSession::new(
381 transport,
382 LoginTokens {
383 session_token: "s".to_string(),
384 master_token: "m".to_string(),
385 session_validity_secs: 3600,
386 master_validity_secs: 14_400,
387 },
388 Duration::from_secs(5),
389 );
390 assert!(session.master_expires_at() > Utc::now());
391 assert!(!session.session_expiring_within(TimeDelta::seconds(60)));
392 assert!(session.session_expiring_within(TimeDelta::seconds(7200)));
393 }
394
395 #[tokio::test]
396 async fn query_returns_inline_rows() {
397 let server = MockServer::start().await;
398 Mock::given(method("POST"))
399 .and(path("/queries/v1/query-request"))
400 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
401 "success": true,
402 "data": {
403 "queryResultFormat": "json",
404 "rowtype": [{ "name": "N", "type": "fixed", "precision": 38, "scale": 0 }],
405 "rowset": [["1"], ["2"]],
406 }
407 })))
408 .mount(&server)
409 .await;
410
411 let rows = live_session(&server, 3600).query("SELECT 1").await.unwrap();
412 assert_eq!(rows.len(), 2);
413 }
414
415 #[tokio::test]
416 async fn query_downloads_and_appends_external_chunks() {
417 let server = MockServer::start().await;
418 let chunk_url = format!("{}/chunk0", server.uri());
419 Mock::given(method("POST"))
420 .and(path("/queries/v1/query-request"))
421 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
422 "success": true,
423 "data": {
424 "queryResultFormat": "json",
425 "rowtype": [{ "name": "N", "type": "text" }],
426 "rowset": [["a"]],
427 "chunks": [{ "url": chunk_url }],
428 }
429 })))
430 .mount(&server)
431 .await;
432 Mock::given(method("GET"))
434 .and(path("/chunk0"))
435 .respond_with(ResponseTemplate::new(200).set_body_bytes(br#"["b"],["c"]"#.to_vec()))
436 .mount(&server)
437 .await;
438
439 let rows = live_session(&server, 3600).query("SELECT 1").await.unwrap();
440 assert_eq!(rows.len(), 3, "1 inline + 2 chunked");
441 }
442
443 #[tokio::test]
444 async fn query_surfaces_session_expired() {
445 let server = MockServer::start().await;
446 Mock::given(method("POST"))
447 .and(path("/queries/v1/query-request"))
448 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
449 "success": false, "code": "390112", "message": "expired", "data": {}
450 })))
451 .mount(&server)
452 .await;
453
454 let err = live_session(&server, 3600)
455 .query("SELECT 1")
456 .await
457 .unwrap_err();
458 assert!(matches!(err, Error::SessionExpired));
459 }
460
461 #[tokio::test]
462 async fn renew_swaps_in_a_fresh_session_token() {
463 let server = MockServer::start().await;
464 Mock::given(method("POST"))
465 .and(path("/session/token-request"))
466 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
467 "success": true,
468 "data": { "sessionToken": "new-sess", "validityInSecondsST": 3600 }
469 })))
470 .mount(&server)
471 .await;
472
473 let session = live_session(&server, 1);
475 assert!(session.session_expiring_within(TimeDelta::seconds(120)));
476 session.renew().await.unwrap();
477 assert!(!session.session_expiring_within(TimeDelta::seconds(120)));
478 }
479
480 #[tokio::test]
481 async fn renew_errors_when_response_lacks_a_token() {
482 let server = MockServer::start().await;
483 Mock::given(method("POST"))
484 .and(path("/session/token-request"))
485 .respond_with(
486 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
487 )
488 .mount(&server)
489 .await;
490
491 let err = live_session(&server, 3600).renew().await.unwrap_err();
492 assert!(matches!(err, Error::Protocol(_)));
493 }
494
495 #[tokio::test]
496 async fn heartbeat_and_close_post_successfully() {
497 let server = MockServer::start().await;
498 Mock::given(method("POST"))
499 .and(path("/session/heartbeat"))
500 .respond_with(
501 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
502 )
503 .mount(&server)
504 .await;
505 Mock::given(method("POST"))
506 .and(path("/session"))
507 .respond_with(
508 ResponseTemplate::new(200).set_body_json(json!({ "success": true, "data": {} })),
509 )
510 .mount(&server)
511 .await;
512
513 let session = live_session(&server, 3600);
514 session.heartbeat().await.unwrap();
515 session.close().await.unwrap();
516 }
517
518 #[test]
519 fn parse_result_builds_columns_and_inline_rows() {
520 let data = json!({
521 "queryResultFormat": "json",
522 "rowtype": [
523 { "name": "ID", "type": "fixed", "nullable": false, "precision": 38, "scale": 0 },
524 { "name": "NAME", "type": "text", "nullable": true, "length": 16_777_216 },
525 ],
526 "rowset": [["1", "alice"], ["2", null]],
527 });
528 let (_columns, _index, rows) = parse_result(&data).unwrap();
529 assert_eq!(rows.len(), 2);
530 let payload = rows_to_payload(&rows);
531 assert_eq!(payload["columns"][0]["name"], "ID");
532 assert_eq!(payload["columns"][0]["type"], "fixed(38,0)");
533 assert_eq!(payload["rows"][0]["ID"], 1);
534 assert_eq!(payload["rows"][0]["NAME"], "alice");
535 assert_eq!(payload["rows"][1]["NAME"], Value::Null);
536 }
537
538 #[test]
539 fn parse_result_rejects_arrow_but_allows_chunked() {
540 let arrow = json!({ "queryResultFormat": "arrow", "rowtype": [], "rowset": [] });
541 assert!(matches!(parse_result(&arrow), Err(Error::Unsupported(_))));
542 let chunked = json!({
545 "queryResultFormat": "json",
546 "rowtype": [{ "name": "C", "type": "text" }],
547 "rowset": [["a"]],
548 "chunks": [{ "url": "https://example/chunk0" }],
549 });
550 let (_c, _i, rows) = parse_result(&chunked).unwrap();
551 assert_eq!(rows.len(), 1);
552 }
553
554 #[test]
555 fn parse_result_requires_rowtype() {
556 assert!(matches!(
557 parse_result(&json!({ "rowset": [] })),
558 Err(Error::Protocol(_))
559 ));
560 }
561
562 #[test]
563 fn decode_chunk_rows_handles_gzip_and_plain_json() {
564 use std::io::Write as _;
565 let (columns, index, _rows) = parse_result(&json!({
566 "queryResultFormat": "json",
567 "rowtype": [
568 { "name": "ID", "type": "fixed", "precision": 38, "scale": 0 },
569 { "name": "NAME", "type": "text" },
570 ],
571 "rowset": [],
572 }))
573 .unwrap();
574
575 let chunk_json = br#"["3","carol"],["4",null]"#;
578
579 let rows = decode_chunk_rows(chunk_json, &columns, &index).unwrap();
581 assert_eq!(rows.len(), 2);
582 assert_eq!(rows[0].to_json_object().get("NAME"), Some(&json!("carol")));
583 assert_eq!(rows[1].to_json_object().get("NAME"), Some(&Value::Null));
584
585 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
587 encoder.write_all(chunk_json).unwrap();
588 let gzipped = encoder.finish().unwrap();
589 let rows = decode_chunk_rows(&gzipped, &columns, &index).unwrap();
590 assert_eq!(rows.len(), 2);
591 assert_eq!(rows[0].to_json_object().get("ID"), Some(&json!(3)));
592 }
593
594 #[test]
595 fn parse_chunk_headers_reads_string_pairs() {
596 let data = json!({ "chunkHeaders": { "x-amz-server-side-encryption": "AES256" } });
597 let headers = parse_chunk_headers(&data);
598 assert_eq!(
599 headers,
600 vec![(
601 "x-amz-server-side-encryption".to_string(),
602 "AES256".to_string()
603 )]
604 );
605 }
606}