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