Skip to main content

dre_protocol/
msg.rs

1//! Control messages. Every JSON frame is an object with a `type` field.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6pub use crate::options::OptionField;
7use crate::{Kind, PluginId};
8
9/// Core → plugin.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum Request {
13    /// Always first. The plugin picks a version in both ranges or replies `version_mismatch`.
14    Hello {
15        min_version: u32,
16        max_version: u32,
17        core_version: String,
18        /// The plugin core wants served, for an executable that provides several. A package
19        /// that doesn't provide it replies `error`; without it, the package serves its first.
20        #[serde(default, skip_serializing_if = "Option::is_none")]
21        plugin: Option<PluginId>,
22    },
23    /// Describe the connection fields a profile output for this plugin takes (used by `dre init`)
24    /// and the options a report's config block for it takes.
25    Describe {},
26    /// Check one config block of options (a format's `output:` keys, a destination entry's keys)
27    /// without doing anything. Replies `validated`.
28    Validate {
29        options: Map<String, Value>,
30    },
31    /// Source: open a session. All later `execute`/`check` requests run on it.
32    Open {
33        connection: Map<String, Value>,
34        read_only: bool,
35    },
36    /// Source: run one statement. Replies `result` (then Arrow frames, then `result_end`) or
37    /// `no_result`.
38    Execute {
39        sql: String,
40        #[serde(default, skip_serializing_if = "Option::is_none")]
41        row_limit: Option<u64>,
42    },
43    /// Source: verify one statement without executing it. Replies `ok` or `error`.
44    Check {
45        sql: String,
46    },
47    /// Format: write result sets to `path`. Followed, per result set, by Arrow frames (at least one,
48    /// carrying the schema) and a `result_set_end`, then `finish`. Replies `written`.
49    Write {
50        path: String,
51        format: String,
52        options: Map<String, Value>,
53        result_sets: Vec<ResultSetMeta>,
54        #[serde(default, skip_serializing_if = "Option::is_none")]
55        template: Option<Value>,
56    },
57    ResultSetEnd {},
58    Finish {},
59    /// Destination: deliver a local file, or with `files` (only to a plugin advertising
60    /// `multi_file`) every file of one output at once. Exactly one of `local_path`/`files` is set.
61    /// `options` are the destination entry's plugin options, rendered by core. Replies `delivered`.
62    Deliver {
63        #[serde(default, skip_serializing_if = "Option::is_none")]
64        local_path: Option<String>,
65        #[serde(default, skip_serializing_if = "Option::is_none")]
66        remote_path: Option<String>,
67        #[serde(default, skip_serializing_if = "Vec::is_empty")]
68        files: Vec<DeliveryFile>,
69        connection: Map<String, Value>,
70        #[serde(default)]
71        options: Map<String, Value>,
72    },
73    /// Source: load rows into a temporary table on the session, named after `name`. Followed by
74    /// Arrow frames (at least one, carrying the schema) and a `result_set_end`. Replies `loaded`.
75    /// Only sent to plugins advertising `load`.
76    Load {
77        name: String,
78    },
79    /// End the conversation; the plugin exits 0.
80    Close {},
81}
82
83/// Plugin → core.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(tag = "type", rename_all = "snake_case")]
86pub enum Response {
87    Hello {
88        protocol_version: u32,
89        kind: Kind,
90        name: String,
91        version: String,
92        #[serde(default)]
93        capabilities: Vec<String>,
94        /// Every plugin the executable serves; empty for one that serves only `kind`/`name`.
95        #[serde(default, skip_serializing_if = "Vec::is_empty")]
96        provides: Vec<PluginId>,
97    },
98    VersionMismatch {
99        min_version: u32,
100        max_version: u32,
101    },
102    Describe {
103        connection_fields: Vec<ConnectionField>,
104        #[serde(default, skip_serializing_if = "Vec::is_empty")]
105        option_fields: Vec<OptionField>,
106        /// Source: the character the database quotes identifiers with (`"` for DuckDB and
107        /// Postgres, a backtick for Databricks). Core uses it to quote names a project asks to
108        /// quote (a source's `quoting:`). Every source sets it; other kinds leave it out.
109        #[serde(default, skip_serializing_if = "Option::is_none")]
110        identifier_quote: Option<String>,
111    },
112    /// Every problem with the options, each a sentence naming the key; empty when they're fine.
113    Validated {
114        errors: Vec<String>,
115    },
116    Ok {},
117    /// A result set follows as Arrow frames, ended by `result_end`.
118    Result {
119        columns: Vec<String>,
120    },
121    ResultEnd {
122        rows: u64,
123    },
124    NoResult {
125        #[serde(default, skip_serializing_if = "Option::is_none")]
126        rows_affected: Option<u64>,
127    },
128    Written {
129        files: Vec<String>,
130        /// Things the person should know about the files (a value written as text, say).
131        #[serde(default, skip_serializing_if = "Vec::is_empty")]
132        warnings: Vec<String>,
133    },
134    Delivered {
135        location: String,
136    },
137    /// `relation` is what SQL uses to read the loaded rows. `warning`, when set, is shown to the
138    /// user (e.g. the database has no bulk path, so a load this size is slow).
139    Loaded {
140        relation: String,
141        rows: u64,
142        #[serde(default, skip_serializing_if = "Option::is_none")]
143        warning: Option<String>,
144    },
145    Error {
146        message: String,
147    },
148}
149
150/// One file of a multi-file `deliver`.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct DeliveryFile {
153    pub local_path: String,
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub remote_path: Option<String>,
156}
157
158/// Per result set, what a format plugin needs to lay it out.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct ResultSetMeta {
161    /// Sheet name (xlsx) or file suffix (single-table formats).
162    pub name: String,
163    /// The query (`.sql` basename) that produced it.
164    pub query: String,
165    /// 1-based index among that query's result sets.
166    pub result_index: usize,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub anchor: Option<String>,
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub header: Option<bool>,
171    /// The query entry's `columns:` map: per result column, how to show it.
172    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
173    pub columns: std::collections::BTreeMap<String, ColumnOptions>,
174}
175
176/// One entry of a `columns:` map.
177#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
178pub struct ColumnOptions {
179    /// An Excel number format code.
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub format: Option<String>,
182    /// A row formula (`={qty}*{price}`), written in place of the column's value, which becomes
183    /// its cached result.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub formula: Option<String>,
186    /// A totals row entry under the data: `sum`, `average`, `count`, `min`, `max`, or a formula
187    /// over whole columns (`=SUM({amount:*})/COUNT({qty:*})`).
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub total: Option<String>,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct ConnectionField {
194    pub name: String,
195    #[serde(default)]
196    pub description: String,
197    #[serde(default)]
198    pub required: bool,
199    /// Offered as an `env_var()` reference by default in `dre init`.
200    #[serde(default)]
201    pub secret: bool,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub default: Option<Value>,
204    /// A source type whose profile has the same field: `dre init` offers the value entered for
205    /// that source as this field's default (e.g. one Databricks host for source and destination).
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub same_as_source: Option<String>,
208}
209
210impl ConnectionField {
211    pub fn new(name: &str, description: &str) -> Self {
212        ConnectionField {
213            name: name.into(),
214            description: description.into(),
215            required: false,
216            secret: false,
217            default: None,
218            same_as_source: None,
219        }
220    }
221    pub fn required(mut self) -> Self {
222        self.required = true;
223        self
224    }
225    pub fn secret(mut self) -> Self {
226        self.secret = true;
227        self
228    }
229    pub fn default(mut self, v: impl Into<Value>) -> Self {
230        self.default = Some(v.into());
231        self
232    }
233    pub fn same_as_source(mut self, source_type: &str) -> Self {
234        self.same_as_source = Some(source_type.into());
235        self
236    }
237}