1pub mod actions;
23
24use std::convert::Infallible;
25use std::net::SocketAddr;
26use std::sync::Arc;
27
28use bytes::Bytes;
29use http_body_util::{BodyExt, Full, Limited};
30use hyper::body::{Body, Incoming};
31use hyper::server::conn::http1;
32use hyper::service::service_fn;
33use hyper::{Method, Request, Response};
34use hyper_util::rt::TokioIo;
35use serde_json::{json, Value};
36use tokio::sync::mpsc;
37use tracing::{debug, error};
38
39use crate::llm::action_helper::call_llm;
40use crate::llm::actions::protocol_trait::ActionResult;
41use crate::llm::ollama_client::OllamaClient;
42use crate::logging::emit::Log;
43use crate::protocol::Event;
44use crate::server::accept_bounded::{
45 accept_bounded, watch_idle, ConnectionActivity, ConnectionLimiter,
46};
47use crate::server::connection::ConnectionId;
48use crate::server::snowflake::actions::{
49 SnowflakeProtocol, SNOWFLAKE_LOGIN_EVENT, SNOWFLAKE_QUERY_EVENT, SNOWFLAKE_SESSION_EVENT,
50};
51use crate::state::app_state::AppState;
52
53pub const MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
60
61const CODE_AUTH_UNAVAILABLE: &str = "390100";
66const CODE_INTERNAL: &str = "000603";
68const CODE_REQUEST_TIMEOUT: &str = "000629";
70
71const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
84
85const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
100
101const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
108
109const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
117 Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
118
119pub struct SnowflakeServer;
121
122impl SnowflakeServer {
123 pub async fn spawn_with_llm_actions(
127 listen_addr: SocketAddr,
128 llm_client: OllamaClient,
129 app_state: Arc<AppState>,
130 status_tx: mpsc::UnboundedSender<String>,
131 server_id: crate::state::ServerId,
132 ) -> anyhow::Result<SocketAddr> {
133 let listener =
134 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
135 let local_addr = listener.local_addr()?;
136
137 Log::new(Some(&status_tx)).info(format!("Snowflake server listening on {}", local_addr));
138
139 let protocol = Arc::new(SnowflakeProtocol::new(
140 ConnectionId::new(0),
141 app_state.clone(),
142 status_tx.clone(),
143 ));
144
145 let task_registrar = app_state.clone();
146 let limiter = ConnectionLimiter::new(MAX_CONNECTIONS);
147 let accept_handle = tokio::spawn(async move {
148 loop {
149 match accept_bounded(
150 &listener,
151 &limiter,
152 CONNECTION_CAP_REFUSAL,
153 "SNOWFLAKE",
154 Some(&status_tx),
155 )
156 .await
157 {
158 Ok((stream, remote_addr, permit)) => {
159 let connection_id =
160 ConnectionId::new(app_state.get_next_unified_id().await);
161 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
162 debug!(
163 "Snowflake connection {} from {}",
164 connection_id, remote_addr
165 );
166
167 use crate::state::server::{
168 ConnectionState as ServerConnectionState, ConnectionStatus,
169 ProtocolConnectionInfo,
170 };
171 let now = crate::utils::clock::Instant::now();
172 let conn_state = ServerConnectionState {
173 id: connection_id,
174 remote_addr,
175 local_addr: local_addr_conn,
176 bytes_sent: 0,
177 bytes_received: 0,
178 packets_sent: 0,
179 packets_received: 0,
180 last_activity: now,
181 status: ConnectionStatus::Active,
182 status_changed_at: now,
183 protocol_info: ProtocolConnectionInfo::empty(),
184 };
185 app_state
186 .add_connection_to_server(server_id, conn_state)
187 .await;
188 let _ = status_tx.send("__UPDATE_UI__".to_string());
189
190 let llm_client_clone = llm_client.clone();
191 let app_state_clone = app_state.clone();
192 let status_tx_clone = status_tx.clone();
193 let protocol_clone = protocol.clone();
194
195 let task_owner = app_state.clone();
196 task_owner
197 .spawn_server_task(server_id, async move {
198 let _permit = permit;
201
202 let spoke = matches!(
209 tokio::time::timeout(
210 FIRST_BYTE_READ_TIMEOUT,
211 stream.peek(&mut [0u8; 1]),
212 )
213 .await,
214 Ok(Ok(n)) if n > 0
215 );
216
217 if spoke {
218 let io = TokioIo::new(stream);
219 let status_for_service = status_tx_clone.clone();
220 let app_state_for_service = app_state_clone.clone();
221
222 let activity =
227 Arc::new(ConnectionActivity::new());
228 let activity_for_service = Arc::clone(&activity);
229 let service = service_fn(move |req: Request<Incoming>| {
230 let llm_clone = llm_client_clone.clone();
231 let state_clone = app_state_for_service.clone();
232 let status_clone = status_for_service.clone();
233 let protocol_clone = protocol_clone.clone();
234 let activity = Arc::clone(&activity_for_service);
240 async move {
241 let _busy = activity.busy();
242 handle_snowflake_request(
243 req,
244 connection_id,
245 server_id,
246 llm_clone,
247 state_clone,
248 status_clone,
249 protocol_clone,
250 )
251 .await
252 }
253 });
254
255 let conn = http1::Builder::new().serve_connection(io, service);
256 tokio::pin!(conn);
257 tokio::select! {
258 result = &mut conn => {
259 if let Err(err) = result {
260 error!("Error serving Snowflake connection: {:?}", err);
261 }
262 }
263 _ = watch_idle(
271 Arc::clone(&activity),
272 IDLE_BETWEEN_REQUESTS_TIMEOUT,
273 ) => {
274 Log::new(Some(&status_tx_clone)).debug(format!(
275 "Snowflake connection {} idle for {}s; closing",
276 connection_id,
277 IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
278 ));
279 }
280 }
281 } else {
282 Log::new(Some(&status_tx_clone)).debug(format!(
283 "Snowflake peer {} sent no request within {}s; closing \
284 decision=fail_closed_first_byte_timeout",
285 remote_addr,
286 FIRST_BYTE_READ_TIMEOUT.as_secs()
287 ));
288 }
289
290 app_state_clone
291 .close_connection_on_server(server_id, connection_id)
292 .await;
293 Log::new(Some(&status_tx_clone))
294 .info(format!("Snowflake connection {connection_id} closed"));
295 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
296 })
297 .await;
298 }
299 Err(e) => {
300 Log::new(Some(&status_tx))
301 .error(format!("Failed to accept Snowflake connection: {}", e));
302 break;
303 }
304 }
305 }
306 });
307
308 task_registrar
309 .register_server_task(server_id, accept_handle)
310 .await;
311
312 Ok(local_addr)
313 }
314}
315
316fn json_200(body: String) -> Response<Full<Bytes>> {
323 match Response::builder()
324 .status(200)
325 .header(hyper::header::CONTENT_TYPE, "application/json")
326 .body(Full::new(Bytes::from(body)))
327 {
328 Ok(resp) => resp,
329 Err(e) => {
330 error!("Snowflake: failed to build response ({e}), sending bare 500");
331 let mut fallback = Response::new(Full::new(Bytes::from_static(b"{\"success\":false}")));
332 *fallback.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
333 fallback
334 }
335 }
336}
337
338fn llm_error_decision(err: &anyhow::Error) -> &'static str {
343 match crate::utils::WireFailure::classify(err) {
344 crate::utils::WireFailure::Overloaded => "fail_closed_llm_overloaded",
345 crate::utils::WireFailure::Unavailable => "fail_closed_llm_error",
346 }
347}
348
349fn error_envelope(code: &str, message: &str) -> String {
354 json!({
355 "data": Value::Null,
356 "code": code,
357 "message": message,
358 "success": false,
359 })
360 .to_string()
361}
362
363fn statement_error_envelope(code: &str, message: &str, sql_state: Option<&str>) -> String {
370 let mut data = json!({ "errorCode": code });
371 if let Some(state) = sql_state {
372 data["sqlState"] = json!(state);
373 }
374 json!({
375 "data": data,
376 "code": code,
377 "message": message,
378 "success": false,
379 })
380 .to_string()
381}
382
383async fn handle_snowflake_request(
384 req: Request<Incoming>,
385 connection_id: ConnectionId,
386 server_id: crate::state::ServerId,
387 llm_client: OllamaClient,
388 app_state: Arc<AppState>,
389 status_tx: mpsc::UnboundedSender<String>,
390 protocol: Arc<SnowflakeProtocol>,
391) -> Result<Response<Full<Bytes>>, Infallible> {
392 let method = req.method().clone();
393 let uri = req.uri().clone();
394 let path = uri.path().to_string();
395 let query = uri.query().unwrap_or("").to_string();
396
397 Log::new(Some(&status_tx)).debug(format!("Snowflake request: {} {}", method, path));
398
399 app_state
400 .update_connection_stats(server_id, connection_id, None, None, Some(1), None)
401 .await;
402
403 let has_auth_token = req
405 .headers()
406 .get(hyper::header::AUTHORIZATION)
407 .and_then(|v| v.to_str().ok())
408 .map(|s| s.contains("Token=") && !s.contains("Token=\"\""))
409 .unwrap_or(false);
410
411 let response = match (method.clone(), path.as_str()) {
412 (Method::POST, "/session/v1/login-request")
413 | (Method::POST, "/session/authenticator-request") => {
414 handle_login(
415 req,
416 connection_id,
417 server_id,
418 llm_client,
419 app_state.clone(),
420 status_tx.clone(),
421 protocol,
422 )
423 .await
424 }
425 (Method::POST, "/queries/v1/query-request") => {
426 handle_query(
427 req,
428 connection_id,
429 server_id,
430 llm_client,
431 app_state.clone(),
432 status_tx.clone(),
433 protocol,
434 has_auth_token,
435 &query,
436 )
437 .await
438 }
439 (Method::POST, "/session/logout-request") | (Method::POST, "/session")
443 if path != "/session" || query.split('&').any(|kv| kv == "delete=true") =>
444 {
445 handle_session(
446 connection_id,
447 server_id,
448 llm_client,
449 app_state.clone(),
450 status_tx.clone(),
451 protocol,
452 "logout",
453 has_auth_token,
454 )
455 .await
456 }
457 (Method::POST, "/session/token-request") => {
458 handle_session(
459 connection_id,
460 server_id,
461 llm_client,
462 app_state.clone(),
463 status_tx.clone(),
464 protocol,
465 "token_renew",
466 has_auth_token,
467 )
468 .await
469 }
470 _ => {
471 debug!(
472 "Snowflake conn={} {} {} decision=unknown_endpoint: no LLM call",
473 connection_id, method, path
474 );
475 Ok(json_200(error_envelope(
476 "390318",
477 "Unknown Snowflake endpoint",
478 )))
479 }
480 };
481
482 let body_size = response
483 .as_ref()
484 .ok()
485 .and_then(|resp| resp.body().size_hint().exact())
486 .unwrap_or(0);
487 app_state
488 .update_connection_stats(
489 server_id,
490 connection_id,
491 None,
492 Some(body_size),
493 None,
494 Some(1),
495 )
496 .await;
497 let _ = status_tx.send("__UPDATE_UI__".to_string());
498 response
499}
500
501async fn read_json_body(req: Request<Incoming>) -> Value {
507 let gzip = req
508 .headers()
509 .get(hyper::header::CONTENT_ENCODING)
510 .and_then(|v| v.to_str().ok())
511 .is_some_and(|v| v.trim().eq_ignore_ascii_case("gzip"));
512 match Limited::new(req.into_body(), MAX_REQUEST_BYTES)
513 .collect()
514 .await
515 {
516 Ok(collected) => {
517 let bytes = collected.to_bytes();
518 if gzip {
519 return match gunzip_bounded(&bytes) {
520 Ok(plain) => serde_json::from_slice(&plain).unwrap_or(Value::Null),
521 Err(e) => {
522 error!("Snowflake: gzip request body rejected: {e}");
523 Value::Null
524 }
525 };
526 }
527 serde_json::from_slice(&bytes).unwrap_or(Value::Null)
528 }
529 Err(e) => {
530 error!(
531 "Snowflake: request body rejected (limit {} bytes): {}",
532 MAX_REQUEST_BYTES, e
533 );
534 Value::Null
535 }
536 }
537}
538
539fn gunzip_bounded(compressed: &[u8]) -> std::io::Result<Vec<u8>> {
546 use std::io::Read;
547 let mut plain = Vec::new();
548 flate2::read::GzDecoder::new(compressed)
549 .take(MAX_REQUEST_BYTES as u64 + 1)
550 .read_to_end(&mut plain)?;
551 if plain.len() > MAX_REQUEST_BYTES {
552 return Err(std::io::Error::other(format!(
553 "inflates past {MAX_REQUEST_BYTES} bytes"
554 )));
555 }
556 Ok(plain)
557}
558
559fn first_custom(results: Vec<ActionResult>) -> Option<(String, Value)> {
561 for r in results {
562 if let ActionResult::Custom { name, data } = r {
563 return Some((name, data));
564 }
565 }
566 None
567}
568
569#[allow(clippy::too_many_arguments)]
570async fn handle_login(
571 req: Request<Incoming>,
572 connection_id: ConnectionId,
573 server_id: crate::state::ServerId,
574 llm_client: OllamaClient,
575 app_state: Arc<AppState>,
576 status_tx: mpsc::UnboundedSender<String>,
577 protocol: Arc<SnowflakeProtocol>,
578) -> Result<Response<Full<Bytes>>, Infallible> {
579 let body = read_json_body(req).await;
580 let data = body.get("data").cloned().unwrap_or(Value::Null);
583 let login_name = data
584 .get("LOGIN_NAME")
585 .and_then(|v| v.as_str())
586 .unwrap_or("")
587 .to_string();
588 let account = data
589 .get("ACCOUNT_NAME")
590 .and_then(|v| v.as_str())
591 .unwrap_or("")
592 .to_string();
593 let has_password = data
594 .get("PASSWORD")
595 .and_then(|v| v.as_str())
596 .map(|s| !s.is_empty())
597 .unwrap_or(false);
598
599 let event = Event::new(
600 &SNOWFLAKE_LOGIN_EVENT,
601 json!({
602 "login_name": login_name,
603 "account": account,
604 "client_app_id": data.get("CLIENT_APP_ID").and_then(|v| v.as_str()).unwrap_or(""),
605 "client_app_version": data.get("CLIENT_APP_VERSION").and_then(|v| v.as_str()).unwrap_or(""),
606 "has_password": has_password,
607 }),
608 );
609
610 match call_llm(
611 &llm_client,
612 &app_state,
613 server_id,
614 Some(connection_id),
615 &event,
616 &*protocol,
617 )
618 .await
619 {
620 Ok(execution_result) => match first_custom(execution_result.protocol_results) {
621 Some((name, payload)) if name == "snowflake_login_success" => {
622 let token = payload
623 .get("token")
624 .and_then(|v| v.as_str())
625 .unwrap_or("")
626 .to_string();
627 if token.is_empty() {
628 Log::new(Some(&status_tx)).error(format!(
630 "Snowflake login conn={} user={} decision=fail_closed_bad_action: \
631 login_success carried no token; refusing the login",
632 connection_id, login_name
633 ));
634 return Ok(json_200(error_envelope(
635 CODE_AUTH_UNAVAILABLE,
636 "netget: login backend returned no token",
637 )));
638 }
639 let validity = payload
640 .get("validity_seconds")
641 .and_then(|v| v.as_u64())
642 .unwrap_or(3600);
643 let master = payload
644 .get("master_token")
645 .and_then(|v| v.as_str())
646 .map(|s| s.to_string())
647 .unwrap_or_else(|| token.clone());
648 let session_id = payload.get("session_id").cloned().unwrap_or(Value::Null);
649
650 let out = json!({
651 "data": {
652 "token": token,
653 "masterToken": master,
654 "sessionId": session_id,
655 "validityInSeconds": validity,
656 "masterValidityInSeconds": validity * 4,
657 "displayUserName": login_name,
658 "serverVersion": "8.0.0",
659 "firstLogin": false,
660 "healthCheckInterval": 45,
661 "sessionInfo": {
662 "databaseName": Value::Null,
663 "schemaName": Value::Null,
664 "warehouseName": Value::Null,
665 "roleName": "PUBLIC"
666 },
667 "parameters": []
668 },
669 "message": Value::Null,
670 "code": Value::Null,
671 "success": true
672 });
673 Log::new(Some(&status_tx)).info(format!(
674 "Snowflake login conn={} user={} decision=model_answer: session token issued",
675 connection_id, login_name
676 ));
677 Ok(json_200(out.to_string()))
678 }
679 Some((name, payload)) if name == "snowflake_error" => {
680 let code = payload
682 .get("code")
683 .and_then(|v| v.as_str())
684 .unwrap_or(CODE_AUTH_UNAVAILABLE);
685 let message = payload
686 .get("message")
687 .and_then(|v| v.as_str())
688 .unwrap_or("Login refused");
689 Log::new(Some(&status_tx)).info(format!(
690 "Snowflake login conn={} user={} decision=model_reject: {}",
691 connection_id, login_name, code
692 ));
693 Ok(json_200(error_envelope(code, message)))
694 }
695 _ => {
696 Log::new(Some(&status_tx)).warn(format!(
698 "Snowflake login conn={} user={} decision=model_silent: no login action \
699 produced; refusing the login",
700 connection_id, login_name
701 ));
702 Ok(json_200(error_envelope(
703 CODE_AUTH_UNAVAILABLE,
704 "netget: no login decision produced",
705 )))
706 }
707 },
708 Err(e) => {
709 Log::new(Some(&status_tx)).error(format!(
712 "Snowflake login conn={} user={} decision={}: refusing, no token issued: {}",
713 connection_id,
714 login_name,
715 llm_error_decision(&e),
716 e
717 ));
718 Ok(json_200(error_envelope(
719 CODE_AUTH_UNAVAILABLE,
720 crate::utils::WireFailure::classify(&e).prefixed_text(),
721 )))
722 }
723 }
724}
725
726#[allow(clippy::too_many_arguments)]
727async fn handle_query(
728 req: Request<Incoming>,
729 connection_id: ConnectionId,
730 server_id: crate::state::ServerId,
731 llm_client: OllamaClient,
732 app_state: Arc<AppState>,
733 status_tx: mpsc::UnboundedSender<String>,
734 protocol: Arc<SnowflakeProtocol>,
735 has_auth_token: bool,
736 query_string: &str,
737) -> Result<Response<Full<Bytes>>, Infallible> {
738 let body = read_json_body(req).await;
739 let sql_text = body
740 .get("sqlText")
741 .and_then(|v| v.as_str())
742 .unwrap_or("")
743 .to_string();
744 let request_id = query_string
745 .split('&')
746 .find_map(|kv| kv.strip_prefix("requestId="))
747 .unwrap_or("")
748 .to_string();
749
750 let event = Event::new(
751 &SNOWFLAKE_QUERY_EVENT,
752 json!({
753 "sql_text": sql_text,
754 "has_auth_token": has_auth_token,
755 "request_id": request_id,
756 }),
757 );
758
759 match call_llm(
760 &llm_client,
761 &app_state,
762 server_id,
763 Some(connection_id),
764 &event,
765 &*protocol,
766 )
767 .await
768 {
769 Ok(execution_result) => match first_custom(execution_result.protocol_results) {
770 Some((name, payload)) if name == "snowflake_query_response" => {
771 let rowtype = normalize_rowtype(payload.get("rowtype"));
772 let rowset = normalize_rowset(payload.get("rowset"));
773 let returned = rowset.len();
774 let query_id = payload
775 .get("query_id")
776 .and_then(|v| v.as_str())
777 .map(|s| s.to_string())
778 .unwrap_or_else(|| "00000000-0000-0000-0000-000000000000".to_string());
779
780 let out = json!({
781 "data": {
782 "parameters": [],
783 "rowtype": rowtype,
784 "rowsetBase64": Value::Null,
785 "rowset": rowset,
786 "total": returned,
787 "returned": returned,
788 "queryId": query_id,
789 "queryResultFormat": "json",
790 "finalDatabaseName": Value::Null,
791 "finalSchemaName": Value::Null,
792 },
793 "message": Value::Null,
794 "code": Value::Null,
795 "success": true
796 });
797 Log::new(Some(&status_tx)).info(format!(
798 "Snowflake query conn={} auth={} decision=model_answer: {} rows",
799 connection_id, has_auth_token, returned
800 ));
801 Ok(json_200(out.to_string()))
802 }
803 Some((name, payload)) if name == "snowflake_error" => {
804 let code = payload
805 .get("code")
806 .and_then(|v| v.as_str())
807 .unwrap_or(CODE_INTERNAL);
808 let message = payload
809 .get("message")
810 .and_then(|v| v.as_str())
811 .unwrap_or("Query failed");
812 Log::new(Some(&status_tx)).info(format!(
813 "Snowflake query conn={} decision=model_reject: {}",
814 connection_id, code
815 ));
816 Ok(json_200(statement_error_envelope(
817 code,
818 message,
819 payload.get("sql_state").and_then(|v| v.as_str()),
820 )))
821 }
822 _ => {
823 Log::new(Some(&status_tx)).warn(format!(
824 "Snowflake query conn={} decision=model_silent: no query action produced; \
825 returning an error envelope",
826 connection_id
827 ));
828 Ok(json_200(statement_error_envelope(
829 CODE_INTERNAL,
830 "netget: no query result produced",
831 None,
832 )))
833 }
834 },
835 Err(e) => {
836 let overloaded = crate::llm::is_overload_error(&e);
837 Log::new(Some(&status_tx)).error(format!(
838 "Snowflake query conn={} decision={}: {}",
839 connection_id,
840 llm_error_decision(&e),
841 e
842 ));
843 let code = if overloaded {
844 CODE_REQUEST_TIMEOUT
845 } else {
846 CODE_INTERNAL
847 };
848 Ok(json_200(statement_error_envelope(
849 code,
850 crate::utils::WireFailure::classify(&e).prefixed_text(),
851 None,
852 )))
853 }
854 }
855}
856
857#[allow(clippy::too_many_arguments)]
858async fn handle_session(
859 connection_id: ConnectionId,
860 server_id: crate::state::ServerId,
861 llm_client: OllamaClient,
862 app_state: Arc<AppState>,
863 status_tx: mpsc::UnboundedSender<String>,
864 protocol: Arc<SnowflakeProtocol>,
865 operation: &str,
866 has_auth_token: bool,
867) -> Result<Response<Full<Bytes>>, Infallible> {
868 let event = Event::new(
869 &SNOWFLAKE_SESSION_EVENT,
870 json!({
871 "operation": operation,
872 "has_auth_token": has_auth_token,
873 }),
874 );
875
876 match call_llm(
877 &llm_client,
878 &app_state,
879 server_id,
880 Some(connection_id),
881 &event,
882 &*protocol,
883 )
884 .await
885 {
886 Ok(execution_result) => match first_custom(execution_result.protocol_results) {
887 Some((name, payload)) if name == "snowflake_session_response" => {
888 let data = payload.get("data").cloned().unwrap_or(json!({}));
889 let out = json!({
890 "data": data,
891 "message": Value::Null,
892 "code": Value::Null,
893 "success": true
894 });
895 Log::new(Some(&status_tx)).info(format!(
896 "Snowflake session conn={} op={} decision=model_answer",
897 connection_id, operation
898 ));
899 Ok(json_200(out.to_string()))
900 }
901 Some((name, payload)) if name == "snowflake_error" => {
902 let code = payload
903 .get("code")
904 .and_then(|v| v.as_str())
905 .unwrap_or(CODE_INTERNAL);
906 let message = payload
907 .get("message")
908 .and_then(|v| v.as_str())
909 .unwrap_or("Session request failed");
910 Log::new(Some(&status_tx)).info(format!(
911 "Snowflake session conn={} op={} decision=model_reject: {}",
912 connection_id, operation, code
913 ));
914 Ok(json_200(statement_error_envelope(
915 code,
916 message,
917 payload.get("sql_state").and_then(|v| v.as_str()),
918 )))
919 }
920 _ => {
921 if operation == "logout" {
924 Log::new(Some(&status_tx)).warn(format!(
931 "Snowflake session conn={} op=logout decision=default_logout_ack: no \
932 model answer, acking anyway (success:true was not decided by the model)",
933 connection_id
934 ));
935 Ok(json_200(
936 json!({"data": {}, "message": Value::Null, "code": Value::Null, "success": true})
937 .to_string(),
938 ))
939 } else {
940 Log::new(Some(&status_tx)).warn(format!(
941 "Snowflake session conn={} op={} decision=model_silent: no session \
942 action produced; refusing the renewal",
943 connection_id, operation
944 ));
945 Ok(json_200(statement_error_envelope(
946 CODE_INTERNAL,
947 "netget: no session decision produced",
948 None,
949 )))
950 }
951 }
952 },
953 Err(e) => {
954 Log::new(Some(&status_tx)).error(format!(
955 "Snowflake session conn={} op={} decision={}: {}",
956 connection_id,
957 operation,
958 llm_error_decision(&e),
959 e
960 ));
961 Ok(json_200(statement_error_envelope(
962 CODE_INTERNAL,
963 crate::utils::WireFailure::classify(&e).prefixed_text(),
964 None,
965 )))
966 }
967 }
968}
969
970fn normalize_rowtype(rowtype: Option<&Value>) -> Vec<Value> {
973 let arr = rowtype
974 .and_then(|v| v.as_array())
975 .cloned()
976 .unwrap_or_default();
977 arr.into_iter()
978 .map(|col| {
979 let name = col.get("name").and_then(|v| v.as_str()).unwrap_or("COL");
980 let ty = col.get("type").and_then(|v| v.as_str()).unwrap_or("text");
981 let nullable = col
982 .get("nullable")
983 .and_then(|v| v.as_bool())
984 .unwrap_or(true);
985 json!({
986 "name": name,
987 "type": ty,
988 "nullable": nullable,
989 "length": col.get("length").cloned().unwrap_or(Value::Null),
990 "precision": col.get("precision").cloned().unwrap_or(Value::Null),
991 "scale": col.get("scale").cloned().unwrap_or(Value::Null),
992 "byteLength": Value::Null,
993 "collation": Value::Null
994 })
995 })
996 .collect()
997}
998
999fn normalize_rowset(rowset: Option<&Value>) -> Vec<Value> {
1002 let arr = rowset
1003 .and_then(|v| v.as_array())
1004 .cloned()
1005 .unwrap_or_default();
1006 arr.into_iter()
1007 .map(|row| {
1008 let cells = row.as_array().cloned().unwrap_or_default();
1009 Value::Array(
1010 cells
1011 .into_iter()
1012 .map(|c| match c {
1013 Value::Null => Value::Null,
1014 Value::String(s) => Value::String(s),
1015 other => Value::String(other.to_string()),
1016 })
1017 .collect(),
1018 )
1019 })
1020 .collect()
1021}