Skip to main content

agent_first_psql/
types.rs

1use agent_first_data::LogFilters;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5
6/// Serialize `LogFilters` as its plain string array for `Output::Config` and
7/// re-normalize on the (currently unused) deserialize path via afdata's parser,
8/// so afpsql never keeps a second log-filter representation.
9mod log_filters_serde {
10    use agent_first_data::{LogFilters, cli_parse_log_filters};
11    use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13    pub fn serialize<S: Serializer>(
14        filters: &LogFilters,
15        serializer: S,
16    ) -> Result<S::Ok, S::Error> {
17        filters.as_slice().serialize(serializer)
18    }
19
20    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<LogFilters, D::Error> {
21        let raw = Vec::<String>::deserialize(deserializer)?;
22        Ok(cli_parse_log_filters(&raw))
23    }
24}
25
26#[derive(Debug, Deserialize)]
27#[serde(tag = "code", deny_unknown_fields)]
28pub enum Input {
29    #[serde(rename = "query")]
30    Query {
31        id: String,
32        #[serde(default)]
33        session: Option<String>,
34        sql: String,
35        #[serde(default)]
36        params: Vec<Value>,
37        #[serde(default)]
38        options: QueryOptions,
39    },
40    #[serde(rename = "config")]
41    Config(ConfigPatch),
42    #[serde(rename = "cancel")]
43    Cancel { id: String },
44    #[serde(rename = "ping")]
45    Ping,
46    #[serde(rename = "close")]
47    Close,
48    #[serde(rename = "session_info")]
49    SessionInfo {
50        #[serde(default)]
51        id: Option<String>,
52        #[serde(default)]
53        session: Option<String>,
54    },
55    /// Open an explicit transaction on the named session. Subsequent
56    /// `query` requests on that session run on the open transaction
57    /// (no implicit `BEGIN..COMMIT` wrap) until `commit` or `rollback`.
58    #[serde(rename = "begin")]
59    Begin {
60        #[serde(default)]
61        id: Option<String>,
62        #[serde(default)]
63        session: Option<String>,
64        /// When true, send `BEGIN READ ONLY`. Default is read-write so the
65        /// caller can run writes; per-query permission still gates the SQL.
66        #[serde(default)]
67        read_only: bool,
68        /// Pass `--permission write` (or matching ssh-write/container-write)
69        /// to allow `BEGIN` on a session that defaults to read-only. Without
70        /// it, an implicit-read session rejects the begin.
71        #[serde(default)]
72        permission: Option<Permission>,
73    },
74    #[serde(rename = "commit")]
75    Commit {
76        #[serde(default)]
77        id: Option<String>,
78        #[serde(default)]
79        session: Option<String>,
80    },
81    #[serde(rename = "rollback")]
82    Rollback {
83        #[serde(default)]
84        id: Option<String>,
85        #[serde(default)]
86        session: Option<String>,
87    },
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
91pub enum Permission {
92    #[serde(rename = "read")]
93    Read,
94    #[serde(rename = "write")]
95    Write,
96    #[serde(rename = "ssh-read")]
97    SshRead,
98    #[serde(rename = "ssh-write")]
99    SshWrite,
100    #[serde(rename = "container-read")]
101    ContainerRead,
102    #[serde(rename = "container-write")]
103    ContainerWrite,
104}
105
106impl Permission {
107    pub fn as_str(self) -> &'static str {
108        match self {
109            Self::Read => "read",
110            Self::Write => "write",
111            Self::SshRead => "ssh-read",
112            Self::SshWrite => "ssh-write",
113            Self::ContainerRead => "container-read",
114            Self::ContainerWrite => "container-write",
115        }
116    }
117
118    pub fn is_read_only(self) -> bool {
119        matches!(self, Self::Read | Self::SshRead | Self::ContainerRead)
120    }
121
122    pub fn allows_ssh(self) -> bool {
123        matches!(self, Self::SshRead | Self::SshWrite)
124    }
125
126    pub fn allows_container(self) -> bool {
127        matches!(self, Self::ContainerRead | Self::ContainerWrite)
128    }
129}
130
131impl std::str::FromStr for Permission {
132    type Err = String;
133
134    fn from_str(value: &str) -> Result<Self, Self::Err> {
135        match value {
136            "read" => Ok(Self::Read),
137            "write" => Ok(Self::Write),
138            "ssh-read" => Ok(Self::SshRead),
139            "ssh-write" => Ok(Self::SshWrite),
140            "container-read" => Ok(Self::ContainerRead),
141            "container-write" => Ok(Self::ContainerWrite),
142            _ => Err(format!(
143                "invalid permission `{value}`; expected read, write, ssh-read, ssh-write, container-read, or container-write"
144            )),
145        }
146    }
147}
148
149#[derive(Debug, Deserialize, Default, Clone)]
150#[serde(deny_unknown_fields)]
151#[allow(dead_code)]
152pub struct QueryOptions {
153    #[serde(default)]
154    pub stream_rows: bool,
155    pub batch_rows: Option<usize>,
156    pub batch_bytes: Option<usize>,
157    pub statement_timeout_ms: Option<u64>,
158    pub lock_timeout_ms: Option<u64>,
159    pub permission: Option<Permission>,
160    pub inline_max_rows: Option<usize>,
161    pub inline_max_bytes: Option<usize>,
162}
163
164#[derive(Debug, Serialize)]
165#[serde(tag = "code")]
166pub enum Output {
167    #[serde(rename = "result")]
168    Result {
169        #[serde(skip_serializing_if = "Option::is_none")]
170        id: Option<String>,
171        #[serde(skip_serializing_if = "Option::is_none")]
172        session: Option<String>,
173        command_tag: String,
174        columns: Vec<ColumnInfo>,
175        rows: Vec<Value>,
176        row_count: usize,
177        /// True when `rows` is a prefix of the full result — emit when the
178        /// inline row or byte cap was hit. Default-false serializes elided.
179        #[serde(skip_serializing_if = "is_false", default)]
180        truncated: bool,
181        /// Inline-row cap if that's what fired.
182        #[serde(skip_serializing_if = "Option::is_none")]
183        truncated_at_rows: Option<usize>,
184        /// Inline-byte cap if that's what fired.
185        #[serde(skip_serializing_if = "Option::is_none")]
186        truncated_at_bytes: Option<usize>,
187        trace: Trace,
188    },
189    #[serde(rename = "result_start")]
190    ResultStart {
191        id: String,
192        #[serde(skip_serializing_if = "Option::is_none")]
193        session: Option<String>,
194        columns: Vec<ColumnInfo>,
195    },
196    #[serde(rename = "result_rows")]
197    ResultRows {
198        id: String,
199        rows: Vec<Value>,
200        rows_batch_count: usize,
201    },
202    #[serde(rename = "result_end")]
203    ResultEnd {
204        id: String,
205        #[serde(skip_serializing_if = "Option::is_none")]
206        session: Option<String>,
207        command_tag: String,
208        trace: Trace,
209    },
210    #[serde(rename = "sql_error")]
211    SqlError {
212        #[serde(skip_serializing_if = "Option::is_none")]
213        id: Option<String>,
214        #[serde(skip_serializing_if = "Option::is_none")]
215        session: Option<String>,
216        sqlstate: String,
217        message: String,
218        #[serde(skip_serializing_if = "Option::is_none")]
219        detail: Option<String>,
220        #[serde(skip_serializing_if = "Option::is_none")]
221        hint: Option<String>,
222        #[serde(skip_serializing_if = "Option::is_none")]
223        position: Option<String>,
224        trace: Trace,
225    },
226    #[serde(rename = "error")]
227    Error {
228        #[serde(skip_serializing_if = "Option::is_none")]
229        id: Option<String>,
230        error_code: String,
231        error: String,
232        #[serde(skip_serializing_if = "Option::is_none")]
233        sqlstate: Option<String>,
234        #[serde(skip_serializing_if = "Option::is_none")]
235        message: Option<String>,
236        #[serde(skip_serializing_if = "Option::is_none")]
237        detail: Option<String>,
238        #[serde(skip_serializing_if = "Option::is_none")]
239        hint: Option<String>,
240        retryable: bool,
241        trace: Trace,
242    },
243    #[serde(rename = "dry_run")]
244    DryRun {
245        #[serde(skip_serializing_if = "Option::is_none")]
246        id: Option<String>,
247        sql: String,
248        params: Vec<String>,
249        #[serde(skip_serializing_if = "Option::is_none")]
250        session: Option<String>,
251        /// Inferred PostgreSQL parameter types in placeholder order
252        /// (`$1`, `$2`, ...). Populated when the server-side PREPARE succeeds.
253        #[serde(skip_serializing_if = "Vec::is_empty", default)]
254        param_types: Vec<String>,
255        /// Output columns inferred from the prepared statement
256        /// (empty for non-SELECT statements).
257        #[serde(skip_serializing_if = "Vec::is_empty", default)]
258        columns: Vec<ColumnInfo>,
259        trace: Trace,
260    },
261    #[serde(rename = "config")]
262    Config(RuntimeConfig),
263    #[serde(rename = "pong")]
264    Pong { trace: PongTrace },
265    #[serde(rename = "close")]
266    Close { message: String, trace: CloseTrace },
267    #[serde(rename = "session_info")]
268    SessionInfo {
269        #[serde(skip_serializing_if = "Option::is_none")]
270        id: Option<String>,
271        session: String,
272        transport_kind: String,
273        permission_default: String,
274        stream_rows_default: bool,
275        batch_rows: usize,
276        batch_bytes: usize,
277        inline_max_rows: usize,
278        inline_max_bytes: usize,
279        statement_timeout_ms: u64,
280        lock_timeout_ms: u64,
281        #[serde(skip_serializing_if = "Option::is_none")]
282        database: Option<String>,
283        #[serde(skip_serializing_if = "Option::is_none")]
284        user: Option<String>,
285        #[serde(skip_serializing_if = "Option::is_none")]
286        host: Option<String>,
287        #[serde(skip_serializing_if = "Option::is_none")]
288        port: Option<u16>,
289        #[serde(skip_serializing_if = "Option::is_none")]
290        server_version: Option<String>,
291        trace: Trace,
292    },
293    #[serde(rename = "log")]
294    Log {
295        event: String,
296        #[serde(skip_serializing_if = "Option::is_none")]
297        request_id: Option<String>,
298        #[serde(skip_serializing_if = "Option::is_none")]
299        session: Option<String>,
300        #[serde(skip_serializing_if = "Option::is_none")]
301        error_code: Option<String>,
302        #[serde(skip_serializing_if = "Option::is_none")]
303        command_tag: Option<String>,
304        #[serde(skip_serializing_if = "Option::is_none")]
305        version: Option<String>,
306        #[serde(skip_serializing_if = "Option::is_none")]
307        config: Option<Value>,
308        #[serde(skip_serializing_if = "Option::is_none")]
309        args: Option<Value>,
310        #[serde(skip_serializing_if = "Option::is_none")]
311        env: Option<Value>,
312        #[serde(skip_serializing_if = "Option::is_none")]
313        chain: Option<String>,
314        trace: Trace,
315    },
316}
317
318fn is_false(b: &bool) -> bool {
319    !*b
320}
321
322#[derive(Debug, Serialize, Clone)]
323pub struct ColumnInfo {
324    pub name: String,
325    #[serde(rename = "type")]
326    pub type_name: String,
327}
328
329#[derive(Debug, Serialize, Clone)]
330pub struct Trace {
331    pub duration_ms: u64,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub row_count: Option<usize>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub payload_bytes: Option<usize>,
336}
337
338impl Trace {
339    pub fn only_duration(duration_ms: u64) -> Self {
340        Self {
341            duration_ms,
342            row_count: None,
343            payload_bytes: None,
344        }
345    }
346}
347
348#[derive(Debug, Serialize)]
349pub struct PongTrace {
350    pub uptime_s: u64,
351    pub requests_total: u64,
352    pub in_flight: usize,
353}
354
355#[derive(Debug, Serialize)]
356pub struct CloseTrace {
357    pub uptime_s: u64,
358    pub requests_total: u64,
359}
360
361#[derive(Debug, Clone, Default)]
362pub struct SessionConfig {
363    pub dsn_secret: Option<String>,
364    pub conninfo_secret: Option<String>,
365    pub host: Option<String>,
366    pub port: Option<u16>,
367    pub user: Option<String>,
368    pub dbname: Option<String>,
369    pub password_secret: Option<String>,
370    pub ssh: SshConfig,
371    pub container: ContainerConfig,
372}
373
374#[derive(Serialize)]
375struct SafeSessionConfig<'a> {
376    #[serde(skip_serializing_if = "Option::is_none")]
377    dsn_secret: Option<&'static str>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    conninfo_secret: Option<&'static str>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    host: Option<&'a str>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    port: Option<u16>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    user: Option<&'a str>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    dbname: Option<&'a str>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    password_secret: Option<&'static str>,
390    #[serde(flatten)]
391    ssh: &'a SshConfig,
392    #[serde(flatten)]
393    container: &'a ContainerConfig,
394}
395
396impl Serialize for SessionConfig {
397    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
398    where
399        S: serde::Serializer,
400    {
401        SafeSessionConfig {
402            dsn_secret: self.dsn_secret.as_ref().map(|_| "***"),
403            conninfo_secret: self.conninfo_secret.as_ref().map(|_| "***"),
404            host: self.host.as_deref(),
405            port: self.port,
406            user: self.user.as_deref(),
407            dbname: self.dbname.as_deref(),
408            password_secret: self.password_secret.as_ref().map(|_| "***"),
409            ssh: &self.ssh,
410            container: &self.container,
411        }
412        .serialize(serializer)
413    }
414}
415
416#[derive(Debug, Serialize, Deserialize, Clone, Default)]
417pub struct SshConfig {
418    #[serde(rename = "ssh", skip_serializing_if = "Option::is_none")]
419    pub destination: Option<String>,
420    #[serde(rename = "ssh_via", default, skip_serializing_if = "Vec::is_empty")]
421    pub via: Vec<String>,
422    #[serde(rename = "ssh_options", default, skip_serializing_if = "Vec::is_empty")]
423    pub options: Vec<String>,
424    #[serde(rename = "ssh_local_host", skip_serializing_if = "Option::is_none")]
425    pub local_host: Option<String>,
426    #[serde(rename = "ssh_local_port", skip_serializing_if = "Option::is_none")]
427    pub local_port: Option<u16>,
428    #[serde(rename = "ssh_remote_socket", skip_serializing_if = "Option::is_none")]
429    pub remote_socket: Option<String>,
430    #[serde(rename = "ssh_sudo_user", skip_serializing_if = "Option::is_none")]
431    pub sudo_user: Option<String>,
432}
433
434impl SshConfig {
435    pub fn has_transport_fields(&self) -> bool {
436        self.destination.is_some()
437            || !self.via.is_empty()
438            || !self.options.is_empty()
439            || self.local_host.is_some()
440            || self.local_port.is_some()
441            || self.remote_socket.is_some()
442            || self.sudo_user.is_some()
443    }
444
445    pub fn has_tunnel_or_bridge_options(&self) -> bool {
446        self.local_host.is_some()
447            || self.local_port.is_some()
448            || !self.via.is_empty()
449            || self.remote_socket.is_some()
450            || self.sudo_user.is_some()
451    }
452}
453
454#[derive(Debug, Serialize, Deserialize, Clone, Default)]
455pub struct ContainerConfig {
456    #[serde(rename = "container", skip_serializing_if = "Option::is_none")]
457    pub target: Option<String>,
458    #[serde(rename = "container_driver", skip_serializing_if = "Option::is_none")]
459    pub driver: Option<String>,
460    #[serde(rename = "container_runtime", skip_serializing_if = "Option::is_none")]
461    pub runtime: Option<String>,
462    #[serde(rename = "container_user", skip_serializing_if = "Option::is_none")]
463    pub user: Option<String>,
464    #[serde(
465        rename = "container_namespace",
466        skip_serializing_if = "Option::is_none"
467    )]
468    pub namespace: Option<String>,
469    #[serde(rename = "container_context", skip_serializing_if = "Option::is_none")]
470    pub context: Option<String>,
471    #[serde(
472        rename = "container_compose_files",
473        default,
474        skip_serializing_if = "Vec::is_empty"
475    )]
476    pub compose_files: Vec<String>,
477    #[serde(
478        rename = "container_compose_project",
479        skip_serializing_if = "Option::is_none"
480    )]
481    pub compose_project: Option<String>,
482    #[serde(
483        rename = "container_pod_container",
484        skip_serializing_if = "Option::is_none"
485    )]
486    pub pod_container: Option<String>,
487}
488
489impl ContainerConfig {
490    pub fn has_transport_fields(&self) -> bool {
491        self.target.is_some()
492            || self.driver.is_some()
493            || self.runtime.is_some()
494            || self.user.is_some()
495            || self.namespace.is_some()
496            || self.context.is_some()
497            || !self.compose_files.is_empty()
498            || self.compose_project.is_some()
499            || self.pod_container.is_some()
500    }
501}
502
503#[derive(Debug, Deserialize, Default)]
504#[serde(deny_unknown_fields)]
505struct SessionConfigFlat {
506    #[serde(default)]
507    dsn_secret: Option<String>,
508    #[serde(default)]
509    conninfo_secret: Option<String>,
510    #[serde(default)]
511    host: Option<String>,
512    #[serde(default)]
513    port: Option<u16>,
514    #[serde(default)]
515    user: Option<String>,
516    #[serde(default)]
517    dbname: Option<String>,
518    #[serde(default)]
519    password_secret: Option<String>,
520    #[serde(default)]
521    ssh: Option<String>,
522    #[serde(default)]
523    ssh_via: Vec<String>,
524    #[serde(default)]
525    ssh_options: Vec<String>,
526    #[serde(default)]
527    ssh_local_host: Option<String>,
528    #[serde(default)]
529    ssh_local_port: Option<u16>,
530    #[serde(default)]
531    ssh_remote_socket: Option<String>,
532    #[serde(default)]
533    ssh_sudo_user: Option<String>,
534    #[serde(default)]
535    container: Option<String>,
536    #[serde(default)]
537    container_driver: Option<String>,
538    #[serde(default)]
539    container_runtime: Option<String>,
540    #[serde(default)]
541    container_user: Option<String>,
542    #[serde(default)]
543    container_namespace: Option<String>,
544    #[serde(default)]
545    container_context: Option<String>,
546    #[serde(default)]
547    container_compose_files: Vec<String>,
548    #[serde(default)]
549    container_compose_project: Option<String>,
550    #[serde(default)]
551    container_pod_container: Option<String>,
552}
553
554impl From<SessionConfigFlat> for SessionConfig {
555    fn from(flat: SessionConfigFlat) -> Self {
556        Self {
557            dsn_secret: flat.dsn_secret,
558            conninfo_secret: flat.conninfo_secret,
559            host: flat.host,
560            port: flat.port,
561            user: flat.user,
562            dbname: flat.dbname,
563            password_secret: flat.password_secret,
564            ssh: SshConfig {
565                destination: flat.ssh,
566                via: flat.ssh_via,
567                options: flat.ssh_options,
568                local_host: flat.ssh_local_host,
569                local_port: flat.ssh_local_port,
570                remote_socket: flat.ssh_remote_socket,
571                sudo_user: flat.ssh_sudo_user,
572            },
573            container: ContainerConfig {
574                target: flat.container,
575                driver: flat.container_driver,
576                runtime: flat.container_runtime,
577                user: flat.container_user,
578                namespace: flat.container_namespace,
579                context: flat.container_context,
580                compose_files: flat.container_compose_files,
581                compose_project: flat.container_compose_project,
582                pod_container: flat.container_pod_container,
583            },
584        }
585    }
586}
587
588impl<'de> Deserialize<'de> for SessionConfig {
589    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
590    where
591        D: serde::Deserializer<'de>,
592    {
593        SessionConfigFlat::deserialize(deserializer).map(Self::from)
594    }
595}
596
597impl SessionConfig {
598    pub fn uses_ssh_transport(&self) -> bool {
599        self.ssh.has_transport_fields()
600    }
601
602    pub fn uses_container_transport(&self) -> bool {
603        self.container.has_transport_fields()
604    }
605
606    pub fn transport_kind(&self) -> Result<TransportKind, String> {
607        let uses_ssh = self.uses_ssh_transport();
608        let uses_container = self.uses_container_transport();
609        match (uses_ssh, uses_container) {
610            (false, false) => Ok(TransportKind::Direct),
611            (true, false) => Ok(TransportKind::Ssh),
612            (false, true) => Ok(TransportKind::Container),
613            // --ssh + --container means "run container exec on that remote host".
614            // The PostgreSQL connection still crosses the container boundary.
615            (true, true) => Ok(TransportKind::Container),
616        }
617    }
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq)]
621pub enum TransportKind {
622    Direct,
623    Ssh,
624    Container,
625}
626
627impl TransportKind {
628    pub fn as_str(self) -> &'static str {
629        match self {
630            Self::Direct => "direct",
631            Self::Ssh => "ssh",
632            Self::Container => "container",
633        }
634    }
635}
636
637#[derive(Debug, Serialize, Deserialize, Clone)]
638pub struct RuntimeConfig {
639    pub default_session: String,
640    #[serde(default)]
641    pub sessions: HashMap<String, SessionConfig>,
642    pub inline_max_rows: usize,
643    pub inline_max_bytes: usize,
644    pub statement_timeout_ms: u64,
645    pub lock_timeout_ms: u64,
646    #[serde(default, with = "log_filters_serde")]
647    pub log: LogFilters,
648}
649
650impl Default for RuntimeConfig {
651    fn default() -> Self {
652        let mut sessions = HashMap::new();
653        sessions.insert("default".to_string(), SessionConfig::default());
654        Self {
655            default_session: "default".to_string(),
656            sessions,
657            inline_max_rows: 1000,
658            inline_max_bytes: 1_048_576,
659            statement_timeout_ms: 30_000,
660            lock_timeout_ms: 5_000,
661            log: LogFilters::default(),
662        }
663    }
664}
665
666#[derive(Debug, Deserialize, Default)]
667#[serde(deny_unknown_fields)]
668pub struct ConfigPatch {
669    pub default_session: Option<String>,
670    pub sessions: Option<HashMap<String, SessionConfigPatch>>,
671    pub inline_max_rows: Option<usize>,
672    pub inline_max_bytes: Option<usize>,
673    pub statement_timeout_ms: Option<u64>,
674    pub lock_timeout_ms: Option<u64>,
675    pub log: Option<Vec<String>>,
676}
677
678#[derive(Debug, Default)]
679pub enum PatchField<T> {
680    #[default]
681    Missing,
682    Null,
683    Value(T),
684}
685
686impl<'de, T> Deserialize<'de> for PatchField<T>
687where
688    T: Deserialize<'de>,
689{
690    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
691    where
692        D: serde::Deserializer<'de>,
693    {
694        let value = Option::<T>::deserialize(deserializer)?;
695        match value {
696            Some(value) => Ok(Self::Value(value)),
697            None => Ok(Self::Null),
698        }
699    }
700}
701
702impl<T> PatchField<T> {
703    pub fn into_update(self) -> Option<Option<T>> {
704        match self {
705            Self::Missing => None,
706            Self::Null => Some(None),
707            Self::Value(value) => Some(Some(value)),
708        }
709    }
710}
711
712#[derive(Debug, Default)]
713pub struct SessionConfigPatch {
714    pub dsn_secret: PatchField<String>,
715    pub conninfo_secret: PatchField<String>,
716    pub host: PatchField<String>,
717    pub port: PatchField<u16>,
718    pub user: PatchField<String>,
719    pub dbname: PatchField<String>,
720    pub password_secret: PatchField<String>,
721    pub ssh: SshConfigPatch,
722    pub container: ContainerConfigPatch,
723}
724
725#[derive(Debug, Default)]
726pub struct SshConfigPatch {
727    pub destination: PatchField<String>,
728    pub via: PatchField<Vec<String>>,
729    pub options: PatchField<Vec<String>>,
730    pub local_host: PatchField<String>,
731    pub local_port: PatchField<u16>,
732    pub remote_socket: PatchField<String>,
733    pub sudo_user: PatchField<String>,
734}
735
736#[derive(Debug, Default)]
737pub struct ContainerConfigPatch {
738    pub target: PatchField<String>,
739    pub driver: PatchField<String>,
740    pub runtime: PatchField<String>,
741    pub user: PatchField<String>,
742    pub namespace: PatchField<String>,
743    pub context: PatchField<String>,
744    pub compose_files: PatchField<Vec<String>>,
745    pub compose_project: PatchField<String>,
746    pub pod_container: PatchField<String>,
747}
748
749#[derive(Debug, Deserialize, Default)]
750#[serde(deny_unknown_fields)]
751struct SessionConfigPatchFlat {
752    #[serde(default)]
753    dsn_secret: PatchField<String>,
754    #[serde(default)]
755    conninfo_secret: PatchField<String>,
756    #[serde(default)]
757    host: PatchField<String>,
758    #[serde(default)]
759    port: PatchField<u16>,
760    #[serde(default)]
761    user: PatchField<String>,
762    #[serde(default)]
763    dbname: PatchField<String>,
764    #[serde(default)]
765    password_secret: PatchField<String>,
766    #[serde(default)]
767    ssh: PatchField<String>,
768    #[serde(default)]
769    ssh_via: PatchField<Vec<String>>,
770    #[serde(default)]
771    ssh_options: PatchField<Vec<String>>,
772    #[serde(default)]
773    ssh_local_host: PatchField<String>,
774    #[serde(default)]
775    ssh_local_port: PatchField<u16>,
776    #[serde(default)]
777    ssh_remote_socket: PatchField<String>,
778    #[serde(default)]
779    ssh_sudo_user: PatchField<String>,
780    #[serde(default)]
781    container: PatchField<String>,
782    #[serde(default)]
783    container_driver: PatchField<String>,
784    #[serde(default)]
785    container_runtime: PatchField<String>,
786    #[serde(default)]
787    container_user: PatchField<String>,
788    #[serde(default)]
789    container_namespace: PatchField<String>,
790    #[serde(default)]
791    container_context: PatchField<String>,
792    #[serde(default)]
793    container_compose_files: PatchField<Vec<String>>,
794    #[serde(default)]
795    container_compose_project: PatchField<String>,
796    #[serde(default)]
797    container_pod_container: PatchField<String>,
798}
799
800impl From<SessionConfigPatchFlat> for SessionConfigPatch {
801    fn from(flat: SessionConfigPatchFlat) -> Self {
802        Self {
803            dsn_secret: flat.dsn_secret,
804            conninfo_secret: flat.conninfo_secret,
805            host: flat.host,
806            port: flat.port,
807            user: flat.user,
808            dbname: flat.dbname,
809            password_secret: flat.password_secret,
810            ssh: SshConfigPatch {
811                destination: flat.ssh,
812                via: flat.ssh_via,
813                options: flat.ssh_options,
814                local_host: flat.ssh_local_host,
815                local_port: flat.ssh_local_port,
816                remote_socket: flat.ssh_remote_socket,
817                sudo_user: flat.ssh_sudo_user,
818            },
819            container: ContainerConfigPatch {
820                target: flat.container,
821                driver: flat.container_driver,
822                runtime: flat.container_runtime,
823                user: flat.container_user,
824                namespace: flat.container_namespace,
825                context: flat.container_context,
826                compose_files: flat.container_compose_files,
827                compose_project: flat.container_compose_project,
828                pod_container: flat.container_pod_container,
829            },
830        }
831    }
832}
833
834impl<'de> Deserialize<'de> for SessionConfigPatch {
835    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
836    where
837        D: serde::Deserializer<'de>,
838    {
839        SessionConfigPatchFlat::deserialize(deserializer).map(Self::from)
840    }
841}
842
843#[derive(Debug, Clone)]
844#[allow(dead_code)]
845pub struct ResolvedOptions {
846    pub stream_rows: bool,
847    pub batch_rows: usize,
848    pub batch_bytes: usize,
849    pub statement_timeout_ms: u64,
850    pub lock_timeout_ms: u64,
851    pub read_only: bool,
852    pub inline_max_rows: usize,
853    pub inline_max_bytes: usize,
854}
855
856#[cfg(test)]
857#[path = "../tests/support/unit_types.rs"]
858mod tests;