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    /// Set only when an administrator-locked readonly profile supplied this
373    /// session, meaning the endpoint is the administrator's and environment
374    /// variables must not redirect it.
375    ///
376    /// Deliberately absent from `SessionConfigFlat`, so it can never be set by
377    /// profile JSON or by a pipe session patch — only by the code path that
378    /// loads a locked profile.
379    pub profile_pinned: bool,
380}
381
382#[derive(Serialize)]
383struct SafeSessionConfig<'a> {
384    #[serde(skip_serializing_if = "Option::is_none")]
385    dsn_secret: Option<&'static str>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    conninfo_secret: Option<&'static str>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    host: Option<&'a str>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    port: Option<u16>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    user: Option<&'a str>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    dbname: Option<&'a str>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    password_secret: Option<&'static str>,
398    #[serde(flatten)]
399    ssh: &'a SshConfig,
400    #[serde(flatten)]
401    container: &'a ContainerConfig,
402}
403
404impl Serialize for SessionConfig {
405    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
406    where
407        S: serde::Serializer,
408    {
409        SafeSessionConfig {
410            dsn_secret: self.dsn_secret.as_ref().map(|_| "***"),
411            conninfo_secret: self.conninfo_secret.as_ref().map(|_| "***"),
412            host: self.host.as_deref(),
413            port: self.port,
414            user: self.user.as_deref(),
415            dbname: self.dbname.as_deref(),
416            password_secret: self.password_secret.as_ref().map(|_| "***"),
417            ssh: &self.ssh,
418            container: &self.container,
419        }
420        .serialize(serializer)
421    }
422}
423
424#[derive(Debug, Serialize, Deserialize, Clone, Default)]
425pub struct SshConfig {
426    #[serde(rename = "ssh", skip_serializing_if = "Option::is_none")]
427    pub destination: Option<String>,
428    #[serde(rename = "ssh_via", default, skip_serializing_if = "Vec::is_empty")]
429    pub via: Vec<String>,
430    #[serde(rename = "ssh_options", default, skip_serializing_if = "Vec::is_empty")]
431    pub options: Vec<String>,
432    #[serde(rename = "ssh_local_host", skip_serializing_if = "Option::is_none")]
433    pub local_host: Option<String>,
434    #[serde(rename = "ssh_local_port", skip_serializing_if = "Option::is_none")]
435    pub local_port: Option<u16>,
436    #[serde(rename = "ssh_remote_socket", skip_serializing_if = "Option::is_none")]
437    pub remote_socket: Option<String>,
438    #[serde(rename = "ssh_sudo_user", skip_serializing_if = "Option::is_none")]
439    pub sudo_user: Option<String>,
440}
441
442impl SshConfig {
443    pub fn has_transport_fields(&self) -> bool {
444        self.destination.is_some()
445            || !self.via.is_empty()
446            || !self.options.is_empty()
447            || self.local_host.is_some()
448            || self.local_port.is_some()
449            || self.remote_socket.is_some()
450            || self.sudo_user.is_some()
451    }
452
453    pub fn has_tunnel_or_bridge_options(&self) -> bool {
454        self.local_host.is_some()
455            || self.local_port.is_some()
456            || !self.via.is_empty()
457            || self.remote_socket.is_some()
458            || self.sudo_user.is_some()
459    }
460}
461
462#[derive(Debug, Serialize, Deserialize, Clone, Default)]
463pub struct ContainerConfig {
464    #[serde(rename = "container", skip_serializing_if = "Option::is_none")]
465    pub target: Option<String>,
466    #[serde(rename = "container_driver", skip_serializing_if = "Option::is_none")]
467    pub driver: Option<String>,
468    #[serde(rename = "container_runtime", skip_serializing_if = "Option::is_none")]
469    pub runtime: Option<String>,
470    #[serde(rename = "container_user", skip_serializing_if = "Option::is_none")]
471    pub user: Option<String>,
472    #[serde(
473        rename = "container_namespace",
474        skip_serializing_if = "Option::is_none"
475    )]
476    pub namespace: Option<String>,
477    #[serde(rename = "container_context", skip_serializing_if = "Option::is_none")]
478    pub context: Option<String>,
479    #[serde(
480        rename = "container_compose_files",
481        default,
482        skip_serializing_if = "Vec::is_empty"
483    )]
484    pub compose_files: Vec<String>,
485    #[serde(
486        rename = "container_compose_project",
487        skip_serializing_if = "Option::is_none"
488    )]
489    pub compose_project: Option<String>,
490    #[serde(
491        rename = "container_pod_container",
492        skip_serializing_if = "Option::is_none"
493    )]
494    pub pod_container: Option<String>,
495}
496
497impl ContainerConfig {
498    pub fn has_transport_fields(&self) -> bool {
499        self.target.is_some()
500            || self.driver.is_some()
501            || self.runtime.is_some()
502            || self.user.is_some()
503            || self.namespace.is_some()
504            || self.context.is_some()
505            || !self.compose_files.is_empty()
506            || self.compose_project.is_some()
507            || self.pod_container.is_some()
508    }
509}
510
511#[derive(Debug, Deserialize, Default)]
512#[serde(deny_unknown_fields)]
513struct SessionConfigFlat {
514    #[serde(default)]
515    dsn_secret: Option<String>,
516    #[serde(default)]
517    conninfo_secret: Option<String>,
518    #[serde(default)]
519    host: Option<String>,
520    #[serde(default)]
521    port: Option<u16>,
522    #[serde(default)]
523    user: Option<String>,
524    #[serde(default)]
525    dbname: Option<String>,
526    #[serde(default)]
527    password_secret: Option<String>,
528    #[serde(default)]
529    ssh: Option<String>,
530    #[serde(default)]
531    ssh_via: Vec<String>,
532    #[serde(default)]
533    ssh_options: Vec<String>,
534    #[serde(default)]
535    ssh_local_host: Option<String>,
536    #[serde(default)]
537    ssh_local_port: Option<u16>,
538    #[serde(default)]
539    ssh_remote_socket: Option<String>,
540    #[serde(default)]
541    ssh_sudo_user: Option<String>,
542    #[serde(default)]
543    container: Option<String>,
544    #[serde(default)]
545    container_driver: Option<String>,
546    #[serde(default)]
547    container_runtime: Option<String>,
548    #[serde(default)]
549    container_user: Option<String>,
550    #[serde(default)]
551    container_namespace: Option<String>,
552    #[serde(default)]
553    container_context: Option<String>,
554    #[serde(default)]
555    container_compose_files: Vec<String>,
556    #[serde(default)]
557    container_compose_project: Option<String>,
558    #[serde(default)]
559    container_pod_container: Option<String>,
560}
561
562impl From<SessionConfigFlat> for SessionConfig {
563    fn from(flat: SessionConfigFlat) -> Self {
564        Self {
565            profile_pinned: false,
566            dsn_secret: flat.dsn_secret,
567            conninfo_secret: flat.conninfo_secret,
568            host: flat.host,
569            port: flat.port,
570            user: flat.user,
571            dbname: flat.dbname,
572            password_secret: flat.password_secret,
573            ssh: SshConfig {
574                destination: flat.ssh,
575                via: flat.ssh_via,
576                options: flat.ssh_options,
577                local_host: flat.ssh_local_host,
578                local_port: flat.ssh_local_port,
579                remote_socket: flat.ssh_remote_socket,
580                sudo_user: flat.ssh_sudo_user,
581            },
582            container: ContainerConfig {
583                target: flat.container,
584                driver: flat.container_driver,
585                runtime: flat.container_runtime,
586                user: flat.container_user,
587                namespace: flat.container_namespace,
588                context: flat.container_context,
589                compose_files: flat.container_compose_files,
590                compose_project: flat.container_compose_project,
591                pod_container: flat.container_pod_container,
592            },
593        }
594    }
595}
596
597impl<'de> Deserialize<'de> for SessionConfig {
598    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
599    where
600        D: serde::Deserializer<'de>,
601    {
602        SessionConfigFlat::deserialize(deserializer).map(Self::from)
603    }
604}
605
606impl SessionConfig {
607    pub fn uses_ssh_transport(&self) -> bool {
608        self.ssh.has_transport_fields()
609    }
610
611    pub fn uses_container_transport(&self) -> bool {
612        self.container.has_transport_fields()
613    }
614
615    pub fn transport_kind(&self) -> Result<TransportKind, String> {
616        let uses_ssh = self.uses_ssh_transport();
617        let uses_container = self.uses_container_transport();
618        match (uses_ssh, uses_container) {
619            (false, false) => Ok(TransportKind::Direct),
620            (true, false) => Ok(TransportKind::Ssh),
621            (false, true) => Ok(TransportKind::Container),
622            // --ssh + --container means "run container exec on that remote host".
623            // The PostgreSQL connection still crosses the container boundary.
624            (true, true) => Ok(TransportKind::Container),
625        }
626    }
627}
628
629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum TransportKind {
631    Direct,
632    Ssh,
633    Container,
634}
635
636impl TransportKind {
637    pub fn as_str(self) -> &'static str {
638        match self {
639            Self::Direct => "direct",
640            Self::Ssh => "ssh",
641            Self::Container => "container",
642        }
643    }
644}
645
646#[derive(Debug, Serialize, Deserialize, Clone)]
647pub struct RuntimeConfig {
648    pub default_session: String,
649    #[serde(default)]
650    pub sessions: HashMap<String, SessionConfig>,
651    pub inline_max_rows: usize,
652    pub inline_max_bytes: usize,
653    pub statement_timeout_ms: u64,
654    pub lock_timeout_ms: u64,
655    #[serde(default, with = "log_filters_serde")]
656    pub log: LogFilters,
657}
658
659impl Default for RuntimeConfig {
660    fn default() -> Self {
661        let mut sessions = HashMap::new();
662        sessions.insert("default".to_string(), SessionConfig::default());
663        Self {
664            default_session: "default".to_string(),
665            sessions,
666            inline_max_rows: 1000,
667            inline_max_bytes: 1_048_576,
668            statement_timeout_ms: 30_000,
669            lock_timeout_ms: 5_000,
670            log: LogFilters::default(),
671        }
672    }
673}
674
675#[derive(Debug, Deserialize, Default)]
676#[serde(deny_unknown_fields)]
677pub struct ConfigPatch {
678    pub default_session: Option<String>,
679    pub sessions: Option<HashMap<String, SessionConfigPatch>>,
680    pub inline_max_rows: Option<usize>,
681    pub inline_max_bytes: Option<usize>,
682    pub statement_timeout_ms: Option<u64>,
683    pub lock_timeout_ms: Option<u64>,
684    pub log: Option<Vec<String>>,
685}
686
687#[derive(Debug, Default)]
688pub enum PatchField<T> {
689    #[default]
690    Missing,
691    Null,
692    Value(T),
693}
694
695impl<'de, T> Deserialize<'de> for PatchField<T>
696where
697    T: Deserialize<'de>,
698{
699    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
700    where
701        D: serde::Deserializer<'de>,
702    {
703        let value = Option::<T>::deserialize(deserializer)?;
704        match value {
705            Some(value) => Ok(Self::Value(value)),
706            None => Ok(Self::Null),
707        }
708    }
709}
710
711impl<T> PatchField<T> {
712    pub fn into_update(self) -> Option<Option<T>> {
713        match self {
714            Self::Missing => None,
715            Self::Null => Some(None),
716            Self::Value(value) => Some(Some(value)),
717        }
718    }
719}
720
721#[derive(Debug, Default)]
722pub struct SessionConfigPatch {
723    pub dsn_secret: PatchField<String>,
724    pub conninfo_secret: PatchField<String>,
725    pub host: PatchField<String>,
726    pub port: PatchField<u16>,
727    pub user: PatchField<String>,
728    pub dbname: PatchField<String>,
729    pub password_secret: PatchField<String>,
730    pub ssh: SshConfigPatch,
731    pub container: ContainerConfigPatch,
732}
733
734#[derive(Debug, Default)]
735pub struct SshConfigPatch {
736    pub destination: PatchField<String>,
737    pub via: PatchField<Vec<String>>,
738    pub options: PatchField<Vec<String>>,
739    pub local_host: PatchField<String>,
740    pub local_port: PatchField<u16>,
741    pub remote_socket: PatchField<String>,
742    pub sudo_user: PatchField<String>,
743}
744
745#[derive(Debug, Default)]
746pub struct ContainerConfigPatch {
747    pub target: PatchField<String>,
748    pub driver: PatchField<String>,
749    pub runtime: PatchField<String>,
750    pub user: PatchField<String>,
751    pub namespace: PatchField<String>,
752    pub context: PatchField<String>,
753    pub compose_files: PatchField<Vec<String>>,
754    pub compose_project: PatchField<String>,
755    pub pod_container: PatchField<String>,
756}
757
758#[derive(Debug, Deserialize, Default)]
759#[serde(deny_unknown_fields)]
760struct SessionConfigPatchFlat {
761    #[serde(default)]
762    dsn_secret: PatchField<String>,
763    #[serde(default)]
764    conninfo_secret: PatchField<String>,
765    #[serde(default)]
766    host: PatchField<String>,
767    #[serde(default)]
768    port: PatchField<u16>,
769    #[serde(default)]
770    user: PatchField<String>,
771    #[serde(default)]
772    dbname: PatchField<String>,
773    #[serde(default)]
774    password_secret: PatchField<String>,
775    #[serde(default)]
776    ssh: PatchField<String>,
777    #[serde(default)]
778    ssh_via: PatchField<Vec<String>>,
779    #[serde(default)]
780    ssh_options: PatchField<Vec<String>>,
781    #[serde(default)]
782    ssh_local_host: PatchField<String>,
783    #[serde(default)]
784    ssh_local_port: PatchField<u16>,
785    #[serde(default)]
786    ssh_remote_socket: PatchField<String>,
787    #[serde(default)]
788    ssh_sudo_user: PatchField<String>,
789    #[serde(default)]
790    container: PatchField<String>,
791    #[serde(default)]
792    container_driver: PatchField<String>,
793    #[serde(default)]
794    container_runtime: PatchField<String>,
795    #[serde(default)]
796    container_user: PatchField<String>,
797    #[serde(default)]
798    container_namespace: PatchField<String>,
799    #[serde(default)]
800    container_context: PatchField<String>,
801    #[serde(default)]
802    container_compose_files: PatchField<Vec<String>>,
803    #[serde(default)]
804    container_compose_project: PatchField<String>,
805    #[serde(default)]
806    container_pod_container: PatchField<String>,
807}
808
809impl From<SessionConfigPatchFlat> for SessionConfigPatch {
810    fn from(flat: SessionConfigPatchFlat) -> Self {
811        Self {
812            dsn_secret: flat.dsn_secret,
813            conninfo_secret: flat.conninfo_secret,
814            host: flat.host,
815            port: flat.port,
816            user: flat.user,
817            dbname: flat.dbname,
818            password_secret: flat.password_secret,
819            ssh: SshConfigPatch {
820                destination: flat.ssh,
821                via: flat.ssh_via,
822                options: flat.ssh_options,
823                local_host: flat.ssh_local_host,
824                local_port: flat.ssh_local_port,
825                remote_socket: flat.ssh_remote_socket,
826                sudo_user: flat.ssh_sudo_user,
827            },
828            container: ContainerConfigPatch {
829                target: flat.container,
830                driver: flat.container_driver,
831                runtime: flat.container_runtime,
832                user: flat.container_user,
833                namespace: flat.container_namespace,
834                context: flat.container_context,
835                compose_files: flat.container_compose_files,
836                compose_project: flat.container_compose_project,
837                pod_container: flat.container_pod_container,
838            },
839        }
840    }
841}
842
843impl<'de> Deserialize<'de> for SessionConfigPatch {
844    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
845    where
846        D: serde::Deserializer<'de>,
847    {
848        SessionConfigPatchFlat::deserialize(deserializer).map(Self::from)
849    }
850}
851
852#[derive(Debug, Clone)]
853#[allow(dead_code)]
854pub struct ResolvedOptions {
855    pub stream_rows: bool,
856    pub batch_rows: usize,
857    pub batch_bytes: usize,
858    pub statement_timeout_ms: u64,
859    pub lock_timeout_ms: u64,
860    pub read_only: bool,
861    pub inline_max_rows: usize,
862    pub inline_max_bytes: usize,
863}
864
865#[cfg(test)]
866#[path = "../tests/support/unit_types.rs"]
867mod tests;