Skip to main content

franken_snowflake_sqlapi/
request.rs

1//! The `POST /api/v2/statements` request body and its query parameters.
2//!
3//! Snowflake JSON keys are camelCase, so the structs use
4//! `#[serde(rename_all = "camelCase")]`; absent optionals are omitted on the
5//! wire (`skip_serializing_if`) so a minimal request serializes to just
6//! `{"statement":"..."}`. Identifier fields reuse the `franken-snowflake-core`
7//! newtypes, which serialize transparently as bare strings.
8
9use std::collections::BTreeMap;
10
11use franken_snowflake_core::ids::{DatabaseName, RoleName, SchemaName, WarehouseName};
12use serde::{Deserialize, Serialize};
13
14/// The body of a `POST /api/v2/statements` submit.
15///
16/// The idempotency `requestId`, `retry`, and `async` controls are **query
17/// parameters** (see [`SubmitQueryParams`]), not body fields. `bindings` are not
18/// permitted together with multi-statement requests — that refusal is enforced
19/// by the planner, not the schema.
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct SubmitStatementRequest {
23    /// The SQL text. A single statement unless `MULTI_STATEMENT_COUNT` is set in
24    /// [`SubmitStatementRequest::parameters`].
25    pub statement: String,
26
27    /// Server-side statement timeout in seconds (`STATEMENT_TIMEOUT_IN_SECONDS`).
28    /// The enforceable cost/runtime guardrail; the client `Budget` is advisory.
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    pub timeout: Option<u32>,
31
32    /// Default database for the statement's session.
33    #[serde(skip_serializing_if = "Option::is_none", default)]
34    pub database: Option<DatabaseName>,
35
36    /// Default schema for the statement's session.
37    #[serde(skip_serializing_if = "Option::is_none", default)]
38    pub schema: Option<SchemaName>,
39
40    /// Warehouse that runs the statement.
41    #[serde(skip_serializing_if = "Option::is_none", default)]
42    pub warehouse: Option<WarehouseName>,
43
44    /// Role the statement runs as.
45    #[serde(skip_serializing_if = "Option::is_none", default)]
46    pub role: Option<RoleName>,
47
48    /// Positional typed bind values, keyed by 1-based **string** indices.
49    #[serde(skip_serializing_if = "Option::is_none", default)]
50    pub bindings: Option<BTreeMap<String, Binding>>,
51
52    /// Session parameters pinned for deterministic output (e.g. `TIMEZONE`, the
53    /// `*_OUTPUT_FORMAT` params, `USE_CACHED_RESULT`, `MULTI_STATEMENT_COUNT`).
54    /// Values are strings on the wire.
55    #[serde(skip_serializing_if = "Option::is_none", default)]
56    pub parameters: Option<BTreeMap<String, String>>,
57}
58
59impl SubmitStatementRequest {
60    /// A bare single-statement request with no session overrides.
61    #[must_use]
62    pub fn new(statement: impl Into<String>) -> Self {
63        Self {
64            statement: statement.into(),
65            timeout: None,
66            database: None,
67            schema: None,
68            warehouse: None,
69            role: None,
70            bindings: None,
71            parameters: None,
72        }
73    }
74}
75
76/// A single positional, typed bind value.
77///
78/// Snowflake keys bindings by 1-based string index and the `value` is **always**
79/// a JSON string regardless of the logical type (e.g. a number bind is
80/// `{"type":"FIXED","value":"42"}`). The type name is uppercase on the wire and
81/// kept as a `String` for lossless round-trips across Snowflake's open type set;
82/// see [`bind_type`] for the common names.
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
84pub struct Binding {
85    /// The Snowflake binding type name (uppercase), e.g. `TEXT`, `FIXED`,
86    /// `BOOLEAN`, `TIMESTAMP_NTZ`.
87    #[serde(rename = "type")]
88    pub value_type: String,
89    /// The bound value, JSON-string-encoded.
90    pub value: String,
91}
92
93impl Binding {
94    /// Construct a binding from a type name and string-encoded value.
95    #[must_use]
96    pub fn new(value_type: impl Into<String>, value: impl Into<String>) -> Self {
97        Self {
98            value_type: value_type.into(),
99            value: value.into(),
100        }
101    }
102}
103
104/// Common Snowflake binding type names (the `type` field of a [`Binding`]).
105pub mod bind_type {
106    /// Text / VARCHAR.
107    pub const TEXT: &str = "TEXT";
108    /// Fixed-point numeric (NUMBER).
109    pub const FIXED: &str = "FIXED";
110    /// Floating point.
111    pub const REAL: &str = "REAL";
112    /// Boolean.
113    pub const BOOLEAN: &str = "BOOLEAN";
114    /// Calendar date.
115    pub const DATE: &str = "DATE";
116    /// Wall-clock time.
117    pub const TIME: &str = "TIME";
118    /// Timestamp without time zone.
119    pub const TIMESTAMP_NTZ: &str = "TIMESTAMP_NTZ";
120    /// Timestamp with local time zone.
121    pub const TIMESTAMP_LTZ: &str = "TIMESTAMP_LTZ";
122    /// Timestamp with time zone.
123    pub const TIMESTAMP_TZ: &str = "TIMESTAMP_TZ";
124    /// Binary.
125    pub const BINARY: &str = "BINARY";
126}
127
128/// The submit-time **query parameters** that ride on the `POST` URL rather than
129/// the body. They are the idempotency contract: a stable [`SubmitQueryParams::request_id`]
130/// plus `retry=true` makes a resubmit safe (the original result is returned
131/// instead of re-running). This struct is a typed carrier for the transport
132/// crate to render into a query string; it is not serialized into the body.
133#[derive(Clone, Debug, PartialEq, Eq, Default)]
134pub struct SubmitQueryParams {
135    /// Client-generated UUID; the SQL API idempotency `requestId`.
136    pub request_id: Option<String>,
137    /// `retry=true` marks a safe resubmit of a previously-sent `requestId`.
138    pub retry: bool,
139    /// `async=true` returns a handle immediately instead of waiting.
140    pub asynchronous: bool,
141    /// `nullable=false` renders SQL NULL as the string `"null"` instead of JSON
142    /// `null`. Unrelated to `rowType[].nullable`.
143    pub nullable: Option<bool>,
144}
145
146impl SubmitQueryParams {
147    /// Render the non-default parameters as `(key, value)` query pairs, in a
148    /// stable order, for the transport crate to URL-encode.
149    #[must_use]
150    pub fn to_query_pairs(&self) -> Vec<(&'static str, String)> {
151        let mut pairs = Vec::new();
152        if let Some(id) = &self.request_id {
153            pairs.push(("requestId", id.clone()));
154        }
155        if self.retry {
156            pairs.push(("retry", "true".to_owned()));
157        }
158        if self.asynchronous {
159            pairs.push(("async", "true".to_owned()));
160        }
161        if let Some(nullable) = self.nullable {
162            pairs.push(("nullable", nullable.to_string()));
163        }
164        pairs
165    }
166}