1use agent_first_data::LogFilters;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5
6mod 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 #[serde(rename = "begin")]
59 Begin {
60 #[serde(default)]
61 id: Option<String>,
62 #[serde(default)]
63 session: Option<String>,
64 #[serde(default = "default_true")]
68 read_only: bool,
69 #[serde(default)]
73 permission: Option<Permission>,
74 },
75 #[serde(rename = "commit")]
76 Commit {
77 #[serde(default)]
78 id: Option<String>,
79 #[serde(default)]
80 session: Option<String>,
81 },
82 #[serde(rename = "rollback")]
83 Rollback {
84 #[serde(default)]
85 id: Option<String>,
86 #[serde(default)]
87 session: Option<String>,
88 },
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum Permission {
93 #[serde(rename = "read")]
94 Read,
95 #[serde(rename = "write")]
96 Write,
97 #[serde(rename = "ssh-read")]
98 SshRead,
99 #[serde(rename = "ssh-write")]
100 SshWrite,
101 #[serde(rename = "container-read")]
102 ContainerRead,
103 #[serde(rename = "container-write")]
104 ContainerWrite,
105}
106
107impl Permission {
108 pub fn as_str(self) -> &'static str {
109 match self {
110 Self::Read => "read",
111 Self::Write => "write",
112 Self::SshRead => "ssh-read",
113 Self::SshWrite => "ssh-write",
114 Self::ContainerRead => "container-read",
115 Self::ContainerWrite => "container-write",
116 }
117 }
118
119 pub fn is_read_only(self) -> bool {
120 matches!(self, Self::Read | Self::SshRead | Self::ContainerRead)
121 }
122
123 pub fn allows_ssh(self) -> bool {
124 matches!(self, Self::SshRead | Self::SshWrite)
125 }
126
127 pub fn allows_container(self) -> bool {
128 matches!(self, Self::ContainerRead | Self::ContainerWrite)
129 }
130}
131
132fn default_true() -> bool {
133 true
134}
135
136impl std::str::FromStr for Permission {
137 type Err = String;
138
139 fn from_str(value: &str) -> Result<Self, Self::Err> {
140 match value {
141 "read" => Ok(Self::Read),
142 "write" => Ok(Self::Write),
143 "ssh-read" => Ok(Self::SshRead),
144 "ssh-write" => Ok(Self::SshWrite),
145 "container-read" => Ok(Self::ContainerRead),
146 "container-write" => Ok(Self::ContainerWrite),
147 _ => Err(format!(
148 "invalid permission `{value}`; expected read, write, ssh-read, ssh-write, container-read, or container-write"
149 )),
150 }
151 }
152}
153
154#[derive(Debug, Deserialize, Default, Clone)]
155#[serde(deny_unknown_fields)]
156pub struct QueryOptions {
157 #[serde(default)]
158 pub stream_rows: bool,
159 pub batch_rows: Option<usize>,
160 pub batch_bytes: Option<usize>,
161 pub statement_timeout_ms: Option<u64>,
162 pub lock_timeout_ms: Option<u64>,
163 pub permission: Option<Permission>,
164 pub inline_max_rows: Option<usize>,
165 pub inline_max_bytes: Option<usize>,
166}
167
168#[derive(Debug, Serialize)]
169#[serde(tag = "code")]
170pub enum Output {
171 #[serde(rename = "result")]
172 Result {
173 #[serde(skip_serializing_if = "Option::is_none")]
174 id: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 session: Option<String>,
177 command_tag: String,
178 columns: Vec<ColumnInfo>,
179 rows: Vec<Value>,
180 row_count: usize,
181 #[serde(skip_serializing_if = "is_false", default)]
184 truncated: bool,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 truncated_at_rows: Option<usize>,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 truncated_at_bytes: Option<usize>,
191 trace: Trace,
192 },
193 #[serde(rename = "result_start")]
194 ResultStart {
195 id: String,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 session: Option<String>,
198 columns: Vec<ColumnInfo>,
199 },
200 #[serde(rename = "result_rows")]
201 ResultRows {
202 id: String,
203 rows: Vec<Value>,
204 rows_batch_count: usize,
205 },
206 #[serde(rename = "result_end")]
207 ResultEnd {
208 id: String,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 session: Option<String>,
211 command_tag: String,
212 trace: Trace,
213 },
214 #[serde(rename = "sql_error")]
215 SqlError {
216 #[serde(skip_serializing_if = "Option::is_none")]
217 id: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 session: Option<String>,
220 sqlstate: String,
221 message: String,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 detail: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 hint: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 position: Option<String>,
228 retryable: bool,
229 trace: Trace,
230 },
231 #[serde(rename = "error")]
232 Error {
233 #[serde(skip_serializing_if = "Option::is_none")]
234 id: Option<String>,
235 error_code: String,
236 error: String,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 sqlstate: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 message: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 detail: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 hint: Option<String>,
245 retryable: bool,
246 trace: Trace,
247 },
248 #[serde(rename = "dry_run")]
249 DryRun {
250 #[serde(skip_serializing_if = "Option::is_none")]
251 id: Option<String>,
252 sql: String,
253 params: Vec<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 session: Option<String>,
256 #[serde(skip_serializing_if = "Vec::is_empty", default)]
259 param_types: Vec<String>,
260 #[serde(skip_serializing_if = "Vec::is_empty", default)]
263 columns: Vec<ColumnInfo>,
264 trace: Trace,
265 },
266 #[serde(rename = "config")]
267 Config(RuntimeConfig),
268 #[serde(rename = "pong")]
269 Pong { trace: PongTrace },
270 #[serde(rename = "close")]
271 Close { message: String, trace: CloseTrace },
272 #[serde(rename = "session_info")]
273 SessionInfo {
274 #[serde(skip_serializing_if = "Option::is_none")]
275 id: Option<String>,
276 session: String,
277 transport_kind: String,
278 permission_default: String,
279 stream_rows_default: bool,
280 batch_rows: usize,
281 batch_bytes: usize,
282 inline_max_rows: usize,
283 inline_max_bytes: usize,
284 statement_timeout_ms: u64,
285 lock_timeout_ms: u64,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 database: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 user: Option<String>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 host: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 port: Option<u16>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 server_version: Option<String>,
296 trace: Trace,
297 },
298 #[serde(rename = "log")]
299 Log {
300 event: String,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 request_id: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 session: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 error_code: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 command_tag: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 version: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 config: Option<Value>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 args: Option<Value>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 env: Option<Value>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 chain: Option<String>,
319 trace: Trace,
320 },
321}
322
323fn is_false(b: &bool) -> bool {
324 !*b
325}
326
327#[derive(Debug, Serialize, Clone)]
328pub struct ColumnInfo {
329 pub name: String,
330 #[serde(rename = "type")]
331 pub type_name: String,
332}
333
334#[derive(Debug, Serialize, Clone)]
335pub struct Trace {
336 pub duration_ms: u64,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub row_count: Option<usize>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub payload_bytes: Option<usize>,
341}
342
343impl Trace {
344 pub fn only_duration(duration_ms: u64) -> Self {
345 Self {
346 duration_ms,
347 row_count: None,
348 payload_bytes: None,
349 }
350 }
351}
352
353#[derive(Debug, Serialize)]
354pub struct PongTrace {
355 pub uptime_s: u64,
356 pub requests_total: u64,
357 pub in_flight: usize,
358}
359
360#[derive(Debug, Serialize)]
361pub struct CloseTrace {
362 pub uptime_s: u64,
363 pub requests_total: u64,
364}
365
366#[derive(Debug, Clone, Default)]
367pub struct SessionConfig {
368 pub dsn_secret: Option<String>,
369 pub conninfo_secret: Option<String>,
370 pub host: Option<String>,
371 pub port: Option<u16>,
372 pub user: Option<String>,
373 pub dbname: Option<String>,
374 pub password_secret: Option<String>,
375 pub ssh: SshConfig,
376 pub container: ContainerConfig,
377 pub profile_pinned: bool,
385}
386
387#[derive(Serialize)]
388struct SafeSessionConfig<'a> {
389 #[serde(skip_serializing_if = "Option::is_none")]
390 dsn_secret: Option<&'static str>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 conninfo_secret: Option<&'static str>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 host: Option<&'a str>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 port: Option<u16>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 user: Option<&'a str>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 dbname: Option<&'a str>,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 password_secret: Option<&'static str>,
403 #[serde(flatten)]
404 ssh: &'a SshConfig,
405 #[serde(flatten)]
406 container: &'a ContainerConfig,
407}
408
409impl Serialize for SessionConfig {
410 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
411 where
412 S: serde::Serializer,
413 {
414 SafeSessionConfig {
415 dsn_secret: self.dsn_secret.as_ref().map(|_| "***"),
416 conninfo_secret: self.conninfo_secret.as_ref().map(|_| "***"),
417 host: self.host.as_deref(),
418 port: self.port,
419 user: self.user.as_deref(),
420 dbname: self.dbname.as_deref(),
421 password_secret: self.password_secret.as_ref().map(|_| "***"),
422 ssh: &self.ssh,
423 container: &self.container,
424 }
425 .serialize(serializer)
426 }
427}
428
429#[derive(Debug, Serialize, Deserialize, Clone, Default)]
430pub struct SshConfig {
431 #[serde(rename = "ssh", skip_serializing_if = "Option::is_none")]
432 pub destination: Option<String>,
433 #[serde(rename = "ssh_via", default, skip_serializing_if = "Vec::is_empty")]
434 pub via: Vec<String>,
435 #[serde(rename = "ssh_options", default, skip_serializing_if = "Vec::is_empty")]
436 pub options: Vec<String>,
437 #[serde(rename = "ssh_local_host", skip_serializing_if = "Option::is_none")]
438 pub local_host: Option<String>,
439 #[serde(rename = "ssh_local_port", skip_serializing_if = "Option::is_none")]
440 pub local_port: Option<u16>,
441 #[serde(rename = "ssh_remote_socket", skip_serializing_if = "Option::is_none")]
442 pub remote_socket: Option<String>,
443 #[serde(rename = "ssh_sudo_user", skip_serializing_if = "Option::is_none")]
444 pub sudo_user: Option<String>,
445}
446
447impl SshConfig {
448 pub fn has_transport_fields(&self) -> bool {
449 self.destination.is_some()
450 || !self.via.is_empty()
451 || !self.options.is_empty()
452 || self.local_host.is_some()
453 || self.local_port.is_some()
454 || self.remote_socket.is_some()
455 || self.sudo_user.is_some()
456 }
457
458 pub fn has_tunnel_or_bridge_options(&self) -> bool {
459 self.local_host.is_some()
460 || self.local_port.is_some()
461 || !self.via.is_empty()
462 || self.remote_socket.is_some()
463 || self.sudo_user.is_some()
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
474pub enum ContainerDriver {
475 Docker,
476 Podman,
477 Nerdctl,
478 Compose,
479 Kubectl,
480}
481
482impl ContainerDriver {
483 pub fn as_str(self) -> &'static str {
484 match self {
485 Self::Docker => "docker",
486 Self::Podman => "podman",
487 Self::Nerdctl => "nerdctl",
488 Self::Compose => "compose",
489 Self::Kubectl => "kubectl",
490 }
491 }
492
493 pub fn default_runtime(self) -> &'static str {
495 match self {
496 Self::Docker | Self::Compose => "docker",
497 Self::Podman => "podman",
498 Self::Nerdctl => "nerdctl",
499 Self::Kubectl => "kubectl",
500 }
501 }
502
503 pub fn target_flag(self) -> &'static str {
505 match self {
506 Self::Docker => "--container-docker-name",
507 Self::Podman => "--container-podman-name",
508 Self::Nerdctl => "--container-nerdctl-name",
509 Self::Compose => "--container-compose-service",
510 Self::Kubectl => "--container-kubectl-pod",
511 }
512 }
513
514 pub fn flag_family(self) -> &'static str {
516 match self {
517 Self::Docker => "--container-docker-*",
518 Self::Podman => "--container-podman-*",
519 Self::Nerdctl => "--container-nerdctl-*",
520 Self::Compose => "--container-compose-*",
521 Self::Kubectl => "--container-kubectl-*",
522 }
523 }
524}
525
526#[derive(Debug, Serialize, Deserialize, Clone, Default)]
533pub struct ContainerConfig {
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub docker_name: Option<String>,
536 #[serde(skip_serializing_if = "Option::is_none")]
537 pub docker_user: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub docker_context: Option<String>,
540 #[serde(skip_serializing_if = "Option::is_none")]
541 pub docker_runtime: Option<String>,
542 #[serde(skip_serializing_if = "Option::is_none")]
543 pub podman_name: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub podman_user: Option<String>,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub podman_runtime: Option<String>,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub nerdctl_name: Option<String>,
550 #[serde(skip_serializing_if = "Option::is_none")]
551 pub nerdctl_user: Option<String>,
552 #[serde(skip_serializing_if = "Option::is_none")]
553 pub nerdctl_runtime: Option<String>,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 pub compose_service: Option<String>,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub compose_user: Option<String>,
558 #[serde(default, skip_serializing_if = "Vec::is_empty")]
559 pub compose_files: Vec<String>,
560 #[serde(skip_serializing_if = "Option::is_none")]
561 pub compose_project: Option<String>,
562 #[serde(skip_serializing_if = "Option::is_none")]
563 pub compose_runtime: Option<String>,
564 #[serde(skip_serializing_if = "Option::is_none")]
565 pub kubectl_pod: Option<String>,
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub kubectl_container: Option<String>,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub kubectl_namespace: Option<String>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub kubectl_context: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub kubectl_runtime: Option<String>,
574}
575
576impl ContainerConfig {
577 pub fn has_transport_fields(&self) -> bool {
578 self.family_flags().iter().any(|(_, flag)| flag.is_some())
579 }
580
581 pub fn selected_driver(&self) -> Result<Option<ContainerDriver>, String> {
587 let mut selected: Option<(ContainerDriver, &'static str)> = None;
588 for (driver, flag) in self.family_flags() {
589 let Some(flag) = flag else { continue };
590 match selected {
591 None => selected = Some((driver, flag)),
592 Some((_, first)) => {
593 return Err(format!(
594 "{first} cannot be combined with {flag}; each container driver has its own flag family"
595 ));
596 }
597 }
598 }
599 Ok(selected.map(|(driver, _)| driver))
600 }
601
602 pub fn target_name(&self) -> Option<&str> {
605 self.docker_name
606 .as_deref()
607 .or(self.podman_name.as_deref())
608 .or(self.nerdctl_name.as_deref())
609 .or(self.compose_service.as_deref())
610 .or(self.kubectl_pod.as_deref())
611 }
612
613 pub fn runtime_override(&self) -> Option<&str> {
615 self.docker_runtime
616 .as_deref()
617 .or(self.podman_runtime.as_deref())
618 .or(self.nerdctl_runtime.as_deref())
619 .or(self.compose_runtime.as_deref())
620 .or(self.kubectl_runtime.as_deref())
621 }
622
623 fn family_flags(&self) -> [(ContainerDriver, Option<&'static str>); 5] {
625 [
626 (
627 ContainerDriver::Docker,
628 first_present(&[
629 ("--container-docker-name", self.docker_name.is_some()),
630 ("--container-docker-user", self.docker_user.is_some()),
631 ("--container-docker-context", self.docker_context.is_some()),
632 ("--container-docker-runtime", self.docker_runtime.is_some()),
633 ]),
634 ),
635 (
636 ContainerDriver::Podman,
637 first_present(&[
638 ("--container-podman-name", self.podman_name.is_some()),
639 ("--container-podman-user", self.podman_user.is_some()),
640 ("--container-podman-runtime", self.podman_runtime.is_some()),
641 ]),
642 ),
643 (
644 ContainerDriver::Nerdctl,
645 first_present(&[
646 ("--container-nerdctl-name", self.nerdctl_name.is_some()),
647 ("--container-nerdctl-user", self.nerdctl_user.is_some()),
648 (
649 "--container-nerdctl-runtime",
650 self.nerdctl_runtime.is_some(),
651 ),
652 ]),
653 ),
654 (
655 ContainerDriver::Compose,
656 first_present(&[
657 (
658 "--container-compose-service",
659 self.compose_service.is_some(),
660 ),
661 ("--container-compose-user", self.compose_user.is_some()),
662 ("--container-compose-file", !self.compose_files.is_empty()),
663 (
664 "--container-compose-project",
665 self.compose_project.is_some(),
666 ),
667 (
668 "--container-compose-runtime",
669 self.compose_runtime.is_some(),
670 ),
671 ]),
672 ),
673 (
674 ContainerDriver::Kubectl,
675 first_present(&[
676 ("--container-kubectl-pod", self.kubectl_pod.is_some()),
677 (
678 "--container-kubectl-container",
679 self.kubectl_container.is_some(),
680 ),
681 (
682 "--container-kubectl-namespace",
683 self.kubectl_namespace.is_some(),
684 ),
685 (
686 "--container-kubectl-context",
687 self.kubectl_context.is_some(),
688 ),
689 (
690 "--container-kubectl-runtime",
691 self.kubectl_runtime.is_some(),
692 ),
693 ]),
694 ),
695 ]
696 }
697}
698
699fn first_present(fields: &[(&'static str, bool)]) -> Option<&'static str> {
700 fields
701 .iter()
702 .find(|(_, present)| *present)
703 .map(|(flag, _)| *flag)
704}
705
706#[derive(Debug, Deserialize, Default)]
707#[serde(deny_unknown_fields)]
708struct SessionConfigFlat {
709 #[serde(default)]
710 dsn_secret: Option<String>,
711 #[serde(default)]
712 conninfo_secret: Option<String>,
713 #[serde(default)]
714 host: Option<String>,
715 #[serde(default)]
716 port: Option<u16>,
717 #[serde(default)]
718 user: Option<String>,
719 #[serde(default)]
720 dbname: Option<String>,
721 #[serde(default)]
722 password_secret: Option<String>,
723 #[serde(default)]
724 ssh: Option<String>,
725 #[serde(default)]
726 ssh_via: Vec<String>,
727 #[serde(default)]
728 ssh_options: Vec<String>,
729 #[serde(default)]
730 ssh_local_host: Option<String>,
731 #[serde(default)]
732 ssh_local_port: Option<u16>,
733 #[serde(default)]
734 ssh_remote_socket: Option<String>,
735 #[serde(default)]
736 ssh_sudo_user: Option<String>,
737 #[serde(default)]
738 docker_name: Option<String>,
739 #[serde(default)]
740 docker_user: Option<String>,
741 #[serde(default)]
742 docker_context: Option<String>,
743 #[serde(default)]
744 docker_runtime: Option<String>,
745 #[serde(default)]
746 podman_name: Option<String>,
747 #[serde(default)]
748 podman_user: Option<String>,
749 #[serde(default)]
750 podman_runtime: Option<String>,
751 #[serde(default)]
752 nerdctl_name: Option<String>,
753 #[serde(default)]
754 nerdctl_user: Option<String>,
755 #[serde(default)]
756 nerdctl_runtime: Option<String>,
757 #[serde(default)]
758 compose_service: Option<String>,
759 #[serde(default)]
760 compose_user: Option<String>,
761 #[serde(default)]
762 compose_files: Vec<String>,
763 #[serde(default)]
764 compose_project: Option<String>,
765 #[serde(default)]
766 compose_runtime: Option<String>,
767 #[serde(default)]
768 kubectl_pod: Option<String>,
769 #[serde(default)]
770 kubectl_container: Option<String>,
771 #[serde(default)]
772 kubectl_namespace: Option<String>,
773 #[serde(default)]
774 kubectl_context: Option<String>,
775 #[serde(default)]
776 kubectl_runtime: Option<String>,
777}
778
779impl From<SessionConfigFlat> for SessionConfig {
780 fn from(flat: SessionConfigFlat) -> Self {
781 Self {
782 profile_pinned: false,
783 dsn_secret: flat.dsn_secret,
784 conninfo_secret: flat.conninfo_secret,
785 host: flat.host,
786 port: flat.port,
787 user: flat.user,
788 dbname: flat.dbname,
789 password_secret: flat.password_secret,
790 ssh: SshConfig {
791 destination: flat.ssh,
792 via: flat.ssh_via,
793 options: flat.ssh_options,
794 local_host: flat.ssh_local_host,
795 local_port: flat.ssh_local_port,
796 remote_socket: flat.ssh_remote_socket,
797 sudo_user: flat.ssh_sudo_user,
798 },
799 container: ContainerConfig {
800 docker_name: flat.docker_name,
801 docker_user: flat.docker_user,
802 docker_context: flat.docker_context,
803 docker_runtime: flat.docker_runtime,
804 podman_name: flat.podman_name,
805 podman_user: flat.podman_user,
806 podman_runtime: flat.podman_runtime,
807 nerdctl_name: flat.nerdctl_name,
808 nerdctl_user: flat.nerdctl_user,
809 nerdctl_runtime: flat.nerdctl_runtime,
810 compose_service: flat.compose_service,
811 compose_user: flat.compose_user,
812 compose_files: flat.compose_files,
813 compose_project: flat.compose_project,
814 compose_runtime: flat.compose_runtime,
815 kubectl_pod: flat.kubectl_pod,
816 kubectl_container: flat.kubectl_container,
817 kubectl_namespace: flat.kubectl_namespace,
818 kubectl_context: flat.kubectl_context,
819 kubectl_runtime: flat.kubectl_runtime,
820 },
821 }
822 }
823}
824
825impl<'de> Deserialize<'de> for SessionConfig {
826 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
827 where
828 D: serde::Deserializer<'de>,
829 {
830 SessionConfigFlat::deserialize(deserializer).map(Self::from)
831 }
832}
833
834impl SessionConfig {
835 pub fn uses_ssh_transport(&self) -> bool {
836 self.ssh.has_transport_fields()
837 }
838
839 pub fn uses_container_transport(&self) -> bool {
840 self.container.has_transport_fields()
841 }
842
843 pub fn transport_kind(&self) -> Result<TransportKind, String> {
844 self.container.selected_driver()?;
848 let uses_ssh = self.uses_ssh_transport();
849 let uses_container = self.uses_container_transport();
850 match (uses_ssh, uses_container) {
851 (false, false) => Ok(TransportKind::Direct),
852 (true, false) => Ok(TransportKind::Ssh),
853 (false, true) => Ok(TransportKind::Container),
854 (true, true) => Ok(TransportKind::Container),
858 }
859 }
860}
861
862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub enum TransportKind {
864 Direct,
865 Ssh,
866 Container,
867}
868
869impl TransportKind {
870 pub fn as_str(self) -> &'static str {
871 match self {
872 Self::Direct => "direct",
873 Self::Ssh => "ssh",
874 Self::Container => "container",
875 }
876 }
877}
878
879#[derive(Debug, Serialize, Deserialize, Clone)]
880pub struct RuntimeConfig {
881 pub default_session: String,
882 #[serde(default)]
883 pub sessions: HashMap<String, SessionConfig>,
884 pub inline_max_rows: usize,
885 pub inline_max_bytes: usize,
886 pub statement_timeout_ms: u64,
887 pub lock_timeout_ms: u64,
888 #[serde(default, with = "log_filters_serde")]
889 pub log: LogFilters,
890}
891
892impl Default for RuntimeConfig {
893 fn default() -> Self {
894 let mut sessions = HashMap::new();
895 sessions.insert("default".to_string(), SessionConfig::default());
896 Self {
897 default_session: "default".to_string(),
898 sessions,
899 inline_max_rows: 1000,
900 inline_max_bytes: 1_048_576,
901 statement_timeout_ms: 30_000,
902 lock_timeout_ms: 5_000,
903 log: LogFilters::default(),
904 }
905 }
906}
907
908#[derive(Debug, Deserialize, Default)]
909#[serde(deny_unknown_fields)]
910pub struct ConfigPatch {
911 pub default_session: Option<String>,
912 pub sessions: Option<HashMap<String, SessionConfigPatch>>,
913 pub inline_max_rows: Option<usize>,
914 pub inline_max_bytes: Option<usize>,
915 pub statement_timeout_ms: Option<u64>,
916 pub lock_timeout_ms: Option<u64>,
917 pub log: Option<Vec<String>>,
918}
919
920#[derive(Debug, Default)]
921pub enum PatchField<T> {
922 #[default]
923 Missing,
924 Null,
925 Value(T),
926}
927
928impl<'de, T> Deserialize<'de> for PatchField<T>
929where
930 T: Deserialize<'de>,
931{
932 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
933 where
934 D: serde::Deserializer<'de>,
935 {
936 let value = Option::<T>::deserialize(deserializer)?;
937 match value {
938 Some(value) => Ok(Self::Value(value)),
939 None => Ok(Self::Null),
940 }
941 }
942}
943
944impl<T> PatchField<T> {
945 pub fn into_update(self) -> Option<Option<T>> {
946 match self {
947 Self::Missing => None,
948 Self::Null => Some(None),
949 Self::Value(value) => Some(Some(value)),
950 }
951 }
952}
953
954#[derive(Debug, Default)]
955pub struct SessionConfigPatch {
956 pub dsn_secret: PatchField<String>,
957 pub conninfo_secret: PatchField<String>,
958 pub host: PatchField<String>,
959 pub port: PatchField<u16>,
960 pub user: PatchField<String>,
961 pub dbname: PatchField<String>,
962 pub password_secret: PatchField<String>,
963 pub ssh: SshConfigPatch,
964 pub container: ContainerConfigPatch,
965}
966
967#[derive(Debug, Default)]
968pub struct SshConfigPatch {
969 pub destination: PatchField<String>,
970 pub via: PatchField<Vec<String>>,
971 pub options: PatchField<Vec<String>>,
972 pub local_host: PatchField<String>,
973 pub local_port: PatchField<u16>,
974 pub remote_socket: PatchField<String>,
975 pub sudo_user: PatchField<String>,
976}
977
978#[derive(Debug, Default)]
979pub struct ContainerConfigPatch {
980 pub docker_name: PatchField<String>,
981 pub docker_user: PatchField<String>,
982 pub docker_context: PatchField<String>,
983 pub docker_runtime: PatchField<String>,
984 pub podman_name: PatchField<String>,
985 pub podman_user: PatchField<String>,
986 pub podman_runtime: PatchField<String>,
987 pub nerdctl_name: PatchField<String>,
988 pub nerdctl_user: PatchField<String>,
989 pub nerdctl_runtime: PatchField<String>,
990 pub compose_service: PatchField<String>,
991 pub compose_user: PatchField<String>,
992 pub compose_files: PatchField<Vec<String>>,
993 pub compose_project: PatchField<String>,
994 pub compose_runtime: PatchField<String>,
995 pub kubectl_pod: PatchField<String>,
996 pub kubectl_container: PatchField<String>,
997 pub kubectl_namespace: PatchField<String>,
998 pub kubectl_context: PatchField<String>,
999 pub kubectl_runtime: PatchField<String>,
1000}
1001
1002#[derive(Debug, Deserialize, Default)]
1003#[serde(deny_unknown_fields)]
1004struct SessionConfigPatchFlat {
1005 #[serde(default)]
1006 dsn_secret: PatchField<String>,
1007 #[serde(default)]
1008 conninfo_secret: PatchField<String>,
1009 #[serde(default)]
1010 host: PatchField<String>,
1011 #[serde(default)]
1012 port: PatchField<u16>,
1013 #[serde(default)]
1014 user: PatchField<String>,
1015 #[serde(default)]
1016 dbname: PatchField<String>,
1017 #[serde(default)]
1018 password_secret: PatchField<String>,
1019 #[serde(default)]
1020 ssh: PatchField<String>,
1021 #[serde(default)]
1022 ssh_via: PatchField<Vec<String>>,
1023 #[serde(default)]
1024 ssh_options: PatchField<Vec<String>>,
1025 #[serde(default)]
1026 ssh_local_host: PatchField<String>,
1027 #[serde(default)]
1028 ssh_local_port: PatchField<u16>,
1029 #[serde(default)]
1030 ssh_remote_socket: PatchField<String>,
1031 #[serde(default)]
1032 ssh_sudo_user: PatchField<String>,
1033 #[serde(default)]
1034 docker_name: PatchField<String>,
1035 #[serde(default)]
1036 docker_user: PatchField<String>,
1037 #[serde(default)]
1038 docker_context: PatchField<String>,
1039 #[serde(default)]
1040 docker_runtime: PatchField<String>,
1041 #[serde(default)]
1042 podman_name: PatchField<String>,
1043 #[serde(default)]
1044 podman_user: PatchField<String>,
1045 #[serde(default)]
1046 podman_runtime: PatchField<String>,
1047 #[serde(default)]
1048 nerdctl_name: PatchField<String>,
1049 #[serde(default)]
1050 nerdctl_user: PatchField<String>,
1051 #[serde(default)]
1052 nerdctl_runtime: PatchField<String>,
1053 #[serde(default)]
1054 compose_service: PatchField<String>,
1055 #[serde(default)]
1056 compose_user: PatchField<String>,
1057 #[serde(default)]
1058 compose_files: PatchField<Vec<String>>,
1059 #[serde(default)]
1060 compose_project: PatchField<String>,
1061 #[serde(default)]
1062 compose_runtime: PatchField<String>,
1063 #[serde(default)]
1064 kubectl_pod: PatchField<String>,
1065 #[serde(default)]
1066 kubectl_container: PatchField<String>,
1067 #[serde(default)]
1068 kubectl_namespace: PatchField<String>,
1069 #[serde(default)]
1070 kubectl_context: PatchField<String>,
1071 #[serde(default)]
1072 kubectl_runtime: PatchField<String>,
1073}
1074
1075impl From<SessionConfigPatchFlat> for SessionConfigPatch {
1076 fn from(flat: SessionConfigPatchFlat) -> Self {
1077 Self {
1078 dsn_secret: flat.dsn_secret,
1079 conninfo_secret: flat.conninfo_secret,
1080 host: flat.host,
1081 port: flat.port,
1082 user: flat.user,
1083 dbname: flat.dbname,
1084 password_secret: flat.password_secret,
1085 ssh: SshConfigPatch {
1086 destination: flat.ssh,
1087 via: flat.ssh_via,
1088 options: flat.ssh_options,
1089 local_host: flat.ssh_local_host,
1090 local_port: flat.ssh_local_port,
1091 remote_socket: flat.ssh_remote_socket,
1092 sudo_user: flat.ssh_sudo_user,
1093 },
1094 container: ContainerConfigPatch {
1095 docker_name: flat.docker_name,
1096 docker_user: flat.docker_user,
1097 docker_context: flat.docker_context,
1098 docker_runtime: flat.docker_runtime,
1099 podman_name: flat.podman_name,
1100 podman_user: flat.podman_user,
1101 podman_runtime: flat.podman_runtime,
1102 nerdctl_name: flat.nerdctl_name,
1103 nerdctl_user: flat.nerdctl_user,
1104 nerdctl_runtime: flat.nerdctl_runtime,
1105 compose_service: flat.compose_service,
1106 compose_user: flat.compose_user,
1107 compose_files: flat.compose_files,
1108 compose_project: flat.compose_project,
1109 compose_runtime: flat.compose_runtime,
1110 kubectl_pod: flat.kubectl_pod,
1111 kubectl_container: flat.kubectl_container,
1112 kubectl_namespace: flat.kubectl_namespace,
1113 kubectl_context: flat.kubectl_context,
1114 kubectl_runtime: flat.kubectl_runtime,
1115 },
1116 }
1117 }
1118}
1119
1120impl<'de> Deserialize<'de> for SessionConfigPatch {
1121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1122 where
1123 D: serde::Deserializer<'de>,
1124 {
1125 SessionConfigPatchFlat::deserialize(deserializer).map(Self::from)
1126 }
1127}
1128
1129#[derive(Debug, Clone)]
1130pub struct ResolvedOptions {
1131 pub stream_rows: bool,
1132 pub batch_rows: usize,
1133 pub batch_bytes: usize,
1134 pub statement_timeout_ms: u64,
1135 pub lock_timeout_ms: u64,
1136 pub read_only: bool,
1137 pub inline_max_rows: usize,
1138 pub inline_max_bytes: usize,
1139}
1140
1141#[cfg(test)]
1142#[path = "../tests/support/unit_types.rs"]
1143mod tests;