Skip to main content

faucet_transform_sql/
config.rs

1//! Config types for the SQL transform. No I/O or DuckDB here.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::BTreeMap;
7
8/// Configuration for the `sql` transform.
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct SqlTransformConfig {
11    /// The SQL statement. The page's records are the relation `batch`. Must
12    /// produce a result set; each result row becomes one output record.
13    pub query: String,
14    /// Reference relations loaded once at compile time and joinable by name.
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub relations: Vec<RelationSpec>,
17    /// Optional DuckDB `memory_limit` pragma (e.g. "1GB"). Default: DuckDB's own.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub memory_limit: Option<String>,
20    /// Optional DuckDB `threads` pragma. Default: DuckDB's own. Set to 1–2 for
21    /// high-fan-out matrices to avoid CPU over-subscription across rows.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub threads: Option<usize>,
24}
25
26/// A reference relation registered before the first page.
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
28pub struct RelationSpec {
29    /// Relation name as referenced in the query. Must be a safe SQL identifier
30    /// and must not be `batch` (reserved for the page).
31    pub name: String,
32    /// Where the relation's data comes from.
33    pub source: RelationSource,
34    /// Re-stat the file's mtime before each page; rebuild + atomic swap if it
35    /// changed. Default false. Ignored for `values` and `http` (both loaded
36    /// once for the whole run).
37    #[serde(default)]
38    pub reload_on_change: bool,
39}
40
41/// HTTP method used to fetch an `http` reference relation. Only the two verbs a
42/// small read-only list endpoint needs are supported; `GET` is the default.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "UPPERCASE")]
45pub enum HttpMethod {
46    /// `GET` — the default.
47    #[default]
48    Get,
49    /// `POST` — for POST-search list endpoints (no request body is sent).
50    Post,
51}
52
53// serde `default = "..."` needs a function, not a literal.
54fn default_true() -> bool {
55    true
56}
57
58/// The data source for a reference relation.
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60#[serde(tag = "type", rename_all = "snake_case")]
61pub enum RelationSource {
62    /// Delimited file loaded via DuckDB `read_csv_auto`.
63    Csv {
64        /// Filesystem path to the CSV file (absolute, or relative to the working directory).
65        path: String,
66        /// Whether the first row is a header row. Default: `true`.
67        #[serde(default = "default_true")]
68        has_header: bool,
69    },
70    /// Newline-delimited JSON loaded via DuckDB `read_json_auto`.
71    Jsonl {
72        /// Filesystem path to the JSONL file (absolute, or relative to the working directory).
73        path: String,
74    },
75    /// Inline rows materialized into a table.
76    Values {
77        /// Column names, in declaration order.
78        columns: Vec<String>,
79        /// Rows of cell values; each inner row must have the same length as `columns`.
80        rows: Vec<Vec<Value>>,
81    },
82    /// Rows fetched from a small REST endpoint **once** at compile/first-use and
83    /// cached for the whole run (never re-fetched per page). The response is
84    /// materialized into a DuckDB table joinable by the relation's `name`.
85    Http {
86        /// Endpoint URL to fetch the rows from.
87        url: String,
88        /// HTTP method. Default: `GET`.
89        #[serde(default)]
90        method: HttpMethod,
91        /// Static request headers sent with the fetch (e.g. a bearer token
92        /// injected via `${...}` at the CLI layer). Optional.
93        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
94        headers: BTreeMap<String, String>,
95        /// JSONPath selecting the row array in the response body (e.g.
96        /// `$.items[*]`). If omitted, the whole body is used and must be a JSON
97        /// array. Every selected element must be a JSON object (one table row).
98        #[serde(default, skip_serializing_if = "Option::is_none")]
99        records_path: Option<String>,
100    },
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn config_round_trips_and_schema_builds() {
109        let cfg: SqlTransformConfig = serde_json::from_value(serde_json::json!({
110            "query": "SELECT * FROM batch",
111            "relations": [
112                {"name": "countries",
113                 "source": {"type": "csv", "path": "c.csv", "has_header": true}}
114            ]
115        }))
116        .unwrap();
117        assert_eq!(cfg.relations.len(), 1);
118        assert!(matches!(
119            cfg.relations[0].source,
120            RelationSource::Csv { .. }
121        ));
122        // schema_for! must succeed (used by `faucet schema transform sql`).
123        let schema = schemars::schema_for!(SqlTransformConfig);
124        let json = serde_json::to_value(&schema).unwrap();
125        assert!(
126            json.get("properties")
127                .and_then(|p| p.get("query"))
128                .is_some()
129        );
130    }
131}