diff --git a/.gitignore b/.gitignore index 4fffb2f..1f66980 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /target /Cargo.lock +/.idea/.gitignore +/.idea/modules.xml +/.idea/snowflake-rs.iml +/.idea/vcs.xml diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 4e1084d..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "jwt", - "snowflake-api", - "snowflake-api/examples/tracing", - "snowflake-api/examples/polars", -] diff --git a/snowflake-api/Cargo.toml b/snowflake-api/Cargo.toml index 206b2b7..5e6aa37 100644 --- a/snowflake-api/Cargo.toml +++ b/snowflake-api/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Andrew Korzhuev "] categories = ["api-bindings", "database"] description = "Snowflake API bindings" documentation = "http://docs.rs/sqlite-api/" -edition = "2021" +edition = "2024" keywords = ["api", "database", "snowflake"] license = "Apache-2.0" name = "snowflake-api" @@ -19,42 +19,34 @@ default = ["cert-auth"] polars = ["dep:polars-core", "dep:polars-io"] [dependencies] -arrow = "53" -async-trait = "0.1" -base64 = "0.22" -bytes = "1" -futures = "0.3" -log = "0.4" -regex = "1" -reqwest = { version = "0.12", default-features = false, features = [ - "gzip", - "json", - "rustls-tls", -] } -reqwest-middleware = { version = "0.3", features = ["json"] } -reqwest-retry = "0.6" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -snowflake-jwt = { version = "0.3", optional = true } -thiserror = "1" -url = "2" -uuid = { version = "1", features = ["v4"] } +anyhow = { workspace = true } +arrow = { workspace = true } +async-trait = { workspace = true } +base64 = { workspace = true } +bytes = { workspace = true } +clap = { workspace = true } +futures = { workspace = true } +log = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +reqwest-middleware = { workspace = true } +reqwest-retry = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +snowflake-jwt = { workspace = true, optional = true } +thiserror = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } # polars-support -polars-core = { version = ">=0.32", optional = true } -polars-io = { version = ">=0.32", features = [ - "json", - "ipc_streaming", -], optional = true } +polars-core = { optional = true, version = "0.46.0" } +polars-io = { optional = true, version = ">=0.32", features = ["json", "ipc_streaming"] } # put request support -glob = { version = "0.3" } -object_store = { version = "0.11", features = ["aws"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +glob = { workspace = true } +object_store = { workspace = true } +tokio = { workspace = true } +flate2 = { workspace = true } [dev-dependencies] -anyhow = "1" -arrow = { version = "53", features = ["prettyprint"] } -clap = { version = "4", features = ["derive"] } -pretty_env_logger = "0.5" -tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } +pretty_env_logger = "0.5" \ No newline at end of file diff --git a/snowflake-api/src/connection.rs b/snowflake-api/src/connection.rs index e7087e1..d8e89a1 100644 --- a/snowflake-api/src/connection.rs +++ b/snowflake-api/src/connection.rs @@ -8,6 +8,10 @@ use thiserror::Error; use url::Url; use uuid::Uuid; +use std::io::Read; +use flate2::bufread::GzDecoder; +use bytes; + #[derive(Error, Debug)] pub enum ConnectionError { #[error(transparent)] @@ -75,6 +79,12 @@ pub struct Connection { client: ClientWithMiddleware, } +impl std::fmt::Debug for Connection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Connection").finish() + } +} + impl Connection { pub fn new() -> Result { let client = Self::default_client_builder()?; @@ -126,8 +136,8 @@ impl Connection { ) -> Result { let context = query_type.query_context(); - let request_id = Uuid::new_v4(); - let request_guid = Uuid::new_v4(); + let request_id = Uuid::now_v7(); + let request_guid = Uuid::now_v7(); let client_start_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() @@ -183,7 +193,7 @@ impl Connection { for (k, v) in headers { header_map.insert( HeaderName::from_bytes(k.as_bytes()).unwrap(), - HeaderValue::from_bytes(v.as_bytes()).unwrap(), + HeaderValue::from_bytes(v.as_bytes())?, ); } let bytes = self @@ -193,7 +203,22 @@ impl Connection { .send() .await? .bytes() - .await?; - Ok(bytes) + .await; + + match bytes { + Ok(bytes) => { + // convert from gzip to Bytes + let mut gz = GzDecoder::new(&bytes[..]); + let mut decoded_bytes = Vec::new(); + gz.read_to_end(&mut decoded_bytes).expect("Failed to decode bytes"); + let decoded = bytes::Bytes::copy_from_slice(&decoded_bytes); + + Ok(decoded) + } + Err(e) => { + Err(ConnectionError::RequestError(e)) + } + } + } } diff --git a/snowflake-api/src/lib.rs b/snowflake-api/src/lib.rs index 2f4789e..d980ce0 100644 --- a/snowflake-api/src/lib.rs +++ b/snowflake-api/src/lib.rs @@ -16,7 +16,7 @@ clippy::missing_panics_doc use std::fmt::{Display, Formatter}; use std::io; use std::sync::Arc; - +use arrow::datatypes::SchemaRef; use arrow::error::ArrowError; use arrow::ipc::reader::StreamReader; use arrow::record_batch::RecordBatch; @@ -41,7 +41,7 @@ pub mod connection; mod polars; mod put; mod requests; -mod responses; +pub mod responses; mod session; #[derive(Error, Debug)] @@ -56,7 +56,7 @@ pub enum SnowflakeApiError { ResponseDeserializationError(#[from] base64::DecodeError), #[error(transparent)] - ArrowError(#[from] arrow::error::ArrowError), + ArrowError(#[from] ArrowError), #[error("S3 bucket path in PUT request is invalid: `{0}`")] InvalidBucketPath(String), @@ -138,32 +138,66 @@ impl From for FieldSchema { /// Container for query result. /// Arrow is returned by-default for all SELECT statements, /// unless there is session configuration issue or it's a different statement type. -pub enum QueryResult { - Arrow(Vec), - Json(JsonResult), - Empty, +pub enum QueryResult { + Arrow { + batches: Vec, + schema: SchemaRef, + meta: M, + }, + Json { + data: JsonResult, + meta: M, + }, + Empty { + meta: M, + }, } /// Raw query result /// Can be transformed into [`QueryResult`] -pub enum RawQueryResult { +pub enum RawQueryResult { /// Arrow IPC chunks /// see: - Bytes(Vec), + Bytes { + data: Vec, + meta: M, + }, /// Json payload is deserialized, /// as it's already a part of REST response - Json(JsonResult), - Empty, + Json { + data: JsonResult, + meta: M, + }, + Empty { + meta: M, + }, } -impl RawQueryResult { - pub fn deserialize_arrow(self) -> Result { +impl RawQueryResult { + pub fn deserialize_arrow(self) -> Result, ArrowError> { match self { - RawQueryResult::Bytes(bytes) => { - Self::flat_bytes_to_batches(bytes).map(QueryResult::Arrow) + RawQueryResult::Bytes { data, meta } => { + let batches = Self::flat_bytes_to_batches(data)?; + let schema = batches[0].schema(); + Ok( + QueryResult::Arrow { + batches, + schema, + meta, + } + ) } - RawQueryResult::Json(j) => Ok(QueryResult::Json(j)), - RawQueryResult::Empty => Ok(QueryResult::Empty), + RawQueryResult::Json { data, meta } => Ok( + QueryResult::Json { + data, + meta + } + ), + RawQueryResult::Empty { meta } => Ok( + QueryResult::Empty { + meta + } + ), } } @@ -182,6 +216,7 @@ impl RawQueryResult { } } +#[derive(Debug)] pub struct AuthArgs { pub account_identifier: String, pub warehouse: Option, @@ -218,6 +253,7 @@ impl AuthArgs { } } +#[derive(Debug)] pub enum AuthType { Password(PasswordArgs), Certificate(CertificateArgs), @@ -227,10 +263,26 @@ pub struct PasswordArgs { pub password: String, } +impl std::fmt::Debug for PasswordArgs { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PasswordArgs") + .field("password", &"******") + .finish() + } +} + pub struct CertificateArgs { pub private_key_pem: String, } +impl std::fmt::Debug for CertificateArgs { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CertificateArgs") + .field("private_key_pem", &"******") + .finish() + } +} + #[must_use] pub struct SnowflakeApiBuilder { pub auth: AuthArgs, @@ -286,7 +338,8 @@ impl SnowflakeApiBuilder { } } -/// Snowflake API, keeps connection pool and manages session for you +/// Snowflake API keeps a connection pool and manages session for you +#[derive(Debug)] pub struct SnowflakeApi { connection: Arc, session: Session, @@ -378,7 +431,7 @@ impl SnowflakeApi { /// Execute a single query against API. /// If statement is PUT, then file will be uploaded to the Snowflake-managed storage - pub async fn exec(&self, sql: &str) -> Result { + pub async fn exec(&self, sql: &str) -> Result, SnowflakeApiError> { let raw = self.exec_raw(sql).await?; let res = raw.deserialize_arrow()?; Ok(res) @@ -387,83 +440,103 @@ impl SnowflakeApi { /// Executes a single query against API. /// If statement is PUT, then file will be uploaded to the Snowflake-managed storage /// Returns raw bytes in the Arrow response - pub async fn exec_raw(&self, sql: &str) -> Result { + pub async fn exec_raw(&self, sql: &str) -> Result, SnowflakeApiError> { let put_re = Regex::new(r"(?i)^(?:/\*.*\*/\s*)*put\s+").unwrap(); // put commands go through a different flow and result is side-effect if put_re.is_match(sql) { log::info!("Detected PUT query"); - self.exec_put(sql).await.map(|()| RawQueryResult::Empty) + self.exec_put(sql).await.map(|meta| RawQueryResult::Empty { meta }) } else { self.exec_arrow_raw(sql).await } } - async fn exec_put(&self, sql: &str) -> Result<(), SnowflakeApiError> { + async fn exec_put(&self, sql: &str) -> Result { let resp = self .run_sql::(sql, QueryType::JsonQuery) .await?; - log::debug!("Got PUT response: {:?}", resp); + log::trace!("Got PUT response: {:?}", resp); + - match resp { + match &resp { ExecResponse::Query(_) => Err(SnowflakeApiError::UnexpectedResponse), - ExecResponse::PutGet(pg) => put::put(pg).await, + ExecResponse::PutGet(pg) => { + put::put(pg.clone()).await?; + Ok(resp) + }, ExecResponse::Error(e) => Err(SnowflakeApiError::ApiError( - e.data.error_code, - e.message.unwrap_or_default(), + e.data.error_code.clone(), + e.message.clone().unwrap_or_default(), )), } } /// Useful for debugging to get the straight query response #[cfg(debug_assertions)] - pub async fn exec_response(&mut self, sql: &str) -> Result { + pub async fn exec_response(&self, sql: &str) -> Result { self.run_sql::(sql, QueryType::ArrowQuery) .await } /// Useful for debugging to get raw JSON response #[cfg(debug_assertions)] - pub async fn exec_json(&mut self, sql: &str) -> Result { + pub async fn exec_json(&self, sql: &str) -> Result { self.run_sql::(sql, QueryType::JsonQuery) .await } - async fn exec_arrow_raw(&self, sql: &str) -> Result { + async fn exec_arrow_raw(&self, sql: &str) -> Result, SnowflakeApiError> { let resp = self .run_sql::(sql, QueryType::ArrowQuery) .await?; - log::debug!("Got query response: {:?}", resp); + + log::trace!("Got query response: {:?}", resp); - let resp = match resp { + let (ref q_resp, ref _meta) = match resp { // processable response - ExecResponse::Query(qr) => Ok(qr), - ExecResponse::PutGet(_) => Err(SnowflakeApiError::UnexpectedResponse), - ExecResponse::Error(e) => Err(SnowflakeApiError::ApiError( - e.data.error_code, - e.message.unwrap_or_default(), - )), + ExecResponse::Query(ref qr) => { + log::info!("Got a response: OK"); + Ok((qr, resp.clone_as_meta())) + }, + ExecResponse::PutGet(_) => { + log::info!("Got a response: Unexpected PUT response"); + Err(SnowflakeApiError::UnexpectedResponse) + }, + ExecResponse::Error(ref e) => + { + log::error!("Got a response: Error - {:?}", e); + Err(SnowflakeApiError::ApiError( + e.data.error_code.clone(), + e.message.clone().unwrap_or_default(), + )) + }, }?; // if response was empty, base64 data is empty string // todo: still return empty arrow batch with proper schema? (schema always included) - if resp.data.returned == 0 { + if q_resp.data.returned == 0 { log::debug!("Got response with 0 rows"); - Ok(RawQueryResult::Empty) - } else if let Some(value) = resp.data.rowset { + Ok(RawQueryResult::Empty { + meta: resp.clone_as_meta() + }) + } else if let Some(ref value) = q_resp.data.rowset { log::debug!("Got JSON response"); // NOTE: json response could be chunked too. however, go clients should receive arrow by-default, // unless user sets session variable to return json. This case was added for debugging and status // information being passed through that fields. - Ok(RawQueryResult::Json(JsonResult { - value, - schema: resp.data.rowtype.into_iter().map(Into::into).collect(), - })) - } else if let Some(base64) = resp.data.rowset_base64 { + Ok(RawQueryResult::Json { + data: JsonResult { + value: value.clone(), + schema: q_resp.data.rowtype.iter().map(|r|r.clone().into()).collect(), + }, + meta: resp.clone_as_meta() + }) + } else if let Some(ref base64) = q_resp.data.rowset_base64 { // fixme: is it possible to give streaming interface? - let mut chunks = try_join_all(resp.data.chunks.iter().map(|chunk| { + let mut chunks = try_join_all(q_resp.data.chunks.iter().map(|chunk| { self.connection - .get_chunk(&chunk.url, &resp.data.chunk_headers) + .get_chunk(&chunk.url, &q_resp.data.chunk_headers) })) .await?; @@ -475,7 +548,10 @@ impl SnowflakeApi { chunks.push(bytes); } - Ok(RawQueryResult::Bytes(chunks)) + Ok(RawQueryResult::Bytes { + data: chunks, + meta: resp.clone_as_meta() + }) } else { Err(SnowflakeApiError::BrokenResponse) } diff --git a/snowflake-api/src/responses.rs b/snowflake-api/src/responses.rs index 3c2f497..36b3eab 100644 --- a/snowflake-api/src/responses.rs +++ b/snowflake-api/src/responses.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; #[allow(clippy::large_enum_variant)] -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum ExecResponse { Query(QueryExecResponse), @@ -11,10 +11,27 @@ pub enum ExecResponse { Error(ExecErrorResponse), } +impl ExecResponse { + pub fn clone_as_meta(&self) -> Self { + match self { + ExecResponse::Query(q) => { + ExecResponse::Query(QueryExecResponse { + code: q.code.clone(), + message: q.message.clone(), + success: q.success, + data: q.data.clone_as_meta(), + }) + }, + ExecResponse::PutGet(g) => ExecResponse::PutGet(g.clone()), + ExecResponse::Error(e) => ExecResponse::Error(e.clone()), + } + } +} + // todo: add close session response, which should be just empty? // FIXME: dead_code #[allow(clippy::large_enum_variant, dead_code)] -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum AuthResponse { Login(LoginResponse), @@ -24,7 +41,7 @@ pub enum AuthResponse { Error(AuthErrorResponse), } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseRestResponse { // null for auth pub code: Option, @@ -43,7 +60,7 @@ pub type RenewSessionResponse = BaseRestResponse; // Data should be always `null` on successful close session response pub type CloseSessionResponse = BaseRestResponse>; -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecErrorResponseData { pub age: i64, @@ -59,7 +76,7 @@ pub struct ExecErrorResponseData { pub sql_state: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // FIXME: dead_code #[allow(dead_code)] @@ -68,13 +85,13 @@ pub struct AuthErrorResponseData { pub error_code: Option, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NameValueParameter { pub name: String, pub value: serde_json::Value, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // FIXME #[allow(dead_code)] @@ -90,7 +107,7 @@ pub struct LoginResponseData { pub validity_in_seconds: i64, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // FIXME: dead_code #[allow(dead_code)] @@ -101,7 +118,7 @@ pub struct SessionInfo { pub role_name: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // FIXME: dead_code #[allow(dead_code)] @@ -111,7 +128,7 @@ pub struct AuthenticatorResponseData { pub proof_key: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // FIXME: dead_code #[allow(dead_code)] @@ -123,17 +140,19 @@ pub struct RenewSessionResponseData { pub session_id: i64, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QueryExecResponseData { pub parameters: Vec, pub rowtype: Vec, // default for non-SELECT queries // GET / PUT has their own response format + #[serde(skip_serializing)] pub rowset: Option, // only exists when binary response is given, eg Arrow // default for all SELECT queries // is base64-encoded Arrow IPC payload + #[serde(skip_serializing)] pub rowset_base64: Option, pub total: i64, pub returned: i64, // unused in .NET @@ -149,7 +168,9 @@ pub struct QueryExecResponseData { pub statement_type_id: i64, pub version: i64, // if response is chunked - #[serde(default)] // soft-default to empty Vec if not present + #[serde(default)] + #[serde(skip_serializing)] + // soft-default to empty Vec if not present pub chunks: Vec, // x-amz-server-side-encryption-customer-key, when chunks are present for download pub qrmk: Option, @@ -163,7 +184,40 @@ pub struct QueryExecResponseData { // `sendResultTime`, `queryResultFormat`, `queryContext` also exist } -#[derive(Deserialize, Debug)] +impl QueryExecResponseData { + /// A helper function to make a metadata version that doesn't include underlying data + /// + /// This is used as the `meta` field on [super::QueryResult] and [super::RawQueryResult] + pub fn clone_as_meta(&self) -> Self { + Self { + parameters: self.parameters.clone(), + rowtype: self.rowtype.clone(), + // skip actual data + rowset: None, + // skip actual data + rowset_base64: None, + total: self.total, + returned: self.returned, + query_id: self.query_id.clone(), + database_provider: self.database_provider.clone(), + final_database_name: self.final_database_name.clone(), + final_schema_name: self.final_schema_name.clone(), + final_warehouse_name: self.final_warehouse_name.clone(), + final_role_name: self.final_role_name.clone(), + number_of_binds: self.number_of_binds.clone(), + statement_type_id: self.statement_type_id, + version: self.version, + // these are just links, so we'll keep them + chunks: self.chunks.clone(), + qrmk: self.qrmk.clone(), + chunk_headers: self.chunk_headers.clone(), + get_result_url: self.get_result_url.clone(), + result_ids: self.result_ids.clone(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecResponseRowType { pub name: String, #[serde(rename = "byteLength")] @@ -178,7 +232,7 @@ pub struct ExecResponseRowType { } // fixme: is it good idea to keep this as an enum if more types could be added in future? -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SnowflakeType { Fixed, @@ -196,7 +250,7 @@ pub enum SnowflakeType { Array, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecResponseChunk { pub url: String, @@ -204,7 +258,7 @@ pub struct ExecResponseChunk { pub uncompressed_size: i64, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PutGetResponseData { // `kind`, `operation` are present in Go implementation, but not in .NET @@ -233,14 +287,14 @@ pub struct PutGetResponseData { pub statement_type_id: Option, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum CommandType { Upload, Download, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum PutGetStageInfo { Aws(AwsPutGetStageInfo), @@ -248,7 +302,7 @@ pub enum PutGetStageInfo { Gcs(GcsPutGetStageInfo), } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AwsPutGetStageInfo { pub location_type: String, @@ -259,7 +313,7 @@ pub struct AwsPutGetStageInfo { pub end_point: Option, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct AwsCredentials { pub aws_key_id: String, @@ -269,7 +323,7 @@ pub struct AwsCredentials { pub aws_key: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GcsPutGetStageInfo { pub location_type: String, @@ -279,13 +333,13 @@ pub struct GcsPutGetStageInfo { pub presigned_url: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct GcsCredentials { pub gcs_access_token: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AzurePutGetStageInfo { pub location_type: String, @@ -294,20 +348,20 @@ pub struct AzurePutGetStageInfo { pub creds: AzureCredentials, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct AzureCredentials { pub azure_sas_token: String, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum EncryptionMaterialVariant { Single(PutGetEncryptionMaterial), Multiple(Vec), } -#[derive(Deserialize, Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PutGetEncryptionMaterial { // base64 encoded diff --git a/snowflake-api/src/session.rs b/snowflake-api/src/session.rs index 90acaaf..d4f2677 100644 --- a/snowflake-api/src/session.rs +++ b/snowflake-api/src/session.rs @@ -131,6 +131,20 @@ pub struct Session { password: Option, } +impl std::fmt::Debug for Session { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Session") + .field("account_identifier", &self.account_identifier) + .field("warehouse", &self.warehouse) + .field("database", &self.database) + .field("schema", &self.schema) + .field("username", &self.username) + .field("role", &self.role) + .field("private_key_pem", &self.private_key_pem) + .finish() + } +} + // todo: make builder impl Session { /// Authenticate using private certificate and JWT @@ -338,10 +352,11 @@ impl Session { body, ) .await?; - log::debug!("Auth response: {:?}", resp); + log::trace!("Auth response: {:?}", resp); match resp { AuthResponse::Login(lr) => { + log::debug!("Authenticated successfully"); let session_token = AuthToken::new(&lr.data.token, lr.data.validity_in_seconds); let master_token = AuthToken::new(&lr.data.master_token, lr.data.master_validity_in_seconds);