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