Skip to main content

agent_first_psql/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5#[derive(Debug, Deserialize)]
6#[serde(tag = "code", deny_unknown_fields)]
7pub enum Input {
8    #[serde(rename = "query")]
9    Query {
10        id: String,
11        #[serde(default)]
12        session: Option<String>,
13        sql: String,
14        #[serde(default)]
15        params: Vec<Value>,
16        #[serde(default)]
17        options: QueryOptions,
18    },
19    #[serde(rename = "config")]
20    Config(ConfigPatch),
21    #[serde(rename = "cancel")]
22    Cancel { id: String },
23    #[serde(rename = "ping")]
24    Ping,
25    #[serde(rename = "close")]
26    Close,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum Permission {
31    #[serde(rename = "read")]
32    Read,
33    #[serde(rename = "write")]
34    Write,
35    #[serde(rename = "ssh-read")]
36    SshRead,
37    #[serde(rename = "ssh-write")]
38    SshWrite,
39}
40
41impl Permission {
42    pub fn as_str(self) -> &'static str {
43        match self {
44            Self::Read => "read",
45            Self::Write => "write",
46            Self::SshRead => "ssh-read",
47            Self::SshWrite => "ssh-write",
48        }
49    }
50
51    pub fn is_read_only(self) -> bool {
52        matches!(self, Self::Read | Self::SshRead)
53    }
54
55    pub fn allows_ssh(self) -> bool {
56        matches!(self, Self::SshRead | Self::SshWrite)
57    }
58}
59
60impl std::str::FromStr for Permission {
61    type Err = String;
62
63    fn from_str(value: &str) -> Result<Self, Self::Err> {
64        match value {
65            "read" => Ok(Self::Read),
66            "write" => Ok(Self::Write),
67            "ssh-read" => Ok(Self::SshRead),
68            "ssh-write" => Ok(Self::SshWrite),
69            _ => Err(format!(
70                "invalid permission `{value}`; expected read, write, ssh-read, or ssh-write"
71            )),
72        }
73    }
74}
75
76#[derive(Debug, Deserialize, Default, Clone)]
77#[serde(deny_unknown_fields)]
78#[allow(dead_code)]
79pub struct QueryOptions {
80    #[serde(default)]
81    pub stream_rows: bool,
82    pub batch_rows: Option<usize>,
83    pub batch_bytes: Option<usize>,
84    pub statement_timeout_ms: Option<u64>,
85    pub lock_timeout_ms: Option<u64>,
86    pub permission: Option<Permission>,
87    pub inline_max_rows: Option<usize>,
88    pub inline_max_bytes: Option<usize>,
89}
90
91#[derive(Debug, Serialize)]
92#[serde(tag = "code")]
93pub enum Output {
94    #[serde(rename = "result")]
95    Result {
96        #[serde(skip_serializing_if = "Option::is_none")]
97        id: Option<String>,
98        #[serde(skip_serializing_if = "Option::is_none")]
99        session: Option<String>,
100        command_tag: String,
101        columns: Vec<ColumnInfo>,
102        rows: Vec<Value>,
103        row_count: usize,
104        trace: Trace,
105    },
106    #[serde(rename = "result_start")]
107    ResultStart {
108        id: String,
109        #[serde(skip_serializing_if = "Option::is_none")]
110        session: Option<String>,
111        columns: Vec<ColumnInfo>,
112    },
113    #[serde(rename = "result_rows")]
114    ResultRows {
115        id: String,
116        rows: Vec<Value>,
117        rows_batch_count: usize,
118    },
119    #[serde(rename = "result_end")]
120    ResultEnd {
121        id: String,
122        #[serde(skip_serializing_if = "Option::is_none")]
123        session: Option<String>,
124        command_tag: String,
125        trace: Trace,
126    },
127    #[serde(rename = "sql_error")]
128    SqlError {
129        #[serde(skip_serializing_if = "Option::is_none")]
130        id: Option<String>,
131        #[serde(skip_serializing_if = "Option::is_none")]
132        session: Option<String>,
133        sqlstate: String,
134        message: String,
135        #[serde(skip_serializing_if = "Option::is_none")]
136        detail: Option<String>,
137        #[serde(skip_serializing_if = "Option::is_none")]
138        hint: Option<String>,
139        #[serde(skip_serializing_if = "Option::is_none")]
140        position: Option<String>,
141        trace: Trace,
142    },
143    #[serde(rename = "error")]
144    Error {
145        #[serde(skip_serializing_if = "Option::is_none")]
146        id: Option<String>,
147        error_code: String,
148        error: String,
149        #[serde(skip_serializing_if = "Option::is_none")]
150        hint: Option<String>,
151        retryable: bool,
152        trace: Trace,
153    },
154    #[serde(rename = "dry_run")]
155    DryRun {
156        #[serde(skip_serializing_if = "Option::is_none")]
157        id: Option<String>,
158        sql: String,
159        params: Vec<String>,
160        #[serde(skip_serializing_if = "Option::is_none")]
161        session: Option<String>,
162        trace: Trace,
163    },
164    #[serde(rename = "config")]
165    Config(RuntimeConfig),
166    #[serde(rename = "pong")]
167    Pong { trace: PongTrace },
168    #[serde(rename = "close")]
169    Close { message: String, trace: CloseTrace },
170    #[serde(rename = "log")]
171    Log {
172        event: String,
173        #[serde(skip_serializing_if = "Option::is_none")]
174        request_id: Option<String>,
175        #[serde(skip_serializing_if = "Option::is_none")]
176        session: Option<String>,
177        #[serde(skip_serializing_if = "Option::is_none")]
178        error_code: Option<String>,
179        #[serde(skip_serializing_if = "Option::is_none")]
180        command_tag: Option<String>,
181        #[serde(skip_serializing_if = "Option::is_none")]
182        version: Option<String>,
183        #[serde(skip_serializing_if = "Option::is_none")]
184        config: Option<Value>,
185        #[serde(skip_serializing_if = "Option::is_none")]
186        args: Option<Value>,
187        #[serde(skip_serializing_if = "Option::is_none")]
188        env: Option<Value>,
189        trace: Trace,
190    },
191}
192
193#[derive(Debug, Serialize, Clone)]
194pub struct ColumnInfo {
195    pub name: String,
196    #[serde(rename = "type")]
197    pub type_name: String,
198}
199
200#[derive(Debug, Serialize, Clone)]
201pub struct Trace {
202    pub duration_ms: u64,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub row_count: Option<usize>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub payload_bytes: Option<usize>,
207}
208
209impl Trace {
210    pub fn only_duration(duration_ms: u64) -> Self {
211        Self {
212            duration_ms,
213            row_count: None,
214            payload_bytes: None,
215        }
216    }
217}
218
219#[derive(Debug, Serialize)]
220pub struct PongTrace {
221    pub uptime_s: u64,
222    pub requests_total: u64,
223    pub in_flight: usize,
224}
225
226#[derive(Debug, Serialize)]
227pub struct CloseTrace {
228    pub uptime_s: u64,
229    pub requests_total: u64,
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone, Default)]
233#[serde(deny_unknown_fields)]
234pub struct SessionConfig {
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub dsn_secret: Option<String>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub conninfo_secret: Option<String>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub host: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub port: Option<u16>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub user: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub dbname: Option<String>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub password_secret: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub ssh: Option<String>,
251    #[serde(default, skip_serializing_if = "Vec::is_empty")]
252    pub ssh_options: Vec<String>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub ssh_local_host: Option<String>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub ssh_local_port: Option<u16>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub ssh_remote_socket: Option<String>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub ssh_sudo_user: Option<String>,
261}
262
263impl SessionConfig {
264    pub fn uses_ssh_transport(&self) -> bool {
265        self.ssh.is_some()
266            || !self.ssh_options.is_empty()
267            || self.ssh_local_host.is_some()
268            || self.ssh_local_port.is_some()
269            || self.ssh_remote_socket.is_some()
270            || self.ssh_sudo_user.is_some()
271    }
272}
273
274#[derive(Debug, Serialize, Deserialize, Clone)]
275pub struct RuntimeConfig {
276    pub default_session: String,
277    #[serde(default)]
278    pub sessions: HashMap<String, SessionConfig>,
279    pub inline_max_rows: usize,
280    pub inline_max_bytes: usize,
281    pub statement_timeout_ms: u64,
282    pub lock_timeout_ms: u64,
283    #[serde(default)]
284    pub log: Vec<String>,
285}
286
287impl Default for RuntimeConfig {
288    fn default() -> Self {
289        let mut sessions = HashMap::new();
290        sessions.insert("default".to_string(), SessionConfig::default());
291        Self {
292            default_session: "default".to_string(),
293            sessions,
294            inline_max_rows: 1000,
295            inline_max_bytes: 1_048_576,
296            statement_timeout_ms: 30_000,
297            lock_timeout_ms: 5_000,
298            log: vec![],
299        }
300    }
301}
302
303#[derive(Debug, Deserialize, Default)]
304#[serde(deny_unknown_fields)]
305pub struct ConfigPatch {
306    pub default_session: Option<String>,
307    pub sessions: Option<HashMap<String, SessionConfigPatch>>,
308    pub inline_max_rows: Option<usize>,
309    pub inline_max_bytes: Option<usize>,
310    pub statement_timeout_ms: Option<u64>,
311    pub lock_timeout_ms: Option<u64>,
312    pub log: Option<Vec<String>>,
313}
314
315#[derive(Debug, Default)]
316pub enum PatchField<T> {
317    #[default]
318    Missing,
319    Null,
320    Value(T),
321}
322
323impl<'de, T> Deserialize<'de> for PatchField<T>
324where
325    T: Deserialize<'de>,
326{
327    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
328    where
329        D: serde::Deserializer<'de>,
330    {
331        let value = Option::<T>::deserialize(deserializer)?;
332        match value {
333            Some(value) => Ok(Self::Value(value)),
334            None => Ok(Self::Null),
335        }
336    }
337}
338
339impl<T> PatchField<T> {
340    pub fn into_update(self) -> Option<Option<T>> {
341        match self {
342            Self::Missing => None,
343            Self::Null => Some(None),
344            Self::Value(value) => Some(Some(value)),
345        }
346    }
347}
348
349#[derive(Debug, Deserialize, Default)]
350#[serde(deny_unknown_fields)]
351pub struct SessionConfigPatch {
352    #[serde(default)]
353    pub dsn_secret: PatchField<String>,
354    #[serde(default)]
355    pub conninfo_secret: PatchField<String>,
356    #[serde(default)]
357    pub host: PatchField<String>,
358    #[serde(default)]
359    pub port: PatchField<u16>,
360    #[serde(default)]
361    pub user: PatchField<String>,
362    #[serde(default)]
363    pub dbname: PatchField<String>,
364    #[serde(default)]
365    pub password_secret: PatchField<String>,
366    #[serde(default)]
367    pub ssh: PatchField<String>,
368    #[serde(default)]
369    pub ssh_options: PatchField<Vec<String>>,
370    #[serde(default)]
371    pub ssh_local_host: PatchField<String>,
372    #[serde(default)]
373    pub ssh_local_port: PatchField<u16>,
374    #[serde(default)]
375    pub ssh_remote_socket: PatchField<String>,
376    #[serde(default)]
377    pub ssh_sudo_user: PatchField<String>,
378}
379
380#[derive(Debug, Clone)]
381#[allow(dead_code)]
382pub struct ResolvedOptions {
383    pub stream_rows: bool,
384    pub batch_rows: usize,
385    pub batch_bytes: usize,
386    pub statement_timeout_ms: u64,
387    pub lock_timeout_ms: u64,
388    pub read_only: bool,
389    pub inline_max_rows: usize,
390    pub inline_max_bytes: usize,
391}
392
393#[cfg(test)]
394#[path = "../tests/support/unit_types.rs"]
395mod tests;