Skip to main content

tessera/readers/
duckdb.rs

1//! Load Tessera partitions into DuckDB relations.
2//!
3//! Uses DuckDB's `httpfs` to range-read the presigned Parquet directly; the
4//! returned connection carries a `tessera` view that can be filtered and
5//! aggregated in SQL with predicate pushdown.
6
7use duckdb::Connection;
8
9use super::ResolvedPartition;
10use crate::error::TesseraError;
11
12/// Quote a value as a SQL string literal.
13fn sql_str(value: &str) -> String {
14    format!("'{}'", value.replace('\'', "''"))
15}
16
17/// Build an in-memory DuckDB connection with a `tessera` view over the
18/// resolved partitions.
19///
20/// For multi-partition reads each leaf is unioned with a `coin` and `month`
21/// column identifying the source partition. Query it with
22/// `SELECT ... FROM tessera`.
23///
24/// # Errors
25///
26/// Returns [`TesseraError::Network`] when the connection or view creation fails.
27pub fn build_relation(
28    parts: &[ResolvedPartition],
29    columns: Option<&[&str]>,
30) -> Result<Connection, TesseraError> {
31    let connection =
32        Connection::open_in_memory().map_err(|err| TesseraError::Network(err.to_string()))?;
33
34    // Recent DuckDB autoloads httpfs for https paths; load explicitly but
35    // don't fail if the environment can't reach the extension repository.
36    let _ = connection.execute_batch("INSTALL httpfs; LOAD httpfs;");
37
38    let multi = parts.len() > 1;
39    let select_cols = columns.map_or_else(|| "*".to_string(), |columns| columns.join(", "));
40    let selects: Vec<String> = parts
41        .iter()
42        .map(|(partition, url)| {
43            let projection = if multi {
44                format!(
45                    "{}, {} AS coin, {} AS month",
46                    select_cols,
47                    sql_str(&partition.coin),
48                    sql_str(&partition.month)
49                )
50            } else {
51                select_cols.clone()
52            };
53            format!("SELECT {projection} FROM read_parquet({})", sql_str(url))
54        })
55        .collect();
56    let query = selects.join("\nUNION ALL\n");
57    connection
58        .execute_batch(&format!("CREATE OR REPLACE TEMP VIEW tessera AS {query}"))
59        .map_err(|err| TesseraError::Network(err.to_string()))?;
60    Ok(connection)
61}