Skip to main content

dre_core/
render.rs

1//! Runtime Jinja rendering: one environment per connection a Binding uses (its queries' SQL) plus
2//! one for output paths and template values. Provides `run.*`, `var()`, `env_var()`,
3//! `run_query()`, `columns()`, `ref()`, `source()`, `target.name`, `connection.*`,
4//! `destination.*`, `profile()`, the calendar functions in [`crate::dates`] and every macro in
5//! `macros/`.
6//!
7//! [`Mode::Parse`] renders without a database, as dbt's parse does, to find every `source()` a
8//! query calls: `run_query()` returns no rows, `columns()` none, `connection.*` nothing, and
9//! `raise_error()` doesn't fire. [`Limited`] renders the values that choose a connection or a
10//! source (every `profile:`, a source's `database`/`schema`/`identifier`), where only `var()`,
11//! `env_var()`, `run.*` and `target.name` exist.
12
13use std::collections::BTreeMap;
14use std::fmt;
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex};
17
18use chrono::{NaiveDate, Utc};
19
20use crate::dates::{Calendar, Date, DateTime};
21
22use crate::lookups::{self, Cell, Load, Lookup, Table};
23use crate::packages::{DispatchOrder, Package};
24use minijinja::value::{Enumerator, Kwargs, Object, ObjectRepr, Value};
25use minijinja::{Environment, Error, ErrorKind, UndefinedBehavior};
26use serde_json::{Map as JsonMap, Value as Json};
27
28const MACROS: &str = "__dre_macros__";
29
30/// How a [`Renderer`] treats the database.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum Mode {
33    /// Rendering SQL to run (or compile): `run_query()` and `columns()` reach the database.
34    #[default]
35    Run,
36    /// The parse pass: no database, to find `source()` calls.
37    Parse,
38}
39
40/// Ambient `run.*` context.
41#[derive(Debug, Clone)]
42pub struct RunContext {
43    pub report: String,
44    pub set: Option<String>,
45    /// The run's target (environment): `run.target` and `target.name`.
46    pub target: String,
47    /// The schedule's name under `dre run --schedule`, else `None`.
48    pub schedule: Option<String>,
49    pub date: NaiveDate,
50    /// When the run started, or the instant it was scheduled for: `run.now`.
51    pub now: chrono::DateTime<Utc>,
52    /// `DRE_RUN_AT`: `run.scheduled_at`.
53    pub scheduled_at: Option<chrono::DateTime<Utc>>,
54    /// The run's timezone and week settings.
55    pub calendar: Calendar,
56}
57
58/// Rows returned to templates by `run_query()`.
59#[derive(Debug, Clone, Default)]
60pub struct QueryRows {
61    pub columns: Vec<String>,
62    pub rows: Vec<Vec<Value>>,
63}
64
65/// One column of a relation, as `columns()` returns it.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Column {
68    pub name: String,
69    /// The Arrow type name (`Int64`, `Utf8`, `Date32`, ...).
70    pub data_type: String,
71}
72
73/// Executes `run_query()` SQL on the Binding's sessions: on `profile`'s connection, else on the
74/// renderer's own.
75pub trait QueryRunner: Send + Sync {
76    /// Run `sql`, failing if it returns more than `max_rows` rows.
77    fn run_query(&self, sql: &str, max_rows: u64, profile: Option<&str>) -> Result<QueryRows, String>;
78    /// The columns `sql` returns, without fetching rows.
79    fn columns(&self, sql: &str, profile: Option<&str>) -> Result<Vec<Column>, String>;
80    /// Load a lookup into a temp table on the renderer's own session: `Some((relation, plugin
81    /// warning))`, or `None` when the source can't load rows.
82    fn load(&self, _name: &str, _table: &Table) -> Result<Option<(String, Option<String>)>, String> {
83        Ok(None)
84    }
85}
86
87/// One profile target as templates see it: `connection.*`, `destination.*` and
88/// `profile('name').*`.
89#[derive(Debug, Clone, Default)]
90pub struct Connection {
91    /// The profile's name: `.name` and `.profile`.
92    pub profile: String,
93    /// The target's name (`dev`, `prod`).
94    pub target: String,
95    /// The plugin type.
96    pub kind: String,
97    /// Every field, with `env_var()` already rendered.
98    pub fields: JsonMap<String, Json>,
99    /// Fields that hold secrets: reading one is an error.
100    pub secrets: Vec<String>,
101}
102
103/// Where `connection`, `destination` and `profile()` get their values. `role` is `connection`
104/// or `destination`; `None` finds the profile in whichever section has it.
105pub trait Connections: Send + Sync {
106    fn profile(&self, name: &str, role: Option<&str>) -> Result<Connection, String>;
107    /// The identifier quote character a source plugin type reports (`None`: it doesn't say).
108    fn identifier_quote(&self, kind: &str) -> Result<Option<String>, String>;
109}
110
111/// Which parts of a source's name to quote.
112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
113pub struct Quoting {
114    pub database: bool,
115    pub schema: bool,
116    pub identifier: bool,
117}
118
119impl Quoting {
120    pub fn any(&self) -> bool {
121        self.database || self.schema || self.identifier
122    }
123}
124
125/// A declared source table with its fields rendered for one Binding: what `source()` returns.
126#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
127pub struct ResolvedSource {
128    pub source: String,
129    pub table: String,
130    /// The connection the source names, rendered; `None` runs it wherever its query runs.
131    pub profile: Option<String>,
132    pub database: Option<String>,
133    pub schema: String,
134    pub identifier: String,
135    pub quoting: Quoting,
136}
137
138impl ResolvedSource {
139    /// `database.schema.identifier`, or `schema.identifier` without a database, with the parts
140    /// `quoting` asks for quoted by `quote` (doubled inside a name).
141    pub fn relation(&self, quote: Option<&str>) -> String {
142        let part = |s: &str, q: bool| match (q, quote) {
143            (true, Some(c)) => format!("{c}{}{c}", s.replace(c, &format!("{c}{c}"))),
144            _ => s.to_string(),
145        };
146        let mut parts = Vec::new();
147        if let Some(d) = &self.database {
148            parts.push(part(d, self.quoting.database));
149        }
150        parts.push(part(&self.schema, self.quoting.schema));
151        parts.push(part(&self.identifier, self.quoting.identifier));
152        parts.join(".")
153    }
154
155    /// `source.table`, as selectors and the manifest name it.
156    pub fn key(&self) -> String {
157        format!("{}.{}", self.source, self.table)
158    }
159}
160
161/// Resolves `source('name', 'table')` for one Binding.
162pub type SourceResolver = Arc<dyn Fn(&str, &str) -> Result<ResolvedSource, String> + Send + Sync>;
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct RenderError {
166    pub file: PathBuf,
167    pub line: Option<usize>,
168    pub message: String,
169}
170
171impl fmt::Display for RenderError {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self.line {
174            Some(l) => write!(f, "{}:{l}: {}", self.file.display(), self.message),
175            None => write!(f, "{}: {}", self.file.display(), self.message),
176        }
177    }
178}
179
180/// What `columns()` found, by relation and connection.
181type ColumnsCache = BTreeMap<(String, Option<String>), Value>;
182
183pub struct Renderer {
184    env: Environment<'static>,
185    /// The parse pass's first `raise_error()` message in the current render.
186    raised: Arc<Mutex<Option<String>>>,
187    /// What `columns()` found, by relation, for the file being rendered.
188    columns_cache: Arc<Mutex<ColumnsCache>>,
189    /// `(source, table)` of every `source()` call since the last [`Renderer::take_sources`].
190    source_calls: Arc<Mutex<Vec<(String, String)>>>,
191    /// The destination being rendered: `destination.*`.
192    destination: Arc<Mutex<Option<Connection>>>,
193    /// Warnings raised while rendering (e.g. a large lookup inlined), drained by the caller.
194    warnings: Arc<Mutex<Vec<String>>>,
195    import: String,
196    /// Per combined macro template: `(first line, file, line count)` of each macro file in it.
197    macro_files: BTreeMap<String, Vec<(usize, PathBuf, usize)>>,
198}
199
200pub struct RendererConfig<'a> {
201    pub root: &'a Path,
202    pub macros: &'a [PathBuf],
203    pub context: RunContext,
204    /// The Binding's merged vars.
205    pub vars: JsonMap<String, Json>,
206    /// `--var` overrides, highest precedence.
207    pub cli_vars: BTreeMap<String, String>,
208    pub runner: Option<Arc<dyn QueryRunner>>,
209    /// Profiles for `connection`, `destination` and `profile()`; `None` offline.
210    pub connections: Option<Arc<dyn Connections>>,
211    pub mode: Mode,
212    /// The connection `connection.*` reads and `run_query()` uses by default: the query's in
213    /// query SQL, the Binding's inherited one elsewhere.
214    pub connection: Option<String>,
215    /// That connection's plugin type, for `dispatch()` and source quoting; empty when unknown.
216    pub source_type: String,
217    /// `source()`; `None`: the project declares no sources.
218    pub sources: Option<SourceResolver>,
219    pub run_query_max_rows: u64,
220    /// Every `.sql` file `ref()` can name, by basename, relative to `root`.
221    pub sql: BTreeMap<String, PathBuf>,
222    /// Lookups `ref()` and `lookup()` can name.
223    pub lookups: BTreeMap<String, Lookup>,
224    pub lookup_inline_max_rows: u64,
225    /// Macro packages, imported under their names.
226    pub packages: Vec<Package>,
227    /// The project's name: the root namespace in `dispatch()` search orders.
228    pub project_name: String,
229    pub dispatch: DispatchOrder,
230}
231
232impl Renderer {
233    pub fn new(cfg: RendererConfig<'_>) -> Result<Renderer, RenderError> {
234        let mut env = Environment::new();
235        let parse = cfg.mode == Mode::Parse;
236        // The parse pass has no data: `run_query(...)[0].x` must render as nothing, not fail.
237        env.set_undefined_behavior(if parse {
238            UndefinedBehavior::Chainable
239        } else {
240            UndefinedBehavior::Strict
241        });
242        env.set_keep_trailing_newline(true);
243        crate::mutable::register(&mut env);
244
245        add_vars(&mut env, cfg.vars, cfg.cli_vars);
246        // The parse pass has no data, so a `raise_error()` there may only mean `run_query()` came
247        // back empty: it doesn't stop the render, but if rendering fails anyway, its message is the
248        // one reported.
249        let raised: Arc<Mutex<Option<String>>> = Arc::default();
250        let raised_c = raised.clone();
251        env.add_function("raise_error", move |message: String| -> Result<Value, Error> {
252            if parse {
253                raised_c.lock().unwrap().get_or_insert(message);
254                return Ok(Value::from(""));
255            }
256            Err(Error::new(ErrorKind::InvalidOperation, message))
257        });
258        let source_type = cfg.source_type.clone();
259        crate::dates::register(&mut env, cfg.context.calendar, cfg.context.date);
260        env.add_global("target", Value::from_object(Target(cfg.context.target.clone())));
261        env.add_global("run", Value::from_object(Run(cfg.context)));
262        let destination: Arc<Mutex<Option<Connection>>> = Arc::default();
263        if parse {
264            env.add_global("connection", Value::from_object(Lenient));
265            env.add_global("destination", Value::from_object(Lenient));
266            env.add_function(
267                "profile",
268                |_name: String, kwargs: Kwargs| -> Result<Value, Error> {
269                    let role: Option<String> = kwargs.get("role")?;
270                    kwargs.assert_all_used()?;
271                    check_role(role.as_deref())?;
272                    Ok(Value::from_object(Lenient))
273                },
274            );
275        } else {
276            env.add_global(
277                "connection",
278                Value::from_object(LazyConnection {
279                    connections: cfg.connections.clone(),
280                    name: cfg.connection.clone(),
281                    resolved: std::sync::OnceLock::new(),
282                }),
283            );
284            env.add_global(
285                "destination",
286                Value::from_object(DestinationSlot(destination.clone())),
287            );
288            let c = cfg.connections.clone();
289            env.add_function(
290                "profile",
291                move |name: String, kwargs: Kwargs| -> Result<Value, Error> {
292                    let role: Option<String> = kwargs.get("role")?;
293                    kwargs.assert_all_used()?;
294                    check_role(role.as_deref())?;
295                    let Some(c) = &c else {
296                        return Err(Error::new(
297                            ErrorKind::InvalidOperation,
298                            format!("`profile('{name}')`: no profiles.yml is loaded here"),
299                        ));
300                    };
301                    c.profile(&name, role.as_deref())
302                        .map(|t| Value::from_object(ConnectionValue(t)))
303                        .map_err(|e| {
304                            Error::new(ErrorKind::InvalidOperation, format!("`profile('{name}')`: {e}"))
305                        })
306                },
307            );
308        }
309        // `source()`: every call is recorded; in a real render the relation is quoted with the
310        // connection's quote character where the source asks for it.
311        let source_calls: Arc<Mutex<Vec<(String, String)>>> = Arc::default();
312        let relations: Arc<Mutex<Vec<(String, ResolvedSource)>>> = Arc::default();
313        {
314            let calls = source_calls.clone();
315            let relations = relations.clone();
316            let resolver = cfg.sources.clone();
317            let connections = cfg.connections.clone();
318            let kind = cfg.source_type.clone();
319            let quote: Arc<std::sync::OnceLock<Result<Option<String>, String>>> = Arc::default();
320            env.add_function(
321                "source",
322                move |source: String, table: String| -> Result<Value, Error> {
323                    let fail = |e: String| {
324                        Error::new(
325                            ErrorKind::InvalidOperation,
326                            format!("`source('{source}', '{table}')`: {e}"),
327                        )
328                    };
329                    let Some(resolve) = &resolver else {
330                        return Err(fail("the project declares no `sources:`".into()));
331                    };
332                    let r = resolve(&source, &table).map_err(fail)?;
333                    calls.lock().unwrap().push((source.clone(), table.clone()));
334                    let q = if parse || !r.quoting.any() {
335                        None
336                    } else {
337                        let got = quote.get_or_init(|| match &connections {
338                            Some(c) if !kind.is_empty() => c.identifier_quote(&kind),
339                            _ => Err("no connection is known here to quote with".into()),
340                        });
341                        match got {
342                            Ok(Some(q)) => Some(q.clone()),
343                            Ok(None) => {
344                                return Err(fail(format!(
345                                    "`quoting` needs the identifier quote character, but the `{kind}` plugin doesn't report one; update it (`dre plugin update {kind}`)"
346                                )));
347                            }
348                            Err(e) => return Err(fail(format!("can't quote: {e}"))),
349                        }
350                    };
351                    let text = r.relation(q.as_deref());
352                    relations.lock().unwrap().push((text.clone(), r));
353                    Ok(Value::from(text))
354                },
355            );
356        }
357        let runner = cfg.runner;
358        let warnings: Arc<Mutex<Vec<String>>> = Arc::default();
359        let lookups = Arc::new(Lookups {
360            root: cfg.root.to_path_buf(),
361            defs: cfg.lookups,
362            inline_max: cfg.lookup_inline_max_rows,
363            runner: runner.clone(),
364            tables: Mutex::default(),
365            loaded: Mutex::default(),
366            warnings: warnings.clone(),
367        });
368        let l = lookups.clone();
369        env.add_function("lookup", move |name: String| -> Result<Value, Error> {
370            let t = l.table(&name)?;
371            let rows = QueryRows {
372                columns: t.columns.clone(),
373                rows: t
374                    .rows
375                    .iter()
376                    .map(|r| r.iter().map(cell_value).collect())
377                    .collect(),
378            };
379            Ok(Value::from_object(QueryResult::new(rows)))
380        });
381        let default_max = cfg.run_query_max_rows;
382        let runner_c = runner.clone();
383        let rels = relations.clone();
384        env.add_function(
385            "run_query",
386            move |sql: String, kwargs: Kwargs| -> Result<Value, Error> {
387                let max_rows: Option<u64> = kwargs.get("max_rows")?;
388                let profile: Option<String> = kwargs.get("profile")?;
389                kwargs.assert_all_used()?;
390                if parse {
391                    let mut empty = QueryResult::new(QueryRows::default());
392                    empty.lenient = true;
393                    return Ok(Value::from_object(empty));
394                }
395                let profile = choose_profile("run_query()", &sql, profile, &rels)?;
396                let Some(runner) = &runner else {
397                    return Err(Error::new(
398                        ErrorKind::InvalidOperation,
399                        "`run_query()` has no connection here",
400                    ));
401                };
402                let rows = runner
403                    .run_query(&sql, max_rows.unwrap_or(default_max), profile.as_deref())
404                    .map_err(|e| Error::new(ErrorKind::InvalidOperation, e))?;
405                Ok(Value::from_object(QueryResult::new(rows)))
406            },
407        );
408        // Per rendered file: a later query may have recreated the relation.
409        let columns_cache: Arc<Mutex<ColumnsCache>> = Arc::default();
410        let cache = columns_cache.clone();
411        let rels = relations.clone();
412        env.add_function(
413            "columns",
414            move |rel: String, kwargs: Kwargs| -> Result<Value, Error> {
415                let profile: Option<String> = kwargs.get("profile")?;
416                kwargs.assert_all_used()?;
417                if parse {
418                    return Ok(Value::from(Vec::<Value>::new()));
419                }
420                let what = format!("columns('{}')", crate::secrets::mask(&rel));
421                let profile = choose_profile(&what, &rel, profile, &rels)?;
422                let key = (rel.clone(), profile.clone());
423                if let Some(v) = cache.lock().unwrap().get(&key) {
424                    return Ok(v.clone());
425                }
426                let Some(runner) = &runner_c else {
427                    return Err(Error::new(
428                        ErrorKind::InvalidOperation,
429                        format!("`{what}` has no connection here"),
430                    ));
431                };
432                let sql = format!("select * from {rel} as _dre_cols where 1=0");
433                let cols = runner
434                    .columns(&sql, profile.as_deref())
435                    .map_err(|e| Error::new(ErrorKind::InvalidOperation, format!("`{what}` failed: {e}")))?;
436                let v = Value::from(
437                    cols.into_iter()
438                        .map(|c| {
439                            Value::from_iter([
440                                ("name", Value::from(c.name)),
441                                ("type", Value::from(c.data_type)),
442                            ])
443                        })
444                        .collect::<Vec<_>>(),
445                );
446                cache.lock().unwrap().insert(key, v.clone());
447                Ok(v)
448            },
449        );
450
451        // Every macro file is combined into one template, imported (on the first line, so line
452        // numbers don't move) into everything rendered. Each package gets its own template,
453        // imported under the package's name.
454        let root = cfg.root.to_path_buf();
455        let display = |p: &Path| {
456            p.strip_prefix(&root)
457                .map(Path::to_path_buf)
458                .unwrap_or_else(|_| p.to_path_buf())
459        };
460        let own: Vec<(PathBuf, PathBuf)> = cfg.macros.iter().map(|m| (cfg.root.join(m), m.clone())).collect();
461        let (combined, names, spans) = combine(&own)?;
462        let mut import = if names.is_empty() {
463            String::new()
464        } else {
465            format!("{{% from \"{MACROS}\" import {} %}}", names.join(", "))
466        };
467        let mut templates = vec![(MACROS.to_string(), combined, spans, !names.is_empty())];
468        for p in &cfg.packages {
469            let files: Vec<(PathBuf, PathBuf)> = p.macros.iter().map(|m| (m.clone(), display(m))).collect();
470            let (src, _, spans) = combine(&files)?;
471            let t = package_template(&p.name);
472            import.push_str(&format!("{{% import \"{t}\" as {} %}}", p.name));
473            templates.push((t, src, spans, true));
474        }
475        add_ref(&mut env, cfg.root, cfg.sql, lookups, &import);
476        add_dispatch(
477            &mut env,
478            &cfg.project_name,
479            !names.is_empty(),
480            &cfg.packages,
481            cfg.dispatch,
482            source_type,
483            parse,
484        );
485        let mut r = Renderer {
486            env,
487            raised,
488            columns_cache,
489            source_calls,
490            destination,
491            warnings,
492            import,
493            macro_files: BTreeMap::new(),
494        };
495        for (name, src, spans, add) in templates {
496            r.macro_files.insert(name.clone(), spans);
497            if add {
498                r.env
499                    .add_template_owned(name.clone(), src)
500                    .map_err(|e| r.error(Path::new(&name), &e))?;
501            }
502        }
503        Ok(r)
504    }
505
506    /// Warnings raised since the last call.
507    pub fn take_warnings(&self) -> Vec<String> {
508        std::mem::take(&mut *self.warnings.lock().unwrap())
509    }
510
511    /// `(source, table)` of every `source()` called since the last call, in order, each once.
512    pub fn take_sources(&self) -> Vec<(String, String)> {
513        let mut out: Vec<(String, String)> = Vec::new();
514        for c in std::mem::take(&mut *self.source_calls.lock().unwrap()) {
515            if !out.contains(&c) {
516                out.push(c);
517            }
518        }
519        out
520    }
521
522    /// Set (or clear) the destination `destination.*` reads.
523    pub fn set_destination(&self, d: Option<Connection>) {
524        *self.destination.lock().unwrap() = d;
525    }
526
527    /// Render `src`, reporting errors against `file`.
528    pub fn render(&self, file: &Path, src: &str) -> Result<String, RenderError> {
529        self.columns_cache.lock().unwrap().clear();
530        *self.raised.lock().unwrap() = None;
531        let full = format!("{}{src}", self.import);
532        let name = file.to_string_lossy().to_string();
533        let tmpl = self
534            .env
535            .template_from_named_str(&name, &full)
536            .map_err(|e| self.error(file, &e))?;
537        tmpl.render(()).map_err(|e| {
538            let mut err = self.error(file, &e);
539            if let Some(m) = self.raised.lock().unwrap().take() {
540                err.message = m;
541            }
542            err
543        })
544    }
545
546    fn error(&self, file: &Path, e: &Error) -> RenderError {
547        // Report the innermost location: a macro file if the error happened inside a macro.
548        let mut deepest: &Error = e;
549        while let Some(src) = std::error::Error::source(deepest).and_then(|s| s.downcast_ref::<Error>()) {
550            deepest = src;
551        }
552        let (name, line) = match (deepest.name(), deepest.line()) {
553            (Some(n), l) => (n.to_string(), l),
554            _ => (e.name().unwrap_or_default().to_string(), e.line()),
555        };
556        let (file, line) = if let Some(spans) = self.macro_files.get(&name) {
557            match line.and_then(|l| {
558                spans
559                    .iter()
560                    .find(|(s, _, n)| l >= *s && l < s + n)
561                    .map(|(s, f, _)| (f, l - s + 1))
562            }) {
563                Some((f, l)) => (f.clone(), Some(l)),
564                None => (file.to_path_buf(), line),
565            }
566        } else {
567            (file.to_path_buf(), line)
568        };
569        let message = deepest
570            .detail()
571            .map(str::to_string)
572            .unwrap_or_else(|| deepest.kind().to_string());
573        RenderError { file, line, message }
574    }
575}
576
577/// `var()` (`--var` first, then the Binding's vars, then the default) and `env_var()`.
578fn add_vars(env: &mut Environment<'static>, vars: JsonMap<String, Json>, cli: BTreeMap<String, String>) {
579    env.add_function(
580        "var",
581        move |name: String, default: Option<Value>| -> Result<Value, Error> {
582            if let Some(v) = cli.get(&name) {
583                return Ok(Value::from(v.clone()));
584            }
585            if let Some(v) = vars.get(&name) {
586                return Ok(Value::from_serialize(v));
587            }
588            default.ok_or_else(|| {
589                Error::new(
590                    ErrorKind::UndefinedError,
591                    format!("`var('{name}')` has no value and no default"),
592                )
593            })
594        },
595    );
596    env.add_function("env_var", |name: String, default: Option<Value>| -> Result<Value, Error> {
597        match std::env::var(&name) {
598            Ok(v) => Ok(Value::from(v)),
599            Err(_) => default.ok_or_else(|| {
600                Error::new(
601                    ErrorKind::UndefinedError,
602                    format!("`env_var('{name}')`: environment variable `{name}` is not set and no default is given"),
603                )
604            }),
605        }
606    });
607}
608
609/// Renders the values that choose a connection or a source: every `profile:` value and a
610/// source's `profile`, `database`, `schema` and `identifier`. Only `var()`, `env_var()`, `run.*`,
611/// `target.name` and the calendar functions exist, because these are rendered before any
612/// connection is chosen: the parse pass, `dre ls` and the manifest need no database.
613pub struct Limited {
614    env: Environment<'static>,
615}
616
617/// What a [`Limited`] value can't use, and why.
618const LIMITED_FUNCTIONS: &[&str] = &[
619    "run_query",
620    "columns",
621    "source",
622    "ref",
623    "lookup",
624    "dispatch",
625    "profile",
626];
627const LIMITED_GLOBALS: &[&str] = &["connection", "destination"];
628
629impl Limited {
630    pub fn new(
631        context: RunContext,
632        vars: JsonMap<String, Json>,
633        cli_vars: BTreeMap<String, String>,
634    ) -> Limited {
635        let mut env = Environment::new();
636        env.set_undefined_behavior(UndefinedBehavior::Strict);
637        crate::mutable::register(&mut env);
638        add_vars(&mut env, vars, cli_vars);
639        env.add_function("raise_error", |message: String| -> Result<Value, Error> {
640            Err(Error::new(ErrorKind::InvalidOperation, message))
641        });
642        crate::dates::register(&mut env, context.calendar, context.date);
643        env.add_global("target", Value::from_object(Target(context.target.clone())));
644        env.add_global("run", Value::from_object(Run(context)));
645        for f in LIMITED_FUNCTIONS {
646            let name = *f;
647            env.add_function(name, move |_args: minijinja::value::Rest<Value>| -> Result<Value, Error> {
648                Err(Error::new(
649                    ErrorKind::InvalidOperation,
650                    format!("`{name}()` can't be used here: this value chooses a connection or a source, so it's rendered before any connection is open"),
651                ))
652            });
653        }
654        for g in LIMITED_GLOBALS {
655            env.add_global(*g, Value::from_object(Forbidden(g)));
656        }
657        Limited { env }
658    }
659
660    /// Render `src`, the value of `key` (named in errors, e.g. "`profile`").
661    pub fn render(&self, key: &str, src: &str) -> Result<String, String> {
662        if !crate::preflight::is_templated(src) {
663            return Ok(src.to_string());
664        }
665        self.env
666            .render_str(src, ())
667            .map(|v| v.trim().to_string())
668            .map_err(|e| {
669                let mut deepest: &Error = &e;
670                while let Some(s) = std::error::Error::source(deepest).and_then(|s| s.downcast_ref::<Error>())
671                {
672                    deepest = s;
673                }
674                let msg = deepest
675                    .detail()
676                    .map(str::to_string)
677                    .unwrap_or_else(|| deepest.kind().to_string());
678                format!("{key} `{src}`: {msg}")
679            })
680    }
681}
682
683/// `connection` or `destination` in a [`Limited`] value.
684#[derive(Debug)]
685struct Forbidden(&'static str);
686
687impl Object for Forbidden {
688    fn get_value(self: &Arc<Self>, _key: &Value) -> Option<Value> {
689        Some(Value::from(Error::new(
690            ErrorKind::InvalidOperation,
691            format!(
692                "`{}.*` can't be used here: this value chooses a connection or a source, so it's rendered before any connection is open",
693                self.0
694            ),
695        )))
696    }
697
698    fn render(self: &Arc<Self>, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        Err(fmt::Error)
700    }
701}
702
703fn package_template(name: &str) -> String {
704    format!("__dre_package_{name}__")
705}
706
707/// Concatenate macro files `(path to read, path to report)` into one template source, with
708/// every macro name defined and where each file starts.
709#[allow(clippy::type_complexity)]
710fn combine(
711    files: &[(PathBuf, PathBuf)],
712) -> Result<(String, Vec<String>, Vec<(usize, PathBuf, usize)>), RenderError> {
713    let re = regex::Regex::new(r"\{%-?\s*macro\s+([A-Za-z_]\w*)").unwrap();
714    let mut combined = String::new();
715    let mut names = Vec::new();
716    let mut spans = Vec::new();
717    for (read, shown) in files {
718        let src = std::fs::read_to_string(read).map_err(|e| RenderError {
719            file: shown.clone(),
720            line: None,
721            message: format!("can't read macro file: {e}"),
722        })?;
723        let start = combined.matches('\n').count() + 1;
724        names.extend(re.captures_iter(&src).map(|c| c[1].to_string()));
725        combined.push_str(&src);
726        if !combined.ends_with('\n') {
727            combined.push('\n');
728        }
729        spans.push((start, shown.clone(), src.matches('\n').count() + 1));
730    }
731    Ok((combined, names, spans))
732}
733
734/// `dispatch('name', 'namespace')` returns the macro to call for the active source: in each
735/// namespace of the search order, `<source type>__name`, then `default__name`. The search order
736/// is `dispatch:` in dre_project.yml, else the root project then the namespace, so a project can
737/// override a package's macro by defining, say, `databricks__name` in its own macros/.
738fn add_dispatch(
739    env: &mut Environment<'static>,
740    project: &str,
741    project_has_macros: bool,
742    packages: &[Package],
743    order: DispatchOrder,
744    source_type: String,
745    parse: bool,
746) {
747    let project = project.to_string();
748    let templates: BTreeMap<String, String> = packages
749        .iter()
750        .map(|p| (p.name.clone(), package_template(&p.name)))
751        .collect();
752    env.add_function(
753        "dispatch",
754        move |state: &minijinja::State<'_, '_>,
755              name: String,
756              namespace: Option<String>|
757              -> Result<Value, Error> {
758            let ns = namespace.unwrap_or_else(|| project.clone());
759            let search = order.get(&ns).cloned().unwrap_or_else(|| {
760                if ns == project {
761                    vec![ns.clone()]
762                } else {
763                    vec![project.clone(), ns.clone()]
764                }
765            });
766            let candidates = [format!("{source_type}__{name}"), format!("default__{name}")];
767            for n in &search {
768                let template = if n == &project {
769                    if !project_has_macros {
770                        continue;
771                    }
772                    MACROS.to_string()
773                } else {
774                    match templates.get(n) {
775                        Some(t) => t.clone(),
776                        None => continue,
777                    }
778                };
779                let tmpl = state.env().get_template(&template)?;
780                let captured = tmpl.render_captured(())?;
781                let st = captured.state();
782                for c in &candidates {
783                    if st.lookup(c).is_some_and(|v| !v.is_undefined()) {
784                        return Ok(Value::from_object(Dispatched {
785                            template,
786                            name: c.clone(),
787                        }));
788                    }
789                }
790            }
791            if parse {
792                // The parse pass doesn't know the connection type: a macro only some types
793                // implement renders as nothing.
794                return Ok(Value::from_object(Dispatched {
795                    template: String::new(),
796                    name: String::new(),
797                }));
798            }
799            Err(Error::new(
800                ErrorKind::InvalidOperation,
801                format!(
802                    "`dispatch('{name}', '{ns}')`: no `{}` or `{}` in {}",
803                    candidates[0],
804                    candidates[1],
805                    search.join(", ")
806                ),
807            ))
808        },
809    );
810}
811
812/// A macro chosen by `dispatch()`, called by name in its own template.
813#[derive(Debug)]
814struct Dispatched {
815    template: String,
816    name: String,
817}
818
819impl Object for Dispatched {
820    fn call(self: &Arc<Self>, state: &minijinja::State<'_, '_>, args: &[Value]) -> Result<Value, Error> {
821        if self.template.is_empty() {
822            return Ok(Value::from(""));
823        }
824        let tmpl = state.env().get_template(&self.template)?;
825        let out = tmpl.render_captured(())?.state().call_macro(&self.name, args)?;
826        Ok(Value::from(out))
827    }
828}
829
830/// `ref('name')`: another `.sql` file, rendered in the same context (vars, `run.*`, macros, the
831/// Binding's connection) and returned in parentheses, ready to use as a subquery or CTE body.
832/// DRE builds no tables, so a ref inlines SQL rather than pointing at a materialised model.
833fn add_ref(
834    env: &mut Environment<'static>,
835    root: &Path,
836    sql: BTreeMap<String, PathBuf>,
837    lookups: Arc<Lookups>,
838    import: &str,
839) {
840    let root = root.to_path_buf();
841    let import = import.to_string();
842    // The chain of refs being rendered, to report cycles instead of recursing forever.
843    let stack: Arc<Mutex<Vec<String>>> = Arc::default();
844    env.add_function(
845        "ref",
846        move |state: &minijinja::State<'_, '_>, name: String| -> Result<Value, Error> {
847            if lookups.defs.contains_key(&name) {
848                return lookups.relation(&name).map(Value::from);
849            }
850            let Some(rel) = sql.get(&name) else {
851                return Err(Error::new(
852                    ErrorKind::InvalidOperation,
853                    format!("`ref('{name}')`: no `{name}.sql` and no lookup `{name}` in the project"),
854                ));
855            };
856            {
857                let mut chain = stack.lock().unwrap();
858                if chain.contains(&name) {
859                    chain.push(name.clone());
860                    let cycle = chain.join(" → ");
861                    chain.clear();
862                    return Err(Error::new(
863                        ErrorKind::InvalidOperation,
864                        format!("`ref()` cycle: {cycle}"),
865                    ));
866                }
867                chain.push(name.clone());
868            }
869            let result = (|| {
870                let src = std::fs::read_to_string(root.join(rel)).map_err(|e| {
871                    Error::new(
872                        ErrorKind::InvalidOperation,
873                        format!("can't read {}: {e}", rel.display()),
874                    )
875                })?;
876                let file = rel.to_string_lossy().to_string();
877                let rendered = state
878                    .env()
879                    .template_from_named_str(&file, &format!("{import}{src}"))?
880                    .render(())?;
881                let statements = crate::sqlsplit::split(&rendered);
882                match statements.as_slice() {
883                    [one] => Ok(Value::from(format!("(\n{}\n)", one.text))),
884                    _ => Err(Error::new(
885                        ErrorKind::InvalidOperation,
886                        format!(
887                            "`ref('{name}')` needs {} to hold exactly one statement, but it has {}",
888                            rel.display(),
889                            statements.len()
890                        ),
891                    )),
892                }
893            })();
894            let mut chain = stack.lock().unwrap();
895            if chain.last() == Some(&name) {
896                chain.pop();
897            }
898            result
899        },
900    );
901}
902
903/// Lookups for one Binding: read once, inlined or loaded once.
904struct Lookups {
905    root: PathBuf,
906    defs: BTreeMap<String, Lookup>,
907    inline_max: u64,
908    runner: Option<Arc<dyn QueryRunner>>,
909    tables: Mutex<BTreeMap<String, Arc<Table>>>,
910    /// What `ref()` returns for each lookup already used: inline SQL or a temp table's name.
911    loaded: Mutex<BTreeMap<String, String>>,
912    warnings: Arc<Mutex<Vec<String>>>,
913}
914
915impl Lookups {
916    fn table(&self, name: &str) -> Result<Arc<Table>, Error> {
917        let def = self.defs.get(name).ok_or_else(|| {
918            Error::new(
919                ErrorKind::InvalidOperation,
920                format!("no lookup `{name}` under lookups/"),
921            )
922        })?;
923        if let Some(t) = self.tables.lock().unwrap().get(name) {
924            return Ok(t.clone());
925        }
926        let t = Arc::new(lookups::read(&self.root, def).map_err(|e| {
927            Error::new(
928                ErrorKind::InvalidOperation,
929                format!("{}: {e}", def.file.display()),
930            )
931        })?);
932        self.tables.lock().unwrap().insert(name.to_string(), t.clone());
933        Ok(t)
934    }
935
936    fn relation(&self, name: &str) -> Result<String, Error> {
937        if let Some(r) = self.loaded.lock().unwrap().get(name) {
938            return Ok(r.clone());
939        }
940        let t = self.table(name)?;
941        let rows = t.rows.len() as u64;
942        let load = match self.defs[name].load {
943            Load::Inline => false,
944            Load::TempTable => true,
945            Load::Auto => rows > self.inline_max,
946        };
947        let loaded = match (&self.runner, load) {
948            (Some(runner), true) => runner.load(name, &t).map_err(|e| {
949                Error::new(
950                    ErrorKind::InvalidOperation,
951                    format!("loading lookup `{name}`: {e}"),
952                )
953            })?,
954            _ => None,
955        };
956        let relation = match loaded {
957            Some((relation, warning)) => {
958                if let Some(w) = warning {
959                    self.warn(format!("lookup `{name}`: {w}"));
960                }
961                relation
962            }
963            None => {
964                if load && self.runner.is_some() {
965                    self.warn(format!(
966                        "lookup `{name}` has {rows} rows; this source can't load it into a temp table, so it's inlined in the SQL. Data this size probably belongs in a table in the database"
967                    ));
968                }
969                t.inline_sql()
970            }
971        };
972        self.loaded
973            .lock()
974            .unwrap()
975            .insert(name.to_string(), relation.clone());
976        Ok(relation)
977    }
978
979    fn warn(&self, w: String) {
980        self.warnings.lock().unwrap().push(w);
981    }
982}
983
984fn cell_value(c: &Cell) -> Value {
985    match c {
986        Cell::Null => Value::from(()),
987        Cell::Text(s) => Value::from(s.clone()),
988        Cell::Int(n) => Value::from(*n),
989        Cell::Num(n) => Value::from(*n),
990        Cell::Bool(b) => Value::from(*b),
991        Cell::Date(d) => Value::from(d.to_string()),
992    }
993}
994
995#[derive(Debug)]
996struct Run(RunContext);
997
998impl Object for Run {
999    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1000        let c = &self.0;
1001        Some(match key.as_str()? {
1002            "report" => Value::from(c.report.clone()),
1003            "set" => c.set.clone().map(Value::from).unwrap_or(Value::from(())),
1004            "target" => Value::from(c.target.clone()),
1005            "profile" => removed("run.profile", "`connection.name` (the query's connection)"),
1006            "source_type" => removed("run.source_type", "`connection.type` (the query's connection)"),
1007            "schedule" => c.schedule.clone().map(Value::from).unwrap_or(Value::from(())),
1008            "date" => Date::value(c.date, c.calendar),
1009            "now" => DateTime::now(c.now, c.calendar),
1010            "scheduled_at" => c
1011                .scheduled_at
1012                .map(|t| DateTime::now(t, c.calendar))
1013                .unwrap_or(Value::from(())),
1014            "timezone" => Value::from(c.calendar.tz.name()),
1015            _ => return None,
1016        })
1017    }
1018
1019    fn call_method(
1020        self: &Arc<Self>,
1021        _: &minijinja::State<'_, '_>,
1022        method: &str,
1023        args: &[Value],
1024    ) -> Result<Value, Error> {
1025        match method {
1026            "date_format" => {
1027                let fmt = args.first().and_then(|a| a.as_str()).ok_or_else(|| {
1028                    Error::new(
1029                        ErrorKind::InvalidOperation,
1030                        "`run.date_format()` takes a strftime format string",
1031                    )
1032                })?;
1033                let mut out = String::new();
1034                use std::fmt::Write;
1035                write!(out, "{}", self.0.date.format(fmt)).map_err(|_| {
1036                    Error::new(
1037                        ErrorKind::InvalidOperation,
1038                        format!("invalid date format `{fmt}`"),
1039                    )
1040                })?;
1041                Ok(Value::from(out))
1042            }
1043            _ => Err(Error::from(ErrorKind::UnknownMethod)),
1044        }
1045    }
1046}
1047
1048/// An error value naming a removed template name and what to write instead.
1049fn removed(name: &str, instead: &str) -> Value {
1050    Value::from(Error::new(
1051        ErrorKind::InvalidOperation,
1052        format!("`{name}` was removed in DRE 0.2: use {instead}"),
1053    ))
1054}
1055
1056/// `profile(..., role=)` takes `connection` or `destination`.
1057fn check_role(role: Option<&str>) -> Result<(), Error> {
1058    match role {
1059        None | Some("connection") | Some("destination") => Ok(()),
1060        Some("source") => Err(Error::new(
1061            ErrorKind::InvalidOperation,
1062            "`role='source'` was renamed in DRE 0.2: use `role='connection'`",
1063        )),
1064        Some(r) => Err(Error::new(
1065            ErrorKind::InvalidOperation,
1066            format!("`role='{r}'`: role must be 'connection' or 'destination'"),
1067        )),
1068    }
1069}
1070
1071/// The connection `run_query()`/`columns()` uses: `profile=` when given (it must agree with
1072/// every source `sql` reads), else the connection of the sources `sql` reads, else `None` (the
1073/// renderer's own).
1074fn choose_profile(
1075    what: &str,
1076    sql: &str,
1077    explicit: Option<String>,
1078    relations: &Mutex<Vec<(String, ResolvedSource)>>,
1079) -> Result<Option<String>, Error> {
1080    let mut used: Vec<(String, String)> = Vec::new();
1081    for (text, r) in relations.lock().unwrap().iter() {
1082        if let Some(p) = &r.profile
1083            && mentions(sql, text)
1084            && !used.iter().any(|(k, _)| *k == r.key())
1085        {
1086            used.push((r.key(), p.clone()));
1087        }
1088    }
1089    let fail = |m: String| Error::new(ErrorKind::InvalidOperation, format!("`{what}`: {m}"));
1090    match explicit {
1091        Some(p) => {
1092            if let Some((k, q)) = used.iter().find(|(_, q)| *q != p) {
1093                return Err(fail(format!(
1094                    "`profile='{p}'` disagrees with source `{k}`, which is on connection `{q}`"
1095                )));
1096            }
1097            Ok(Some(p))
1098        }
1099        None => match used.as_slice() {
1100            [] => Ok(None),
1101            [(_, p), rest @ ..] => {
1102                if let Some((k, q)) = rest.iter().find(|(_, q)| q != p) {
1103                    return Err(fail(format!(
1104                        "reads source `{}` on connection `{p}` and source `{k}` on `{q}`; one statement runs on one connection",
1105                        used[0].0
1106                    )));
1107                }
1108                Ok(Some(p.clone()))
1109            }
1110        },
1111    }
1112}
1113
1114/// Whether `text` (a rendered relation) appears in `sql` as a whole name.
1115fn mentions(sql: &str, text: &str) -> bool {
1116    let ident = |c: char| c.is_alphanumeric() || c == '_';
1117    sql.match_indices(text).any(|(i, _)| {
1118        let before = sql[..i].chars().next_back();
1119        let after = sql[i + text.len()..].chars().next();
1120        !before.is_some_and(|c| ident(c) || c == '.') && !after.is_some_and(|c| ident(c) || c == '.')
1121    })
1122}
1123
1124/// `target`: the run's environment. Only `target.name` remains; every other field moved to
1125/// `connection.*` in DRE 0.2.
1126#[derive(Debug)]
1127struct Target(String);
1128
1129impl Object for Target {
1130    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1131        let k = key.as_str()?;
1132        Some(match k {
1133            "name" => Value::from(self.0.clone()),
1134            "type" => removed("target.type", "`connection.type` (the query's connection)"),
1135            "profile" => removed("target.profile", "`connection.name` (the query's connection)"),
1136            _ => removed(
1137                &format!("target.{k}"),
1138                &format!(
1139                    "`connection.{k}` (the query's connection) or `profile('<name>').{k}`; `target` is now only the environment, `target.name`"
1140                ),
1141            ),
1142        })
1143    }
1144
1145    fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1146        f.write_str(&self.0)
1147    }
1148}
1149
1150/// The parse pass's stand-in for `connection`, `destination` and `profile()`: every field is
1151/// undefined, which renders as nothing.
1152#[derive(Debug)]
1153struct Lenient;
1154
1155impl Object for Lenient {
1156    fn get_value(self: &Arc<Self>, _key: &Value) -> Option<Value> {
1157        None
1158    }
1159
1160    fn render(self: &Arc<Self>, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
1161        Ok(())
1162    }
1163}
1164
1165/// `profile('name')`: a profile target's fields, refusing secret ones.
1166#[derive(Debug)]
1167struct ConnectionValue(Connection);
1168
1169impl Object for ConnectionValue {
1170    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1171        connection_field(&self.0, key)
1172    }
1173
1174    fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1175        write!(f, "{}", self.0.profile)
1176    }
1177}
1178
1179/// `connection`: the query's connection (or the Binding's inherited one outside query SQL),
1180/// looked up the first time a template reads it.
1181struct LazyConnection {
1182    connections: Option<Arc<dyn Connections>>,
1183    name: Option<String>,
1184    resolved: std::sync::OnceLock<Result<Connection, String>>,
1185}
1186
1187impl fmt::Debug for LazyConnection {
1188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1189        f.write_str("connection")
1190    }
1191}
1192
1193impl LazyConnection {
1194    fn get(&self) -> &Result<Connection, String> {
1195        self.resolved.get_or_init(|| {
1196            let name = self.name.as_deref().ok_or(
1197                "no connection here: this isn't a query's SQL and the Binding inherits no `profile`",
1198            )?;
1199            match &self.connections {
1200                Some(c) => c.profile(name, Some("connection")),
1201                None => Err("no profiles.yml is loaded here".into()),
1202            }
1203        })
1204    }
1205}
1206
1207impl Object for LazyConnection {
1208    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1209        match self.get() {
1210            Ok(c) => connection_field(c, key),
1211            Err(e) => Some(Value::from(Error::new(
1212                ErrorKind::InvalidOperation,
1213                format!("`connection`: {e}"),
1214            ))),
1215        }
1216    }
1217
1218    fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1219        match &self.name {
1220            Some(n) => f.write_str(n),
1221            None => Err(fmt::Error),
1222        }
1223    }
1224}
1225
1226/// `destination`: the destination whose `path` and options are being rendered.
1227#[derive(Debug)]
1228struct DestinationSlot(Arc<Mutex<Option<Connection>>>);
1229
1230impl Object for DestinationSlot {
1231    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1232        match &*self.0.lock().unwrap() {
1233            Some(c) => connection_field(c, key),
1234            None => Some(Value::from(Error::new(
1235                ErrorKind::InvalidOperation,
1236                "`destination` only exists while rendering a destination's `path` and options",
1237            ))),
1238        }
1239    }
1240
1241    fn render(self: &Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1242        match &*self.0.lock().unwrap() {
1243            Some(c) => f.write_str(&c.profile),
1244            None => Err(fmt::Error),
1245        }
1246    }
1247}
1248
1249/// One field of a profile target for templates; a secret or missing one is an error value.
1250fn connection_field(c: &Connection, key: &Value) -> Option<Value> {
1251    {
1252        let k = key.as_str()?;
1253        Some(match k {
1254            "name" | "profile" => Value::from(c.profile.clone()),
1255            "target" => Value::from(c.target.clone()),
1256            "type" => Value::from(c.kind.clone()),
1257            _ if c.secrets.iter().any(|s| s == k) => Value::from(Error::new(
1258                ErrorKind::InvalidOperation,
1259                format!(
1260                    "`{k}` of profile `{}` holds a secret, so templates can't read it (it would end up in compiled SQL and logs)",
1261                    c.profile
1262                ),
1263            )),
1264            _ => match c.fields.get(k) {
1265                Some(v) => Value::from_serialize(v),
1266                None => Value::from(Error::new(
1267                    ErrorKind::UndefinedError,
1268                    format!(
1269                        "profile `{}` (target `{}`) has no field `{k}`; it has: {}",
1270                        c.profile,
1271                        c.target,
1272                        c.fields
1273                            .keys()
1274                            .filter(|f| !c.secrets.contains(f))
1275                            .cloned()
1276                            .collect::<Vec<_>>()
1277                            .join(", ")
1278                    ),
1279                )),
1280            },
1281        })
1282    }
1283}
1284
1285#[derive(Debug)]
1286struct QueryResult {
1287    columns: Vec<String>,
1288    rows: Vec<Value>,
1289    /// The parse pass's empty result: any `column()` is an empty list.
1290    lenient: bool,
1291}
1292
1293impl QueryResult {
1294    fn new(q: QueryRows) -> QueryResult {
1295        let cols: Arc<Vec<String>> = Arc::new(q.columns.clone());
1296        let rows = q
1297            .rows
1298            .into_iter()
1299            .map(|values| {
1300                Value::from_object(Row {
1301                    columns: cols.clone(),
1302                    values,
1303                })
1304            })
1305            .collect();
1306        QueryResult {
1307            columns: q.columns,
1308            rows,
1309            lenient: false,
1310        }
1311    }
1312}
1313
1314impl Object for QueryResult {
1315    fn repr(self: &Arc<Self>) -> ObjectRepr {
1316        ObjectRepr::Seq
1317    }
1318
1319    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1320        match key.as_str() {
1321            Some("rows") => Some(Value::from(self.rows.clone())),
1322            Some("columns") => Some(Value::from(self.columns.clone())),
1323            _ => self.rows.get(key.as_usize()?).cloned(),
1324        }
1325    }
1326
1327    fn enumerate(self: &Arc<Self>) -> Enumerator {
1328        Enumerator::Seq(self.rows.len())
1329    }
1330
1331    /// `result.column('name')` (or an index): that column's values, one per row.
1332    fn call_method(
1333        self: &Arc<Self>,
1334        _: &minijinja::State<'_, '_>,
1335        method: &str,
1336        args: &[Value],
1337    ) -> Result<Value, Error> {
1338        if method != "column" {
1339            return Err(Error::from(ErrorKind::UnknownMethod));
1340        }
1341        if self.lenient {
1342            return Ok(Value::from(Vec::<Value>::new()));
1343        }
1344        let [key] = args else {
1345            return Err(Error::new(
1346                ErrorKind::InvalidOperation,
1347                "`column()` takes one column name or index",
1348            ));
1349        };
1350        let i = match key.as_str() {
1351            Some(n) => self.columns.iter().position(|c| c == n).ok_or_else(|| {
1352                Error::new(
1353                    ErrorKind::InvalidOperation,
1354                    format!("no column `{n}` (columns: {})", self.columns.join(", ")),
1355                )
1356            })?,
1357            None => key
1358                .as_usize()
1359                .filter(|i| *i < self.columns.len())
1360                .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, format!("no column {key}")))?,
1361        };
1362        Ok(Value::from(
1363            self.rows
1364                .iter()
1365                .map(|r| r.get_item_by_index(i).unwrap_or_default())
1366                .collect::<Vec<_>>(),
1367        ))
1368    }
1369}
1370
1371/// One `run_query()` row: accessible by column name (`row.region`, `row['region']`) or index.
1372#[derive(Debug)]
1373struct Row {
1374    columns: Arc<Vec<String>>,
1375    values: Vec<Value>,
1376}
1377
1378impl Object for Row {
1379    fn repr(self: &Arc<Self>) -> ObjectRepr {
1380        ObjectRepr::Seq
1381    }
1382
1383    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1384        if let Some(name) = key.as_str() {
1385            let i = self.columns.iter().position(|c| c == name)?;
1386            return self.values.get(i).cloned();
1387        }
1388        self.values.get(key.as_usize()?).cloned()
1389    }
1390
1391    fn enumerate(self: &Arc<Self>) -> Enumerator {
1392        Enumerator::Seq(self.values.len())
1393    }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398    use super::*;
1399
1400    fn renderer(vars: Json, cli: &[(&str, &str)], macros: &[(&str, &str)]) -> (tempfile::TempDir, Renderer) {
1401        let dir = tempfile::tempdir().unwrap();
1402        let mut files = Vec::new();
1403        for (name, src) in macros {
1404            let p = PathBuf::from("macros").join(name);
1405            std::fs::create_dir_all(dir.path().join("macros")).unwrap();
1406            std::fs::write(dir.path().join(&p), src).unwrap();
1407            files.push(p);
1408        }
1409        let r = Renderer::new(RendererConfig {
1410            root: dir.path(),
1411            macros: &files,
1412            context: RunContext {
1413                report: "monthly".into(),
1414                set: Some("client_a".into()),
1415                target: "prod".into(),
1416                schedule: None,
1417                date: NaiveDate::from_ymd_opt(2026, 1, 25).unwrap(),
1418                now: Utc::now(),
1419                scheduled_at: None,
1420                calendar: Calendar::default(),
1421            },
1422            vars: vars.as_object().cloned().unwrap_or_default(),
1423            cli_vars: cli.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
1424            runner: None,
1425            connections: None,
1426            mode: Mode::Run,
1427            connection: None,
1428            source_type: "duckdb".into(),
1429            sources: None,
1430            run_query_max_rows: 10_000,
1431            sql: BTreeMap::new(),
1432            lookups: BTreeMap::new(),
1433            lookup_inline_max_rows: 200,
1434            packages: Vec::new(),
1435            project_name: "acme".into(),
1436            dispatch: DispatchOrder::new(),
1437        })
1438        .unwrap();
1439        (dir, r)
1440    }
1441
1442    fn render(r: &Renderer, src: &str) -> Result<String, RenderError> {
1443        r.render(Path::new("reports/q.sql"), src)
1444    }
1445
1446    fn ok(src: &str) -> String {
1447        let (_d, r) = renderer(serde_json::json!({"fixed": [3, 1, 2], "m": {"a": 1}}), &[], &[]);
1448        render(&r, src).unwrap_or_else(|e| panic!("{src}: {e}"))
1449    }
1450
1451    fn err(src: &str) -> String {
1452        let (_d, r) = renderer(serde_json::json!({"fixed": [3, 1, 2], "m": {"a": 1}}), &[], &[]);
1453        render(&r, src).unwrap_err().to_string()
1454    }
1455
1456    #[test]
1457    fn python_string_and_dict_methods_work() {
1458        assert_eq!(ok("{{ 'a,b,c'.split(',') | join('-') }}"), "a-b-c");
1459        assert_eq!(
1460            ok("{{ 'y' if 'abc'.startswith('a') }}{{ 'y' if 'abc'.endswith('x') }}"),
1461            "y"
1462        );
1463        assert_eq!(
1464            ok("{{ ' x '.strip() }}|{{ 'abc'.upper() }}|{{ 'a-b'.replace('-', '_') }}"),
1465            "x|ABC|a_b"
1466        );
1467        assert_eq!(
1468            ok("{{ var('m').keys() | list }} {{ var('m').items() | list }}"),
1469            r#"["a"] [["a", 1]]"#
1470        );
1471        assert_eq!(
1472            ok("{{ var('m').get('a') }} {{ var('m').get('z', 'none') }}"),
1473            "1 none"
1474        );
1475    }
1476
1477    #[test]
1478    fn list_appends_in_a_loop_and_joins() {
1479        assert_eq!(
1480            ok(
1481                "{% set sq = list() %}{% for i in range(1, 5) %}{% set _ = sq.append(i * i) %}{% endfor %}{{ sq | join('+') }}"
1482            ),
1483            "1+4+9+16"
1484        );
1485    }
1486
1487    #[test]
1488    fn list_methods_follow_python() {
1489        assert_eq!(
1490            ok("{% set l = list([3, 1, 2]) %}{% set _ = l.sort() %}{{ l }}"),
1491            "[1, 2, 3]"
1492        );
1493        assert_eq!(
1494            ok("{% set l = list([3, 1, 2]) %}{% set _ = l.sort(reverse=true) %}{{ l }}"),
1495            "[3, 2, 1]"
1496        );
1497        assert_eq!(
1498            ok("{% set l = list([1, 2]) %}{% set _ = l.extend([3, 4]) %}{{ l }} {{ l | length }}"),
1499            "[1, 2, 3, 4] 4"
1500        );
1501        assert_eq!(
1502            ok("{% set l = list([1, 2, 3]) %}{{ l.pop() }} {{ l.pop(0) }} {{ l }}"),
1503            "3 1 [2]"
1504        );
1505        assert_eq!(
1506            ok("{% set l = list([1, 2]) %}{% set _ = l.insert(0, 9) %}{% set _ = l.insert(99, 7) %}{{ l }}"),
1507            "[9, 1, 2, 7]"
1508        );
1509        assert_eq!(
1510            ok(
1511                "{% set l = list([1, 2, 1]) %}{% set _ = l.remove(1) %}{{ l }} {{ l.index(1) }} {{ l.count(1) }}"
1512            ),
1513            "[2, 1] 1 1"
1514        );
1515        assert_eq!(
1516            ok("{% set l = list([1, 2]) %}{% set _ = l.reverse() %}{{ l }}"),
1517            "[2, 1]"
1518        );
1519        assert_eq!(
1520            ok("{% set l = list([1, 2]) %}{% set _ = l.clear() %}{{ l }} {{ 'empty' if not l }}"),
1521            "[] empty"
1522        );
1523        assert_eq!(
1524            ok("{% set l = list([1, 2, 3]) %}{{ l[0] }} {{ l[-1] }} {{ l[1:] }}"),
1525            "1 3 [2, 3]"
1526        );
1527    }
1528
1529    #[test]
1530    fn lists_share_by_reference_and_copy_apart() {
1531        assert_eq!(
1532            ok("{% set a = list() %}{% set b = a %}{% set _ = b.append(1) %}{{ a }}"),
1533            "[1]"
1534        );
1535        assert_eq!(
1536            ok("{% set a = list([1]) %}{% set b = a.copy() %}{% set _ = b.append(2) %}{{ a }} {{ b }}"),
1537            "[1] [1, 2]"
1538        );
1539    }
1540
1541    #[test]
1542    fn a_list_changed_inside_its_own_loop_does_not_skip_or_repeat() {
1543        assert_eq!(
1544            ok(
1545                "{% set l = list([1, 2, 3]) %}{% for x in l %}{% set _ = l.append(x * 10) %}{{ x }} {% endfor %}{{ l }}"
1546            ),
1547            "1 2 3 [1, 2, 3, 10, 20, 30]"
1548        );
1549        assert_eq!(
1550            ok("{% set l = list([1, 2]) %}{% set _ = l.extend(l) %}{{ l }}"),
1551            "[1, 2, 1, 2]"
1552        );
1553    }
1554
1555    #[test]
1556    fn a_list_starts_from_a_var_without_changing_it() {
1557        assert_eq!(
1558            ok("{% set l = list(var('fixed')) %}{% set _ = l.append(4) %}{{ l }} {{ var('fixed') }}"),
1559            "[3, 1, 2, 4] [3, 1, 2]"
1560        );
1561    }
1562
1563    #[test]
1564    fn dict_methods_follow_python() {
1565        assert_eq!(
1566            ok("{% set d = dict(a=1) %}{% set _ = d.update(b=2) %}{% set _ = d.update({'c': 3}) %}{{ d }}"),
1567            r#"{"a": 1, "b": 2, "c": 3}"#
1568        );
1569        // Insertion order, as in Python. (Keyword arguments and `{}` literals reach us already sorted
1570        // until `preserve_order` is on, and the `| items` filter sorts; so add keys one by one.)
1571        let make = "{% set d = dict() %}{% set _ = d.setdefault('b', 1) %}{% set _ = d.setdefault('a', 2) %}";
1572        assert_eq!(
1573            ok(&(make.to_string() + "{% for k, v in d.items() %}{{ k }}={{ v }} {% endfor %}")),
1574            "b=1 a=2 "
1575        );
1576        assert_eq!(
1577            ok(&(make.to_string() + "{% for k in d %}{{ k }} {% endfor %}{{ d.values() | list }}")),
1578            "b a [1, 2]"
1579        );
1580        assert_eq!(
1581            ok("{% set d = dict(a=1) %}{{ d.setdefault('a', 9) }} {{ d.setdefault('b', 2) }} {{ d }}"),
1582            r#"1 2 {"a": 1, "b": 2}"#
1583        );
1584        assert_eq!(
1585            ok("{% set d = dict(a=1) %}{{ d.pop('a') }} {{ d.pop('a', 'gone') }} {{ d }}"),
1586            "1 gone {}"
1587        );
1588        assert_eq!(
1589            ok(
1590                "{% set d = dict(a=1) %}{{ d.get('a') }} {{ d['a'] }} {{ d.a }} {{ 'in' if 'a' in d }} {{ d.keys() | list }}"
1591            ),
1592            r#"1 1 1 in ["a"]"#
1593        );
1594        assert_eq!(
1595            ok("{% set d = dict(a=1) %}{% set e = d.copy() %}{% set _ = e.update(a=2) %}{{ d.a }} {{ e.a }}"),
1596            "1 2"
1597        );
1598        assert_eq!(
1599            ok("{% set d = dict(a=1) %}{% set _ = d.update(a=5, b=6) %}{{ d | length }} {{ d.a }}"),
1600            "2 5"
1601        );
1602        assert_eq!(
1603            ok("{% set d = dict(a=1) %}{% set _ = d.update(d) %}{{ d }}"),
1604            r#"{"a": 1}"#
1605        );
1606    }
1607
1608    #[test]
1609    fn mutable_values_print_and_serialise_like_plain_ones() {
1610        assert_eq!(
1611            ok("{{ list([1, 'a']) }} {{ dict(k='v') }}"),
1612            r#"[1, "a"] {"k": "v"}"#
1613        );
1614        assert_eq!(
1615            ok("{{ list(dict(k='v')) }} {{ list(list([2, 1])) | sort }}"),
1616            r#"["k"] [1, 2]"#
1617        );
1618        assert_eq!(ok("{% set l = list() %}{{ 'yes' if l else 'no' }}"), "no");
1619    }
1620
1621    #[test]
1622    fn mutating_an_immutable_value_says_what_to_write() {
1623        let e = err("{% set l = [] %}{% set _ = l.append(1) %}");
1624        assert!(e.contains("append()") && e.contains("list()"), "{e}");
1625        let e = err("{% set _ = var('fixed').append(1) %}");
1626        assert!(e.contains("list()"), "{e}");
1627        let e = err("{% set d = {} %}{% set _ = d.update(a=1) %}");
1628        assert!(e.contains("dict()"), "{e}");
1629    }
1630
1631    #[test]
1632    fn bad_arguments_to_mutable_methods_are_clear_errors() {
1633        assert!(err("{% set l = list() %}{% set _ = l.append() %}").contains("append()"));
1634        assert!(err("{% set l = list() %}{% set _ = l.pop() %}").contains("pop()"));
1635        assert!(err("{% set l = list([1]) %}{% set _ = l.remove(2) %}").contains("not in the list"));
1636        assert!(err("{% set d = dict() %}{% set _ = d.pop('x') %}").contains("no key"));
1637        assert!(err("{% set d = dict('nope') %}").contains("mapping"));
1638    }
1639
1640    #[test]
1641    fn run_context_and_date_formats() {
1642        let (_d, r) = renderer(Json::Null, &[], &[]);
1643        let out = render(
1644            &r,
1645            "{{ run.report }}/{{ run.set }}/{{ run.target }}/{{ target.name }}/{{ target }} {{ run.date }} {{ run.date.yyyymmdd }} \
1646             {{ run.date.ddmmyyyy }} {{ run.date.yyyy }}-{{ run.date.mm }}-{{ run.date.dd }} {{ run.date_format('%Y-W%V') }}",
1647        )
1648        .unwrap();
1649        assert_eq!(
1650            out,
1651            "monthly/client_a/prod/prod/prod 2026-01-25 20260125 25012026 2026-01-25 2026-W04"
1652        );
1653    }
1654
1655    #[test]
1656    fn var_precedence_is_cli_then_binding_then_default() {
1657        let (_d, r) = renderer(
1658            serde_json::json!({"a": "binding", "b": "binding", "n": 5}),
1659            &[("a", "cli")],
1660            &[],
1661        );
1662        assert_eq!(
1663            render(
1664                &r,
1665                "{{ var('a') }} {{ var('b') }} {{ var('c', 'dflt') }} {{ var('n') + 1 }}"
1666            )
1667            .unwrap(),
1668            "cli binding dflt 6"
1669        );
1670        let e = render(&r, "select\n{{ var('missing') }}").unwrap_err();
1671        assert_eq!(
1672            (e.line, e.message.contains("`var('missing')` has no value")),
1673            (Some(2), true),
1674            "{e}"
1675        );
1676    }
1677
1678    #[test]
1679    fn env_var_reads_the_environment_or_errors() {
1680        let (_d, r) = renderer(Json::Null, &[], &[]);
1681        assert_eq!(
1682            render(&r, "{{ env_var('DRE_TEST_UNSET_VAR', 'x') }}").unwrap(),
1683            "x"
1684        );
1685        let home = std::env::var("PATH").unwrap();
1686        assert_eq!(render(&r, "{{ env_var('PATH') }}").unwrap(), home);
1687        assert!(
1688            render(&r, "{{ env_var('DRE_TEST_UNSET_VAR') }}")
1689                .unwrap_err()
1690                .message
1691                .contains("is not set")
1692        );
1693    }
1694
1695    #[test]
1696    fn macros_from_every_file_are_callable_and_keep_line_numbers() {
1697        let (_d, r) = renderer(
1698            Json::Null,
1699            &[],
1700            &[
1701                ("a.sql", "{% macro double(x) %}{{ x * 2 }}{% endmacro %}\n"),
1702                (
1703                    "b.sql",
1704                    "{% macro broken() %}\n{{ var('nope') }}\n{% endmacro %}\n{% macro quad(x) %}{{ x * 4 }}{% endmacro %}\n",
1705                ),
1706            ],
1707        );
1708        assert_eq!(
1709            render(&r, "select {{ double(2) }}, {{ quad(1) }}\n").unwrap(),
1710            "select 4, 4\n"
1711        );
1712        let e = render(&r, "line one\nselect {{ broken() }}").unwrap_err();
1713        assert_eq!(e.file, PathBuf::from("macros/b.sql"), "{e}");
1714        assert_eq!(e.line, Some(2), "{e}");
1715        let e = render(&r, "one\ntwo\n{{ nope }}").unwrap_err();
1716        assert_eq!(
1717            (e.file.clone(), e.line),
1718            (PathBuf::from("reports/q.sql"), Some(3)),
1719            "{e}"
1720        );
1721    }
1722
1723    struct Fake;
1724    impl QueryRunner for Fake {
1725        fn run_query(&self, sql: &str, max_rows: u64, _profile: Option<&str>) -> Result<QueryRows, String> {
1726            if max_rows < 2 {
1727                return Err(format!("`{sql}` returned more than {max_rows} rows"));
1728            }
1729            Ok(QueryRows {
1730                columns: vec!["region".into(), "n".into()],
1731                rows: vec![
1732                    vec![Value::from("apac"), Value::from(1)],
1733                    vec![Value::from("emea"), Value::from(2)],
1734                ],
1735            })
1736        }
1737        fn columns(&self, _: &str, _: Option<&str>) -> Result<Vec<Column>, String> {
1738            Ok(Vec::new())
1739        }
1740    }
1741
1742    #[test]
1743    fn run_query_rows_are_accessible_by_name_and_index() {
1744        let dir = tempfile::tempdir().unwrap();
1745        let r = Renderer::new(RendererConfig {
1746            root: dir.path(),
1747            macros: &[],
1748            context: RunContext {
1749                report: "r".into(),
1750                set: None,
1751                target: "t".into(),
1752                schedule: None,
1753                date: NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
1754                now: Utc::now(),
1755                scheduled_at: None,
1756                calendar: Calendar::default(),
1757            },
1758            vars: JsonMap::new(),
1759            cli_vars: BTreeMap::new(),
1760            runner: Some(Arc::new(Fake)),
1761            connections: None,
1762            mode: Mode::Run,
1763            connection: None,
1764            source_type: "duckdb".into(),
1765            sources: None,
1766            run_query_max_rows: 10_000,
1767            sql: BTreeMap::new(),
1768            lookups: BTreeMap::new(),
1769            lookup_inline_max_rows: 200,
1770            packages: Vec::new(),
1771            project_name: "acme".into(),
1772            dispatch: DispatchOrder::new(),
1773        })
1774        .unwrap();
1775        let src = "{% set res = run_query('select') %}{{ res.columns | join(',') }}|\
1776                   {% for row in res.rows %}{{ row.region }}={{ row[1] }}{{ ',' if not loop.last }}{% endfor %}|{{ res | length }}|{{ res[1]['region'] }}";
1777        assert_eq!(render(&r, src).unwrap(), "region,n|apac=1,emea=2|2|emea");
1778        let e = render(&r, "{{ run_query('select', max_rows=1) }}").unwrap_err();
1779        assert!(e.message.contains("more than 1 rows"), "{e}");
1780        assert!(render(&r, "{% if run.set is none %}none{% endif %}").unwrap() == "none");
1781    }
1782
1783    #[test]
1784    fn removed_names_say_what_to_write_instead() {
1785        let e = err("{{ run.profile }}");
1786        assert!(
1787            e.contains("`run.profile` was removed") && e.contains("connection.name"),
1788            "{e}"
1789        );
1790        let e = err("{{ target.schema }}");
1791        assert!(
1792            e.contains("`target.schema` was removed") && e.contains("connection.schema"),
1793            "{e}"
1794        );
1795    }
1796
1797    #[test]
1798    fn a_source_relation_quotes_the_parts_asked_for() {
1799        let r = ResolvedSource {
1800            source: "sales".into(),
1801            table: "orders".into(),
1802            profile: None,
1803            database: None,
1804            schema: "Sales".into(),
1805            identifier: "Or\"ders".into(),
1806            quoting: Quoting {
1807                database: true,
1808                schema: false,
1809                identifier: true,
1810            },
1811        };
1812        assert_eq!(r.relation(Some("\"")), "Sales.\"Or\"\"ders\"");
1813        assert_eq!(r.relation(None), "Sales.Or\"ders");
1814        let r = ResolvedSource {
1815            database: Some("main".into()),
1816            ..r
1817        };
1818        assert_eq!(r.relation(Some("`")), "`main`.Sales.`Or\"ders`");
1819    }
1820
1821    #[test]
1822    fn a_relation_is_mentioned_only_as_a_whole_name() {
1823        assert!(mentions("select * from sales.orders where 1", "sales.orders"));
1824        assert!(!mentions("select * from sales.orders_old", "sales.orders"));
1825        assert!(!mentions("select * from x.sales.orders", "sales.orders"));
1826    }
1827}