1use std::collections::BTreeMap;
8use std::fs::File;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13
14use arrow::array::RecordBatch;
15use arrow::datatypes::SchemaRef;
16use arrow::ipc::reader::FileReader;
17use arrow::ipc::writer::FileWriter;
18use chrono::NaiveDate;
19use dre_protocol::host::{Execution, HostError, LogSink, PluginProcess};
20use dre_protocol::msg::{ColumnOptions, DeliveryFile, ResultSetMeta};
21use dre_protocol::{CAP_LOAD, CAP_MESSAGE, CAP_MESSAGE_ONLY, CAP_MULTI_FILE, CAP_READ_ONLY, CAP_SESSIONS};
22use serde::Serialize;
23use serde_json::{Map as JsonMap, Value as Json, json};
24
25use crate::codes::{Code, ErrorCode};
26use crate::dates::Calendar;
27use crate::engine::{CancelToken, Events, RunEvent, RunStore};
28use crate::lookups::Table;
29use crate::message::QueryResult;
30use crate::parse::ParsedBinding;
31use crate::profiles::{Entry, LOCAL_TYPE, ProfileTarget, Profiles, Role};
32use crate::project::{Binding, PluginKind, Project, QueryEntry, Report};
33use crate::render::{
34 Column, Connection, Connections, Mode, QueryRows, QueryRunner, RenderError, Renderer, RendererConfig,
35};
36use crate::run_results::{self, Delivery, RunResults};
37use crate::selector;
38use crate::sqlsplit::{self, StatementKind};
39
40pub(crate) mod per_row;
41
42#[derive(Debug, Clone, Default)]
44pub struct RunOptions {
45 pub selector: Option<String>,
46 pub set: Option<String>,
48 pub target: Option<String>,
50 pub profile: Option<String>,
52 pub vars: BTreeMap<String, String>,
53 pub output_name: Option<String>,
54 pub output_path: Option<String>,
55 pub dry_run: bool,
56 pub no_deliver: bool,
57 pub output_json: Option<PathBuf>,
59 pub from_results: Option<PathBuf>,
62 pub row_concurrency: Option<usize>,
64 pub hold: bool,
66 pub retry_failed: Option<String>,
69 pub only_keys: Option<Vec<String>>,
71 pub limit_rows: Option<usize>,
73 pub unmask: bool,
75 pub rate: Option<crate::rate::Rate>,
77 pub preview: Option<u64>,
79 pub accept_schema_change: bool,
80 pub interactive: bool,
82 pub date: Option<NaiveDate>,
84 pub scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
87 pub live_check: bool,
89 pub schedule: Option<String>,
91 pub timezone: Option<String>,
93 pub manifest_checksum: Option<String>,
95 pub cancel: CancelToken,
97 pub keep_runs: Option<usize>,
99 pub threads: Option<usize>,
102}
103
104impl RunOptions {
105 pub fn params(&self, date: NaiveDate) -> run_results::Params {
107 run_results::Params {
108 selector: self.selector.clone(),
109 set: self.set.clone(),
110 schedule: self.schedule.clone(),
111 target: self.target.clone(),
112 profile: self.profile.clone(),
113 vars: self.vars.clone(),
114 run_date: date.to_string(),
115 scheduled_at: self.scheduled_at.map(rfc3339),
116 timezone: self.timezone.clone(),
117 output_name: self.output_name.clone(),
118 output_path: self.output_path.clone(),
119 dry_run: self.dry_run,
120 preview: self.preview,
121 accept_schema_change: self.accept_schema_change,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
128pub enum Level {
129 Debug,
130 Info,
131}
132
133pub trait Ui {
136 fn targets(&mut self, _targets: &RunTargets) {}
138 fn plan(&mut self, _bindings: usize) {}
140 fn binding_start(&mut self, _report: &str, _set: Option<&str>) {}
141 fn binding_log(&mut self, _path: &Path) {}
143 fn binding_vars(
145 &mut self,
146 _schedule: Option<&str>,
147 _schedule_vars: Option<&JsonMap<String, Json>>,
148 _vars: &JsonMap<String, Json>,
149 ) {
150 }
151 fn step(&mut self, level: Level, verb: &str, detail: &str, elapsed: Option<Duration>);
153 fn warn(&mut self, msg: &str);
154 fn binding_end(&mut self, _outcome: &BindingOutcome) {}
155 fn compiled(&mut self, _plan: &BindingPlan) {}
157 fn message(&mut self, _message: &ShownMessage) {}
159 fn choose_set(&mut self, report: &str, sets: &[String]) -> Result<Option<String>, String>;
161 fn running(&mut self, _label: &str, _running: bool) {}
164 fn rows(&mut self, _progress: &RowProgress) {}
166 fn plugin_log(&self) -> LogSink;
168 fn sql_log(&self) -> LogSink {
170 Arc::new(|_, _| {})
171 }
172}
173
174#[derive(Debug, Clone, Serialize)]
177pub struct RowProgress {
178 pub output: String,
180 pub done: u64,
181 pub failed: u64,
182 pub total: u64,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub rate: Option<crate::rate::Rate>,
186 pub finished: bool,
188}
189
190#[derive(Debug, Clone, Serialize)]
192pub struct ShownMessage {
193 pub output: Option<String>,
195 pub title: String,
196 pub text: String,
198 pub notes: Vec<String>,
200}
201
202pub use crate::run_results::Status;
203
204#[derive(Debug, Clone, Serialize)]
205pub struct BindingOutcome {
206 pub report: String,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub set: Option<String>,
209 pub binding: String,
210 pub status: Status,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub error: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub error_code: Option<ErrorCode>,
216 pub files: Vec<PathBuf>,
217 pub summary: String,
219 pub schedule: Option<String>,
221 pub schedule_vars: Option<JsonMap<String, Json>>,
222 pub vars: JsonMap<String, Json>,
224 #[serde(skip_serializing_if = "String::is_empty")]
226 pub timezone: String,
227 #[serde(skip)]
228 pub elapsed: Duration,
229}
230
231#[derive(Debug, Clone, Serialize)]
233pub struct BindingPlan {
234 pub report: String,
235 pub set: Option<String>,
236 pub compiled: Vec<PathBuf>,
238 pub profile: Option<String>,
240 pub queries: Vec<PlannedQuery>,
242 pub target: String,
243 pub format: String,
244 pub output: PathBuf,
247 pub destinations: Vec<PlannedDestination>,
248 pub outputs: Vec<PlannedOutput>,
250 pub schedules: Vec<String>,
251}
252
253#[derive(Debug, Clone, Serialize)]
254pub struct PlannedOutput {
255 pub name: Option<String>,
256 pub format: String,
257 pub output: PathBuf,
259 pub destinations: Vec<PlannedDestination>,
260}
261
262#[derive(Debug, Clone, Serialize)]
263pub struct PlannedQuery {
264 pub query: String,
265 pub connection: String,
266 #[serde(rename = "type")]
267 pub kind: String,
268 pub target: String,
270 #[serde(skip_serializing_if = "Vec::is_empty")]
271 pub sources: Vec<String>,
272}
273
274#[derive(Debug, Clone, Serialize)]
275pub struct PlannedDestination {
276 pub profile: String,
277 pub kind: Option<String>,
279 pub target: Option<String>,
281 pub path: Option<String>,
282 pub delivers: bool,
284}
285
286#[derive(Debug, Default)]
287pub struct RunSummary {
288 pub outcomes: Vec<BindingOutcome>,
289 pub error: Option<String>,
291 pub missing_entry: bool,
293 pub cancelled: Option<crate::engine::CancelReason>,
295 pub not_run: usize,
297}
298
299impl RunSummary {
300 pub fn failed(&self) -> bool {
301 self.error.is_some()
302 || self.cancelled.is_some()
303 || self
304 .outcomes
305 .iter()
306 .any(|o| matches!(o.status, Status::Error | Status::Cancelled | Status::TimedOut))
307 }
308
309 pub fn error_code(&self) -> Option<ErrorCode> {
311 if self.error.is_some() {
312 return Some(ErrorCode::Core(if self.missing_entry {
313 Code::MissingTargetEntry
314 } else {
315 Code::InvalidSelector
316 }));
317 }
318 self.outcomes
319 .iter()
320 .find(|o| matches!(o.status, Status::Error | Status::Cancelled | Status::TimedOut))
321 .map(|o| o.error_code.clone().unwrap_or(ErrorCode::Core(Code::RunFailed)))
322 .or_else(|| {
323 self.cancelled.map(|r| {
324 ErrorCode::Core(match r {
325 crate::engine::CancelReason::Timeout => Code::RunTimedOut,
326 _ => Code::RunCancelled,
327 })
328 })
329 })
330 }
331
332 pub fn exit_code(&self) -> u8 {
336 if let Some(r) = self.cancelled {
337 return r.exit_code();
338 }
339 if self.error.is_some() {
340 return self.error_code().map_or(2, |c| c.kind().exit_code());
342 }
343 let failed: Vec<&BindingOutcome> = self
344 .outcomes
345 .iter()
346 .filter(|o| matches!(o.status, Status::Error | Status::Cancelled | Status::TimedOut))
347 .collect();
348 if !failed.is_empty()
350 && failed.iter().all(|o| {
351 o.error_code
352 .as_ref()
353 .is_some_and(|c| c.kind() == crate::codes::Kind::Refused)
354 })
355 && self.outcomes.len() == failed.len()
356 {
357 return 2;
358 }
359 if failed.is_empty() { 0 } else { 1 }
361 }
362}
363
364pub struct Plan<'a> {
366 pub bindings: Vec<(&'a Report, Binding)>,
367 pub targets: RunTargets,
368}
369
370pub fn run(project: &Project, opts: &RunOptions, ui: &mut dyn Ui) -> RunSummary {
373 let mut summary = RunSummary::default();
374 let Some(plan) = plan(project, opts, ui, &mut summary) else {
375 return summary;
376 };
377 ui.targets(&plan.targets);
378 ui.plan(plan.bindings.len());
379 let (plugin_log, sql_log) = (ui.plugin_log(), ui.sql_log());
380 let (tx, rx) = std::sync::mpsc::channel();
381 let cancel = opts.cancel.clone();
382 let outcomes = std::thread::scope(|s| {
383 let worker = s.spawn(|| execute(project, &plan, opts, &cancel, Events::new(tx)));
384 for e in rx {
385 crate::engine::dispatch(ui, e, &plugin_log, &sql_log);
386 }
387 worker.join().expect("the run's worker doesn't panic")
388 });
389 summary.not_run = plan.bindings.len().saturating_sub(outcomes.len());
390 summary.cancelled = cancel.reason();
391 summary.outcomes.extend(outcomes);
392 summary
393}
394
395pub fn plan<'a>(
400 project: &'a Project,
401 opts: &RunOptions,
402 ui: &mut dyn Ui,
403 summary: &mut RunSummary,
404) -> Option<Plan<'a>> {
405 if let Err(e) = crate::target::ensure(&project.target_dir) {
406 summary.error = Some(format!(
407 "can't write to the target path {} (from {}): {e}",
408 project.target_dir.display(),
409 project.target_source
410 ));
411 return None;
412 }
413 let planned: Vec<(&Report, Binding)> = match &opts.schedule {
414 Some(name) => {
415 if !project.schedules.iter().any(|e| &e.name == name) {
416 summary.error = Some(unknown_schedule(project, name));
417 return None;
418 }
419 match schedule_bindings(project, name, opts) {
420 Ok(p) => p,
421 Err(e) => {
422 summary.error = Some(e);
423 return None;
424 }
425 }
426 }
427 None => {
428 let reports: Vec<&Report> = match &opts.selector {
429 None => project.reports.iter().collect(),
430 Some(s) => match selector::resolve(project, s) {
431 Ok(r) if r.is_empty() => {
432 summary.error = Some(format!("selector `{s}` matches no report"));
433 return None;
434 }
435 Ok(r) => r,
436 Err(e) => {
437 summary.error = Some(e.to_string());
438 return None;
439 }
440 },
441 };
442 let mut planned = Vec::new();
445 for report in reports {
446 match choose_bindings(project, report, opts, ui) {
447 Ok(bs) => planned.extend(bs.into_iter().map(|b| (report, b))),
448 Err(e) => {
449 let outcome = BindingOutcome {
450 report: report.name.clone(),
451 set: None,
452 binding: "-".into(),
453 status: Status::Error,
454 error: Some(e),
455 error_code: Some(Code::InvalidSelector.into()),
456 files: Vec::new(),
457 summary: String::new(),
458 schedule: opts.schedule.clone(),
459 schedule_vars: None,
460 vars: JsonMap::new(),
461 timezone: String::new(),
462 elapsed: Duration::ZERO,
463 };
464 ui.binding_end(&outcome);
465 summary.outcomes.push(outcome);
466 }
467 }
468 }
469 planned
470 }
471 };
472 match run_targets(project, &planned, opts) {
474 Ok(targets) => Some(Plan {
475 bindings: planned,
476 targets,
477 }),
478 Err(e) => {
479 summary.error = Some(e);
480 summary.missing_entry = true;
481 None
482 }
483 }
484}
485
486pub fn execute(
490 project: &Project,
491 plan: &Plan<'_>,
492 opts: &RunOptions,
493 cancel: &CancelToken,
494 events: Events,
495) -> Vec<BindingOutcome> {
496 let limits = Limits::new(project, plan, opts);
497 let target = opts
498 .output_json
499 .as_ref()
500 .map(|p| project.root.join(p))
501 .unwrap_or_else(|| project.target_dir.clone());
502 if limits.sequential() {
503 let store = RunStore::new(&target);
504 let mut outcomes = Vec::new();
505 for (report, b) in &plan.bindings {
506 if cancel.is_cancelled() {
507 break;
508 }
509 events.emit(RunEvent::BindingStart {
510 report: report.name.clone(),
511 set: b.set.clone(),
512 });
513 let mut r = BindingRun::new(project, report, b, opts, events.clone(), &store);
514 let outcome = r.run();
515 events.emit(RunEvent::BindingEnd(outcome.clone()));
516 outcomes.push(outcome);
517 }
518 return outcomes;
519 }
520 let store = RunStore::concurrent(&target);
521 let n = plan.bindings.len();
522 let state = std::sync::Mutex::new(Running::new(n));
523 let wake = std::sync::Condvar::new();
524 let outcomes: std::sync::Mutex<Vec<Option<BindingOutcome>>> = std::sync::Mutex::new(vec![None; n]);
525 std::thread::scope(|s| {
526 for _ in 0..limits.cap.min(n) {
527 s.spawn(|| {
528 loop {
529 let i = {
530 let mut st = state.lock().unwrap();
531 loop {
532 if cancel.is_cancelled() || st.started.iter().all(|x| *x) {
533 break None;
534 }
535 match limits.next(&st) {
536 Some(i) => {
537 st.start(i, &limits.uses[i]);
538 break Some(i);
539 }
540 None => st = wake.wait(st).unwrap(),
541 }
542 }
543 };
544 let Some(i) = i else {
545 wake.notify_all();
546 return;
547 };
548 let (report, b) = &plan.bindings[i];
549 let label = match &b.set {
550 Some(set) => format!("{} [{set}]", report.name),
551 None => report.name.clone(),
552 };
553 let held = events.held();
554 held.emit_now(RunEvent::Running {
555 label: label.clone(),
556 running: true,
557 });
558 held.emit(RunEvent::BindingStart {
559 report: report.name.clone(),
560 set: b.set.clone(),
561 });
562 let mut r = BindingRun::new(project, report, b, opts, held.clone(), &store);
563 let outcome = r.run();
564 held.emit(RunEvent::BindingEnd(outcome.clone()));
565 held.emit_now(RunEvent::Running {
566 label,
567 running: false,
568 });
569 held.flush();
570 outcomes.lock().unwrap()[i] = Some(outcome);
571 state.lock().unwrap().finish(&limits.uses[i]);
572 wake.notify_all();
573 }
574 });
575 }
576 });
577 outcomes.into_inner().unwrap().into_iter().flatten().collect()
579}
580
581struct Limits {
585 uses: Vec<Vec<String>>,
587 per_connection: BTreeMap<String, usize>,
588 cap: usize,
589}
590
591struct Running {
593 started: Vec<bool>,
594 total: usize,
595 on: BTreeMap<String, usize>,
596 warm: std::collections::BTreeSet<String>,
599}
600
601impl Running {
602 fn new(n: usize) -> Running {
603 Running {
604 started: vec![false; n],
605 total: 0,
606 on: BTreeMap::new(),
607 warm: Default::default(),
608 }
609 }
610
611 fn start(&mut self, i: usize, uses: &[String]) {
612 self.started[i] = true;
613 self.total += 1;
614 for c in uses {
615 *self.on.entry(c.clone()).or_default() += 1;
616 }
617 }
618
619 fn finish(&mut self, uses: &[String]) {
620 self.total -= 1;
621 for c in uses {
622 *self.on.entry(c.clone()).or_default() -= 1;
623 self.warm.insert(c.clone());
624 }
625 }
626}
627
628impl Limits {
629 fn new(project: &Project, plan: &Plan<'_>, opts: &RunOptions) -> Limits {
630 let schedule_vars = opts
631 .schedule
632 .as_ref()
633 .and_then(|n| project.schedules.iter().find(|e| &e.name == n))
634 .map(|e| e.vars.clone());
635 let inputs = crate::parse::Inputs {
636 target: project.target_name.clone(),
637 cli_vars: opts.vars.clone(),
638 date: opts.date,
639 scheduled_at: opts.scheduled_at,
640 timezone: opts.timezone.clone(),
641 schedule: opts.schedule.clone(),
642 started_at: None,
643 };
644 let uses: Vec<Vec<String>> = plan
645 .bindings
646 .iter()
647 .map(|(report, b)| {
648 let mut vars = b.vars.clone();
649 vars.extend(schedule_vars.clone().unwrap_or_default());
650 crate::parse::binding(project, report, b, &vars, &inputs)
651 .connections()
652 .into_iter()
653 .map(str::to_string)
654 .collect()
655 })
656 .collect();
657 let mut per_connection = BTreeMap::new();
658 for c in uses.iter().flatten() {
659 per_connection
660 .entry(c.clone())
661 .or_insert_with(|| connection_threads(project, c));
662 }
663 let cap = if opts.dry_run || opts.live_check {
665 1
666 } else {
667 opts.threads.unwrap_or(usize::MAX).max(1)
668 };
669 Limits {
670 uses,
671 per_connection,
672 cap,
673 }
674 }
675
676 fn sequential(&self) -> bool {
678 self.cap == 1 || (self.cap == usize::MAX && self.per_connection.values().all(|n| *n <= 1))
679 }
680
681 fn next(&self, st: &Running) -> Option<usize> {
683 if st.total >= self.cap {
684 return None;
685 }
686 (0..st.started.len()).find(|&i| {
687 !st.started[i]
688 && self.uses[i].iter().all(|c| {
689 let on = st.on.get(c).copied().unwrap_or(0);
690 let limit = if st.warm.contains(c) {
691 self.per_connection[c]
692 } else {
693 1
694 };
695 on < limit
696 })
697 })
698 }
699}
700
701fn connection_threads(project: &Project, connection: &str) -> usize {
703 let Some(t) = project.profiles.target(Role::Connection, connection) else {
704 return 1;
705 };
706 let n = match t.fields.get("threads") {
707 Some(Json::Number(n)) => n.as_u64().unwrap_or(1),
708 Some(Json::String(s)) => s.trim().parse().unwrap_or(1),
709 _ => 1,
710 } as usize;
711 let duckdb_file = t.kind == "duckdb"
712 && t.fields
713 .get("path")
714 .and_then(Json::as_str)
715 .is_some_and(|p| p != ":memory:");
716 if duckdb_file { 1 } else { n.max(1) }
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
721pub struct UsedProfile {
722 pub role: &'static str,
724 pub profile: String,
725 pub target: String,
727 pub deliver: bool,
729}
730
731#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
733pub struct RunTargets {
734 pub name: String,
736 pub from: String,
738 pub profiles: Vec<UsedProfile>,
739}
740
741impl RunTargets {
742 pub fn line(&self) -> String {
744 let mut out = format!("{} ({})", self.name, self.from);
745 for p in self.profiles.iter().filter(|p| p.target != self.name) {
746 out.push_str(&format!("; {} `{}`: {}", p.role, p.profile, p.target));
747 }
748 out
749 }
750
751 pub fn mismatch(&self) -> Option<String> {
754 let first = &self.profiles.first()?.target;
755 if first == &self.name || self.profiles.iter().any(|p| &p.target != first) {
756 return None;
757 }
758 Some(format!(
759 "every profile is on `{first}` but the run's target is `{}`: pass `--target {first}` or set `{}`",
760 self.name,
761 crate::profiles::TARGET_ENV
762 ))
763 }
764}
765
766pub fn run_targets(
770 project: &Project,
771 planned: &[(&Report, Binding)],
772 opts: &RunOptions,
773) -> Result<RunTargets, String> {
774 let profiles = &project.profiles;
775 let schedule_vars = opts
776 .schedule
777 .as_ref()
778 .and_then(|n| project.schedules.iter().find(|e| &e.name == n))
779 .map(|e| e.vars.clone());
780 let inputs = crate::parse::Inputs {
781 target: project.target_name.clone(),
782 cli_vars: opts.vars.clone(),
783 date: opts.date,
784 scheduled_at: opts.scheduled_at,
785 timezone: opts.timezone.clone(),
786 schedule: opts.schedule.clone(),
787 started_at: None,
788 };
789 let mut used: Vec<(Role, String)> = Vec::new();
790 for (report, b) in planned {
791 let mut vars = b.vars.clone();
792 vars.extend(schedule_vars.clone().unwrap_or_default());
793 let parsed = crate::parse::binding(project, report, b, &vars, &inputs);
795 let names = parsed
796 .connections()
797 .into_iter()
798 .map(|c| (Role::Connection, c.to_string()))
799 .chain(
800 parsed
801 .destinations
802 .iter()
803 .flatten()
804 .filter(|_| !opts.no_deliver)
805 .filter(|d| !profiles.is_builtin_local(d))
806 .map(|d| (Role::Destination, d.clone())),
807 );
808 for u in names {
809 if !used.contains(&u) {
810 used.push(u);
811 }
812 }
813 }
814 let mut missing = Vec::new();
815 let mut out = Vec::new();
816 for (role, name) in &used {
817 let deliver = match profiles.entry(*role, name) {
818 Entry::Use(_) => true,
819 Entry::Nowhere => false,
820 Entry::Missing => {
821 missing.push(profiles.missing_entry(*role, name));
822 continue;
823 }
824 Entry::Unknown => continue,
826 };
827 out.push(UsedProfile {
828 role: role.as_str(),
829 profile: name.clone(),
830 target: profiles.target_of(*role, name),
831 deliver,
832 });
833 }
834 match missing.len() {
835 0 => Ok(RunTargets {
836 name: project.target_name.clone(),
837 from: project.target_from.to_string(),
838 profiles: out,
839 }),
840 1 => Err(format!("{}; nothing was run", missing[0])),
841 n => Err(format!(
842 "{n} profiles this run uses have no entry for it; nothing was run:\n {}",
843 missing.join("\n ")
844 )),
845 }
846}
847
848fn schedule_bindings<'a>(
851 project: &'a Project,
852 name: &str,
853 opts: &RunOptions,
854) -> Result<Vec<(&'a Report, Binding)>, String> {
855 let reports: Option<Vec<&str>> = match &opts.selector {
856 None => None,
857 Some(s) => Some(
858 selector::resolve(project, s)
859 .map_err(|e| e.to_string())?
860 .into_iter()
861 .map(|r| r.name.as_str())
862 .collect(),
863 ),
864 };
865 let set = opts.set.as_deref().filter(|s| *s != "all");
866 let all: Vec<(&Report, &Binding)> = project
867 .reports
868 .iter()
869 .flat_map(|r| {
870 r.bindings
871 .iter()
872 .filter(|b| b.schedules.iter().any(|s| s == name))
873 .map(move |b| (r, b))
874 })
875 .collect();
876 let picked: Vec<(&Report, Binding)> = all
877 .iter()
878 .filter(|(r, _)| reports.as_ref().is_none_or(|rs| rs.contains(&r.name.as_str())))
879 .filter(|(_, b)| set.is_none() || b.set.as_deref() == set)
880 .map(|(r, b)| {
881 let mut b = (*b).clone();
882 if let Some(p) = &opts.profile {
883 b.profile = Some(p.clone());
884 b.profile_at = Some(profile_flag_at());
885 }
886 (*r, b)
887 })
888 .collect();
889 if picked.is_empty() && (reports.is_some() || set.is_some()) {
890 let mut asked = Vec::new();
891 if let Some(s) = &opts.selector {
892 asked.push(format!("`{s}`"));
893 }
894 if let Some(s) = set {
895 asked.push(format!("Set `{s}`"));
896 }
897 let runs: Vec<String> = all
898 .iter()
899 .map(|(r, b)| match &b.set {
900 Some(s) => format!("{}/{s}", r.name),
901 None => r.name.clone(),
902 })
903 .collect();
904 return Err(format!(
905 "schedule `{name}` doesn't run {}; it runs: {}",
906 asked.join(" with "),
907 if runs.is_empty() {
908 "nothing".to_string()
909 } else {
910 runs.join(", ")
911 }
912 ));
913 }
914 Ok(picked)
915}
916
917pub fn unknown_schedule(project: &Project, name: &str) -> String {
919 let names: Vec<&str> = project.schedules.iter().map(|e| e.name.as_str()).collect();
920 if names.is_empty() {
921 format!("no schedule `{name}`: this project's schedules.yml declares none")
922 } else {
923 format!("no schedule `{name}`; valid names: {}", names.join(", "))
924 }
925}
926
927pub fn choose_bindings(
929 project: &Project,
930 report: &Report,
931 opts: &RunOptions,
932 ui: &mut dyn Ui,
933) -> Result<Vec<Binding>, String> {
934 let mut out: Vec<Binding> = match opts.set.as_deref() {
935 Some("all") => report.bindings.clone(),
936 Some(name) => match report.binding(name) {
937 Some(b) => vec![b.clone()],
938 None => vec![ad_hoc(project, report, name)],
939 },
940 None if !report.has_sets => report.bindings.clone(),
941 None => {
942 let names: Vec<String> = report.bindings.iter().filter_map(|b| b.set.clone()).collect();
943 let pick = report
944 .default_set
945 .clone()
946 .or_else(|| project.default_set.clone().filter(|d| names.contains(d)))
947 .or_else(|| (names.len() == 1).then(|| names[0].clone()));
948 match pick {
949 Some(p) => report.binding(&p).cloned().into_iter().collect(),
950 None if opts.interactive => match ui.choose_set(&report.name, &names)? {
951 Some(p) => report.binding(&p).cloned().into_iter().collect(),
952 None => report.bindings.clone(),
953 },
954 None => {
955 return Err(format!(
956 "report `{}` has several Sets ({}) and no `default_set`; declare `default_set:` or pass `--set <name>` or `--set all`",
957 report.name,
958 names.join(", ")
959 ));
960 }
961 }
962 }
963 };
964 if let Some(p) = &opts.profile {
965 for b in &mut out {
966 b.profile = Some(p.clone());
967 b.profile_at = Some(profile_flag_at());
968 }
969 }
970 Ok(out)
971}
972
973fn ad_hoc(project: &Project, report: &Report, name: &str) -> Binding {
976 let mut b = report.base.clone();
977 b.set = Some(name.to_string());
978 if let Some(reg) = project.sets.get(name) {
979 if let Some(p) = ®.profile {
980 b.profile = Some(p.clone());
981 b.profile_at = Some(crate::project::ProfileAt {
982 file: reg.file.clone(),
983 line: reg.line,
984 key: format!("`profile` of Set `{name}`"),
985 });
986 }
987 b.vars.extend(reg.vars.clone());
988 }
989 b
990}
991
992fn profile_flag_at() -> crate::project::ProfileAt {
994 crate::project::ProfileAt {
995 file: PathBuf::from("--profile"),
996 line: None,
997 key: "`--profile`".into(),
998 }
999}
1000
1001struct Produced {
1003 query: String,
1004 connection: String,
1005 schema: SchemaRef,
1006 rows: u64,
1007 spool: PathBuf,
1008 name: String,
1009 anchor: Option<String>,
1010 header: Option<bool>,
1011 columns: BTreeMap<String, ColumnOptions>,
1012 autofit: Option<bool>,
1013 style: Option<dre_protocol::style::SheetStyle>,
1014}
1015
1016struct Statement {
1017 query: String,
1018 connection: String,
1020 file: PathBuf,
1021 line: usize,
1022 text: String,
1023 kind: StatementKind,
1024 sensitive: bool,
1027 tab: bool,
1030}
1031
1032struct BindingRun<'a> {
1033 project: &'a Project,
1034 report: &'a Report,
1035 b: &'a Binding,
1036 vars: JsonMap<String, Json>,
1038 rendered_vars: JsonMap<String, Json>,
1040 schedule_vars: Option<JsonMap<String, Json>>,
1041 opts: &'a RunOptions,
1042 date: NaiveDate,
1044 calendar: Calendar,
1045 ui: Events,
1046 store: &'a RunStore,
1047 compiled_dir: PathBuf,
1048 runs: crate::runs::BindingRuns,
1050 run_id: String,
1052 run_dir: PathBuf,
1053 becomes_current: bool,
1056 schema_dir: PathBuf,
1057 target: String,
1059 parsed: ParsedBinding,
1061 pool: Option<Arc<Pool>>,
1063 renderers: BTreeMap<Option<String>, Arc<Renderer>>,
1065 started: Instant,
1066 started_at: chrono::DateTime<chrono::Utc>,
1067 produced: Vec<Produced>,
1068 outs: Vec<OutputRun>,
1070 attempts: u32,
1072 drift: Vec<String>,
1073 protected_snapshot: Option<Json>,
1075 retry: Option<per_row::Retry>,
1077 held: Vec<crate::release::HeldEntry>,
1079}
1080
1081#[derive(Default)]
1083struct OutputRun {
1084 dests: Vec<RenderedDest>,
1086 files: Vec<(PathBuf, Option<String>)>,
1088 deliveries: Vec<Delivery>,
1090 delivery_note: Option<String>,
1091 status: Option<OutputStatus>,
1093 error: Option<String>,
1094 when: Option<bool>,
1096 message: Option<(String, String)>,
1098 reason: Option<String>,
1100 no_data: Vec<String>,
1102 dropped: Vec<String>,
1104 for_each: Option<run_results::ForEachResult>,
1106 row_files: std::collections::HashMap<String, Vec<PathBuf>>,
1108}
1109
1110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112enum OutputStatus {
1113 Delivered,
1114 Kept,
1116 Skipped,
1118 Failed,
1119 Held,
1121}
1122
1123impl OutputStatus {
1124 fn as_str(self) -> &'static str {
1125 match self {
1126 OutputStatus::Delivered => "delivered",
1127 OutputStatus::Kept => "kept",
1128 OutputStatus::Skipped => "skipped",
1129 OutputStatus::Failed => "failed",
1130 OutputStatus::Held => "held",
1131 }
1132 }
1133}
1134
1135struct RenderedDest {
1137 profile: String,
1138 path: Option<String>,
1139 options: JsonMap<String, Json>,
1140 attach: Vec<String>,
1142 hold: bool,
1144 rate: Option<crate::rate::Rate>,
1146}
1147
1148type Fail = crate::error::Error;
1150
1151fn masked_source_error(error: &HostError, sensitive: bool) -> String {
1152 let error = error.to_string();
1153 crate::secrets::mask_source_error(&error, sensitive).into_owned()
1154}
1155
1156fn masked_sql_summary(sql: &str, max: usize, sensitive: bool) -> String {
1157 if sensitive {
1158 crate::secrets::MASK.to_string()
1159 } else {
1160 dre_protocol::util::summarize(&crate::secrets::mask(sql), max)
1161 }
1162}
1163
1164fn protected_sql(sql: &str, sensitive: bool) -> std::borrow::Cow<'_, str> {
1165 if sensitive {
1166 std::borrow::Cow::Borrowed(crate::secrets::MASK)
1167 } else {
1168 crate::secrets::mask(sql)
1169 }
1170}
1171
1172impl<'a> BindingRun<'a> {
1173 fn new(
1174 project: &'a Project,
1175 report: &'a Report,
1176 b: &'a Binding,
1177 opts: &'a RunOptions,
1178 ui: Events,
1179 store: &'a RunStore,
1180 ) -> Self {
1181 let schedule = opts
1182 .schedule
1183 .as_ref()
1184 .and_then(|n| project.schedules.iter().find(|e| &e.name == n));
1185 let schedule_vars = schedule.map(|e| e.vars.clone());
1186 let calendar: Calendar = crate::parse::calendar(
1188 project,
1189 report,
1190 opts.timezone.as_deref(),
1191 opts.schedule.as_deref(),
1192 );
1193 let date = crate::parse::run_date(&calendar, opts.date, opts.scheduled_at);
1194 let mut vars = b.vars.clone();
1196 vars.extend(schedule_vars.clone().unwrap_or_default());
1197 let rendered_vars = {
1198 let mut v = vars.clone();
1199 v.extend(
1200 opts.vars
1201 .iter()
1202 .map(|(k, x)| (k.clone(), Json::String(x.clone()))),
1203 );
1204 v
1205 };
1206 ui.binding_vars(opts.schedule.as_deref(), schedule_vars.as_ref(), &rendered_vars);
1207 let started_at = chrono::Utc::now();
1208 let runs = crate::runs::BindingRuns::new(&store.run_dir(&report.name, b.dir_name()));
1209 let run_id = if opts.dry_run || opts.live_check {
1211 "<run id>".to_string()
1212 } else {
1213 crate::runs::new_run_id(started_at)
1214 };
1215 BindingRun {
1216 project,
1217 report,
1218 b,
1219 vars,
1220 rendered_vars,
1221 schedule_vars,
1222 opts,
1223 date,
1224 calendar,
1225 ui,
1226 store,
1227 compiled_dir: store.compiled_dir(&report.name, b.dir_name()),
1228 run_dir: runs.run_dir(&run_id),
1229 runs,
1230 run_id,
1231 becomes_current: true,
1232 schema_dir: store.schema_dir(&report.name, b.dir_name()),
1233 target: project.target_name.clone(),
1234 parsed: ParsedBinding::default(),
1235 pool: None,
1236 renderers: BTreeMap::new(),
1237 started: Instant::now(),
1238 started_at,
1239 produced: Vec::new(),
1240 outs: b.outputs.iter().map(|_| OutputRun::default()).collect(),
1241 attempts: 1,
1242 drift: Vec::new(),
1243 protected_snapshot: None,
1244 retry: None,
1245 held: Vec::new(),
1246 }
1247 }
1248
1249 fn connections(&self) -> Arc<ProfileConnections> {
1251 Arc::new(ProfileConnections {
1252 profiles: self.project.profiles.clone(),
1253 root: self.project.root.clone(),
1254 plugins: self.project.plugins.clone(),
1255 log: self.ui.plugin_log(),
1256 })
1257 }
1258
1259 fn renderer(&mut self, connection: Option<&str>) -> Result<Arc<Renderer>, Fail> {
1262 let key = connection.map(str::to_string);
1263 if let Some(r) = self.renderers.get(&key) {
1264 return Ok(r.clone());
1265 }
1266 let pool = self.pool.clone().expect("the pool is made before rendering");
1267 let name = key.clone().or_else(|| self.parsed.inherited.clone());
1269 let source_type = match (&key, &name) {
1270 (Some(n), _) => pool.kind(n)?,
1271 (None, Some(n)) => pool.kind(n).unwrap_or_default(),
1273 (None, None) => String::new(),
1274 };
1275 let context =
1276 crate::parse::run_context(self.project, self.report, self.b, &self.inputs(), self.started_at);
1277 let limited = Arc::new(crate::render::Limited::new(
1278 context.clone(),
1279 self.vars.clone(),
1280 self.opts.vars.clone(),
1281 ));
1282 let r = Renderer::new(RendererConfig {
1283 root: &self.project.root,
1284 macros: &self.project.macros,
1285 context,
1286 vars: self.vars.clone(),
1287 cli_vars: self.opts.vars.clone(),
1288 runner: Some(Arc::new(PoolRunner {
1289 pool: pool.clone(),
1290 default: name.clone(),
1291 log: self.ui.sql_log(),
1292 })),
1293 connections: Some(pool.connections.clone()),
1294 mode: Mode::Run,
1295 connection: name,
1296 source_type,
1297 sources: crate::parse::resolver(self.project, limited),
1298 run_query_max_rows: self.project.run_query_max_rows,
1299 sql: self.project.sql.clone(),
1300 lookups: self.project.lookups.clone(),
1301 lookup_inline_max_rows: self.project.lookup_inline_max_rows,
1302 packages: self.project.packages.clone(),
1303 project_name: self.project.name.clone(),
1304 dispatch: self.project.dispatch.clone(),
1305 })
1306 .map_err(|e| e.to_string())?;
1307 let r = Arc::new(r);
1308 self.renderers.insert(key, r.clone());
1309 Ok(r)
1310 }
1311
1312 fn inputs(&self) -> crate::parse::Inputs {
1314 crate::parse::Inputs {
1315 target: self.target.clone(),
1316 cli_vars: self.opts.vars.clone(),
1317 date: self.opts.date,
1318 scheduled_at: self.opts.scheduled_at,
1319 timezone: self.opts.timezone.clone(),
1320 schedule: self.opts.schedule.clone(),
1321 started_at: Some(self.started_at),
1322 }
1323 }
1324
1325 fn connection_of(&self, query: &str) -> Result<String, Fail> {
1327 self.parsed
1328 .query(query)
1329 .and_then(|q| q.connection.clone())
1330 .ok_or_else(|| format!("query `{query}` has no connection").into())
1331 }
1332
1333 fn label(&self) -> String {
1335 match &self.b.set {
1336 Some(s) => format!("{} (Set {s})", self.report.name),
1337 None => self.report.name.clone(),
1338 }
1339 }
1340
1341 fn lock_failure(&self, e: crate::runs::LockError) -> Fail {
1343 let unlock = match &self.b.set {
1344 Some(s) => format!("dre unlock {} --binding {s}", self.report.name),
1345 None => format!("dre unlock {}", self.report.name),
1346 };
1347 let message = match e {
1348 crate::runs::LockError::Held(h) => format!(
1349 "{} is already running ({}); this run changed nothing. Wait for it, or if it's gone, `{unlock}`",
1350 self.label(),
1351 h.describe()
1352 ),
1353 crate::runs::LockError::Unreadable(p) => format!(
1354 "{} has an unreadable lock at {}; if no run is going on, `{unlock}`",
1355 self.label(),
1356 p.display()
1357 ),
1358 crate::runs::LockError::Io(e) => format!("can't lock {}: {e}", self.label()),
1359 };
1360 Fail::new(Code::RunInProgress, message)
1361 }
1362
1363 fn older_than_current(&self) -> bool {
1366 let Some(dir) = self.runs.current_dir() else {
1367 return false;
1368 };
1369 let Ok(text) = std::fs::read_to_string(dir.join("run_results.json")) else {
1370 return false;
1371 };
1372 let Ok(v) = serde_json::from_str::<Json>(&text) else {
1373 return false;
1374 };
1375 let theirs = v
1376 .get("scheduled_at")
1377 .and_then(Json::as_str)
1378 .or_else(|| v.get("started_at").and_then(Json::as_str))
1379 .and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())
1380 .map(|t| t.with_timezone(&chrono::Utc));
1381 let mine = self.opts.scheduled_at.unwrap_or(self.started_at);
1382 theirs.is_some_and(|t| mine < t)
1383 }
1384
1385 fn row_limit(&self) -> Option<u64> {
1389 if self.b.outputs.iter().any(|o| o.for_each.is_some()) {
1390 None
1391 } else {
1392 self.opts.preview
1393 }
1394 }
1395
1396 fn not_cancelled(&self) -> Result<(), Fail> {
1397 match self.opts.cancel.reason() {
1398 None => Ok(()),
1399 Some(crate::engine::CancelReason::Timeout) => {
1400 Err(Fail::new(Code::RunTimedOut, "the run timed out"))
1401 }
1402 Some(_) => Err(Fail::new(Code::RunCancelled, "the run was cancelled")),
1403 }
1404 }
1405
1406 fn outcome(&self, status: Status, error: Option<Fail>) -> BindingOutcome {
1407 BindingOutcome {
1408 report: self.report.name.clone(),
1409 set: self.b.set.clone(),
1410 binding: self.b.dir_name().to_string(),
1411 status,
1412 error_code: error.as_ref().map(|e| e.code.clone()),
1413 error: error.map(|e| e.message),
1414 files: self.files().map(|(p, _)| p.clone()).collect(),
1415 summary: self.summary_line(),
1416 schedule: self.opts.schedule.clone(),
1417 schedule_vars: self.schedule_vars.clone(),
1418 vars: self.rendered_vars.clone(),
1419 timezone: self.calendar.tz.name().to_string(),
1420 elapsed: self.started.elapsed(),
1421 }
1422 }
1423
1424 fn run(&mut self) -> BindingOutcome {
1425 let dry = self.opts.dry_run || self.opts.live_check;
1426 let guard = if dry {
1428 None
1429 } else {
1430 match self.runs.lock(&self.run_id, self.started_at) {
1431 Ok(g) => Some(g),
1432 Err(e) => {
1433 let error = self.lock_failure(e);
1434 return self.outcome(Status::Error, Some(error));
1435 }
1436 }
1437 };
1438 if let Some(g) = &guard {
1439 if let Some(h) = &g.took_over {
1440 self.ui.warn(&format!(
1441 "took over the lock of {} for {}: that process is gone (it crashed or was killed)",
1442 h.describe(),
1443 self.label()
1444 ));
1445 }
1446 match self.runs.migrate_legacy(self.started_at) {
1447 Ok(Some(id)) => self.ui.step(
1448 Level::Debug,
1449 "Moved",
1450 &format!("the files of the run before run folders into runs/{id}/"),
1451 None,
1452 ),
1453 Ok(None) => {}
1454 Err(e) => self
1455 .ui
1456 .warn(&format!("can't move the earlier run's files into runs/: {e}")),
1457 }
1458 self.becomes_current = !self.older_than_current() && self.opts.retry_failed.is_none();
1460 if std::fs::create_dir_all(&self.run_dir).is_ok() {
1462 self.ui.emit(RunEvent::BindingLog(self.run_dir.join("dre.log")));
1463 }
1464 }
1465 let result = self.run_inner();
1466 let stopped =
1469 matches!(&result, Err(e) if e.code == Code::RunCancelled || e.code == Code::RunTimedOut);
1470 let result = match (result, self.opts.cancel.reason()) {
1471 (Err(e), Some(reason)) if !stopped => {
1472 let (code, what) = match reason {
1473 crate::engine::CancelReason::Timeout => (Code::RunTimedOut, "timed out"),
1474 _ => (Code::RunCancelled, "was cancelled"),
1475 };
1476 Err(Fail::new(code, format!("the run {what}: {}", e.message)))
1477 }
1478 (r, _) => r,
1479 };
1480 let status = match (&result, dry, self.opts.live_check) {
1481 (Err(e), _, _) if e.code == Code::RunCancelled => Status::Cancelled,
1482 (Err(e), _, _) if e.code == Code::RunTimedOut => Status::TimedOut,
1483 (Err(_), _, _) => Status::Error,
1484 (Ok(()), _, true) => Status::Checked,
1485 (Ok(()), true, _) => Status::DryRun,
1486 (Ok(()), false, _) => Status::Success,
1487 };
1488 let err = result.err();
1489 if !dry && let Err(e) = self.write_results(&status, err.as_ref()) {
1490 self.ui.warn(&format!("can't write run_results.json: {e}"));
1491 }
1492 if guard.is_some() {
1493 if self.becomes_current {
1494 if let Err(e) = self.runs.switch(&self.run_id) {
1495 self.ui.warn(&format!("can't make this run the current one: {e}"));
1496 }
1497 } else {
1498 self.ui.warn(&format!(
1499 "{}: a run for a later instant is current, so this one doesn't replace it; its files are in {}",
1500 self.label(),
1501 rel(&self.project.root, &self.run_dir).display()
1502 ));
1503 }
1504 for id in self
1505 .runs
1506 .prune(self.opts.keep_runs.unwrap_or(self.project.keep_runs))
1507 {
1508 self.ui.step(Level::Debug, "Removed", &format!("run {id}"), None);
1509 }
1510 }
1511 let _ = std::fs::remove_dir_all(self.run_dir.join(".spool"));
1513 self.outcome(status, err)
1514 }
1515
1516 fn run_inner(&mut self) -> Result<(), Fail> {
1517 let errors = crate::variables::check_run_values(&self.report.var_schema, &self.vars, &self.opts.vars);
1518 if !errors.is_empty() {
1519 return Err(errors.join("; ").into());
1520 }
1521 self.parsed = crate::parse::binding(self.project, self.report, self.b, &self.vars, &self.inputs());
1523 if !self.parsed.errors.is_empty() {
1524 let msgs: Vec<String> = self
1525 .parsed
1526 .errors
1527 .iter()
1528 .map(|p| match p.line {
1529 Some(l) => format!("{}:{l}: {}", p.file.display(), p.message),
1530 None => format!("{}: {}", p.file.display(), p.message),
1531 })
1532 .collect();
1533 return Err(Fail::new(Code::ParseFailed, msgs.join("\n ")));
1534 }
1535 let pool = Arc::new(Pool::new(
1538 self.connections(),
1539 self.project.root.clone(),
1540 self.ui.plugin_log(),
1541 !self.report.managed,
1542 ));
1543 self.pool = Some(pool.clone());
1544
1545 if !self.report.managed {
1546 self.ui.warn(&format!(
1547 "`{}` is an unmanaged report ({}): for quick tests only — add a YAML to make it a managed report",
1548 self.report.name,
1549 self.report.file.display()
1550 ));
1551 }
1552
1553 std::fs::create_dir_all(&self.compiled_dir).map_err(|e| e.to_string())?;
1555 let mut per_connection: BTreeMap<String, usize> = BTreeMap::new();
1557 for q in &self.b.queries {
1558 *per_connection.entry(self.connection_of(&q.query)?).or_default() += 1;
1559 }
1560 let interleave = !self.opts.dry_run && !self.opts.live_check && self.report.managed;
1564 let mut statements = Vec::new();
1565 let mut two_tabs: Option<String> = None;
1567 let retry_snapshot = match &self.opts.retry_failed {
1568 Some(id) => self.start_retry(id)?,
1569 None => None,
1570 };
1571 if let Some(dir) = retry_snapshot {
1572 self.render_destinations().map_err(|e| e.or(Code::RenderFailed))?;
1573 self.replay_from(&dir, "the saved results")?;
1574 } else if let Some(dir) = self.opts.from_results.clone() {
1575 self.replay(&dir)?;
1576 } else if interleave {
1577 self.render_destinations().map_err(|e| e.or(Code::RenderFailed))?;
1578 let mut checked: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1579 for (c, n) in &per_connection {
1580 if *n > 1 {
1581 self.check_sessions(&pool, c, *n)?;
1582 checked.insert(c.clone());
1583 }
1584 }
1585 std::fs::create_dir_all(self.run_dir.join(".spool")).map_err(|e| e.to_string())?;
1586 let mut i = 0;
1587 let queries = self.b.queries.clone();
1588 for q in &queries {
1589 let conn = self.connection_of(&q.query)?;
1590 let renderer = self.renderer(Some(&conn))?;
1591 let sts = self.render_query(&renderer, q, &conn, &mut two_tabs)?;
1592 for w in renderer.take_warnings() {
1593 self.ui.warn(&w);
1594 }
1595 if let Some(e) = two_tabs.take() {
1596 return Err(e.into());
1597 }
1598 if sts.len() > 1 && checked.insert(conn.clone()) {
1599 self.check_sessions(&pool, &conn, sts.len())?;
1600 }
1601 let session = pool.session(&conn)?;
1602 for st in &sts {
1603 self.execute_statement(&session, i, st)?;
1604 i += 1;
1605 }
1606 }
1607 } else {
1608 let queries = self.b.queries.clone();
1609 for q in &queries {
1610 let conn = self.connection_of(&q.query)?;
1611 let renderer = self.renderer(Some(&conn))?;
1612 statements.extend(self.render_query(&renderer, q, &conn, &mut two_tabs)?);
1613 }
1614 self.render_destinations().map_err(|e| e.or(Code::RenderFailed))?;
1615 for r in self.renderers.values() {
1616 for w in r.take_warnings() {
1617 self.ui.warn(&w);
1618 }
1619 }
1620
1621 if !self.report.managed {
1623 if let Some(bad) = statements.iter().find(|s| !s.kind.is_read_only_safe()) {
1624 return Err(format!(
1625 "{}:{}: unmanaged report `{}` may only run SELECT/WITH or CREATE [OR REPLACE] TEMP|TEMPORARY TABLE|VIEW, but found `{}`; nothing was run — rewrite the statement, or give the report a YAML to declare it",
1626 bad.file.display(),
1627 bad.line,
1628 self.report.name,
1629 masked_sql_summary(&bad.text, 60, bad.sensitive)
1630 ).into());
1631 }
1632 for conn in per_connection.keys() {
1633 let session = pool.session(conn)?;
1634 let creates_temp = statements
1635 .iter()
1636 .any(|s| &s.connection == conn && s.kind == StatementKind::TempCreate)
1637 || session.lock().unwrap().loaded;
1638 session.lock().unwrap().want_read_only(!creates_temp);
1639 }
1640 }
1641 if let Some(e) = two_tabs {
1642 return Err(e.into());
1643 }
1644
1645 if self.opts.dry_run {
1646 let plan = self.plan();
1647 self.ui.compiled(&plan);
1648 return Ok(());
1649 }
1650
1651 for conn in per_connection.keys() {
1653 let n = statements.iter().filter(|s| &s.connection == conn).count();
1654 if n > 1 {
1655 self.check_sessions(&pool, conn, n)?;
1656 }
1657 }
1658
1659 if self.opts.live_check {
1660 return self.live_check(&statements);
1661 }
1662
1663 std::fs::create_dir_all(self.run_dir.join(".spool")).map_err(|e| e.to_string())?;
1665 for (i, st) in statements.iter().enumerate() {
1666 let session = pool.session(&st.connection)?;
1667 self.execute_statement(&session, i, st)?;
1668 }
1669 }
1670 pool.close_all();
1671
1672 self.name_result_sets()?;
1673 if let Some(dir) = &self.opts.output_json {
1674 let dir = self
1675 .project
1676 .root
1677 .join(dir)
1678 .join(&self.report.name)
1679 .join(self.b.dir_name());
1680 std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
1681 let mut index = crate::saved::Index {
1682 schema_version: crate::saved::SCHEMA_VERSION.into(),
1683 run_id: self.run_id.clone(),
1684 results: Vec::new(),
1685 };
1686 for result in &self.produced {
1687 crate::rows::write_spool(
1688 &result.spool,
1689 &dir.join(format!("{}.json", result.name)),
1690 result.rows,
1691 )
1692 .map_err(|e| e.to_string())?;
1693 let file = format!("{}.arrows", result.name);
1695 crate::saved::spool_to_stream(&result.spool, &dir.join(&file))?;
1696 index.results.push(crate::saved::SavedResult {
1697 query: result.query.clone(),
1698 name: result.name.clone(),
1699 connection: result.connection.clone(),
1700 rows: result.rows,
1701 sql_sha256: std::fs::read_to_string(
1702 self.compiled_dir.join(format!("{}.sha256", result.query)),
1703 )
1704 .ok()
1705 .map(|h| h.trim().to_string()),
1706 file,
1707 });
1708 }
1709 crate::saved::write_index(&dir, &index).map_err(|e| e.to_string())?;
1710 }
1711
1712 let (file_outs, message_outs): (Vec<usize>, Vec<usize>) =
1714 (0..self.outs.len()).partition(|&i| !self.b.outputs[i].is_message());
1715 if self.retry.is_none()
1717 && self.opts.from_results.is_none()
1718 && self.opts.preview.is_none()
1719 && self
1720 .b
1721 .outputs
1722 .iter()
1723 .any(|o| o.for_each.as_ref().is_some_and(|f| f.keep_results))
1724 {
1725 self.save_snapshot()?;
1726 }
1727 for oi in 0..self.outs.len() {
1729 self.has_data(oi)?;
1730 }
1731 if self.retry.is_some() {
1732 for oi in 0..self.outs.len() {
1733 if self.b.outputs[oi].for_each.is_none() {
1734 self.skip(oi, "--retry-failed redoes per-row outputs only");
1735 self.outs[oi].reason = Some("retry".into());
1736 }
1737 }
1738 }
1739 for &oi in &file_outs {
1740 if self.outs[oi].status == Some(OutputStatus::Skipped) || !self.passes_when(oi)? {
1741 continue;
1742 }
1743 if self.b.outputs[oi].for_each.is_some() {
1745 continue;
1746 }
1747 let filename = self.file_names(oi);
1748 self.format(oi, &filename).map_err(|e| e.or(Code::FormatFailed))?;
1749 }
1750
1751 if self.opts.preview.is_none() {
1753 self.drift = self.schema_drift();
1754 if !self.drift.is_empty() {
1755 if self.opts.accept_schema_change {
1756 self.ui.warn(&format!(
1757 " schema changed since the last successful run (accepted): {}",
1758 self.drift.join("; ")
1759 ));
1760 } else {
1761 return Err(format!(
1762 "schema drift since the last successful run: {}; the output is in {} but was not delivered — pass --accept-schema-change to deliver it and accept the new schema",
1763 self.drift.join("; "),
1764 rel(&self.project.root, &self.run_dir).display()
1765 ).into());
1766 }
1767 }
1768 }
1769
1770 let preview = self.opts.preview.is_some() || self.opts.no_deliver;
1773 let mut failures = Vec::new();
1774 for oi in file_outs.into_iter().chain(message_outs) {
1775 if self.outs[oi].status == Some(OutputStatus::Skipped) {
1776 continue;
1777 }
1778 if self.b.outputs[oi].for_each.is_some() {
1779 if let Err(e) = self.run_rows(oi) {
1780 if e.code == Code::RunCancelled || e.code == Code::RunTimedOut {
1781 return Err(e);
1782 }
1783 self.outs[oi].status = Some(OutputStatus::Failed);
1784 self.outs[oi].error.get_or_insert_with(|| e.message.clone());
1785 failures.push(e);
1786 }
1787 continue;
1788 }
1789 if self.b.outputs[oi].is_message() {
1790 match self.render_message(oi) {
1791 Ok(true) => {}
1792 Ok(false) => continue,
1793 Err(e) => {
1794 let e = e.or(Code::RenderFailed);
1795 let e = Fail::new(e.code.clone(), format!("{}: {e}", self.b.outputs[oi].label(oi)));
1796 self.outs[oi].status = Some(OutputStatus::Failed);
1797 self.outs[oi].error = Some(e.message.clone());
1798 failures.push(e);
1799 continue;
1800 }
1801 }
1802 }
1803 if preview {
1804 self.outs[oi].delivery_note = Some(if self.opts.no_deliver {
1805 "--no-deliver: output kept locally".into()
1806 } else {
1807 "preview: not delivered".into()
1808 });
1809 self.outs[oi].status = Some(OutputStatus::Kept);
1810 } else if let Err(e) = self.deliver(oi) {
1811 failures.push(e.or(Code::DeliveryFailed));
1812 }
1813 }
1814 if preview {
1815 let dir = rel(&self.project.root, &self.run_dir);
1816 self.ui.step(
1817 Level::Info,
1818 "Preview",
1819 &format!("not delivered; output stays in {}", dir.display()),
1820 None,
1821 );
1822 }
1823 if let Some(first) = failures.first() {
1824 let messages: Vec<&str> = failures.iter().map(|f| f.message.as_str()).collect();
1825 return Err(Fail::new(first.code.clone(), messages.join("; ")));
1826 }
1827
1828 if self.opts.preview.is_none()
1831 && self.becomes_current
1832 && let Err(error) = self.write_snapshot()
1833 {
1834 self.ui.warn(&format!(
1835 "delivery completed, but the schema snapshot could not be written: {error}; the next run may compare against an older schema"
1836 ));
1837 }
1838 Ok(())
1839 }
1840
1841 fn results_for(&mut self, oi: usize, max_rows: u64) -> Result<Vec<(String, Arc<QueryResult>)>, Fail> {
1843 let o = &self.b.outputs[oi];
1844 let mut out = Vec::new();
1845 for p in self.produced.iter().filter(|p| o.feeds(&p.query)) {
1846 let r = QueryResult::read(&p.spool, p.rows, max_rows, self.calendar)
1847 .map_err(|e| format!("can't read the result of `{}`: {e}", p.query))?;
1848 out.push((p.query.clone(), Arc::new(r)));
1849 }
1850 Ok(out)
1851 }
1852
1853 fn template_context(&self, oi: usize, results: &[(String, Arc<QueryResult>)]) -> minijinja::Value {
1856 use minijinja::Value;
1857 let results: BTreeMap<String, Value> = results
1858 .iter()
1859 .map(|(q, r)| (q.clone(), r.clone().value()))
1860 .collect();
1861 let mut outputs: BTreeMap<String, Value> = BTreeMap::new();
1862 for (j, (o, run)) in self.b.outputs.iter().zip(&self.outs).enumerate() {
1863 let Some(name) = &o.name else { continue };
1864 if j == oi {
1865 continue;
1866 }
1867 let locations: Vec<&str> = run
1868 .deliveries
1869 .iter()
1870 .filter_map(|d| d.location.as_deref())
1871 .collect();
1872 let files: Vec<String> = run
1873 .files
1874 .iter()
1875 .map(|(f, _)| record_path(self.project, f).to_string_lossy().to_string())
1876 .collect();
1877 let status = run.status.map_or("pending", OutputStatus::as_str);
1878 outputs.insert(
1879 name.clone(),
1880 Value::from(minijinja::value::Serde(json!({
1881 "location": locations.join(", "),
1882 "files": files,
1883 "status": status,
1884 }))),
1885 );
1886 }
1887 Value::from(BTreeMap::from([
1888 ("results".to_string(), Value::from(results)),
1889 ("outputs".to_string(), Value::from(outputs)),
1890 ]))
1891 }
1892
1893 fn has_data(&mut self, oi: usize) -> Result<bool, Fail> {
1896 let o = &self.b.outputs[oi];
1897 let label = o.label(oi);
1898 let queries: Vec<crate::no_data::Query<'_>> = self
1900 .produced
1901 .iter()
1902 .filter(|p| match &o.for_each {
1903 Some(f) => p.query == f.query,
1904 None => o.feeds(&p.query),
1905 })
1906 .map(|p| crate::no_data::Query {
1907 name: &p.query,
1908 rows: p.rows,
1909 on_empty: self
1910 .b
1911 .queries
1912 .iter()
1913 .find(|q| q.query == p.query)
1914 .and_then(|q| q.on_empty),
1915 })
1916 .collect();
1917 let droppable = !(o.is_message()
1918 || crate::document::FORMATS.contains(&o.format.as_str())
1919 || o.for_each.is_some());
1920 let d = crate::no_data::decide(o.on_empty, &queries, droppable);
1921 self.outs[oi].no_data = d.empty.clone();
1922 for w in &d.warnings {
1923 self.ui.warn(&format!(" {label}: {w}"));
1924 }
1925 if let Some(info) = &d.info {
1926 self.ui
1927 .step(Level::Info, "No data", &format!("{label}: {info}"), None);
1928 }
1929 if let Some(msg) = d.fail {
1930 self.outs[oi].reason = Some("no_data".into());
1931 return Err(Fail::new(Code::NoData, format!("{label}: {msg}")));
1932 }
1933 if let Some(why) = d.skip {
1934 self.outs[oi].reason = Some("no_data".into());
1935 self.skip(oi, &why);
1936 return Ok(false);
1937 }
1938 self.outs[oi].dropped = d.drop;
1939 Ok(true)
1940 }
1941
1942 fn passes_when(&mut self, oi: usize) -> Result<bool, Fail> {
1944 if self.b.outputs[oi].when.is_none() {
1945 return Ok(true);
1946 }
1947 let results = self.results_for(oi, crate::message::DEFAULT_MAX_ROWS)?;
1948 self.when_holds(oi, &results)
1949 }
1950
1951 fn when_holds(&mut self, oi: usize, results: &[(String, Arc<QueryResult>)]) -> Result<bool, Fail> {
1953 let Some(when) = self.b.outputs[oi].when.clone() else {
1954 return Ok(true);
1955 };
1956 let ctx = self.template_context(oi, results);
1957 let renderer = self.renderer(None)?;
1958 let label = self.b.outputs[oi].label(oi);
1959 let src = format!("{{% if {when} %}}true{{% endif %}}");
1960 let passed = renderer
1961 .render_with(&self.report.file, &src, ctx, false)
1962 .map_err(|e| format!("{label}: `when` `{when}`: {}", e.message))?
1963 == "true";
1964 self.outs[oi].when = Some(passed);
1965 if !passed {
1966 self.skip(oi, &format!("`when` is false ({when})"));
1967 }
1968 Ok(passed)
1969 }
1970
1971 fn skip(&mut self, oi: usize, why: &str) {
1972 let label = self.b.outputs[oi].label(oi);
1973 self.outs[oi].status = Some(OutputStatus::Skipped);
1974 self.outs[oi].delivery_note = Some(format!("skipped: {why}"));
1975 self.ui
1976 .step(Level::Info, "Skipped", &format!("{label}: {why}"), None);
1977 }
1978
1979 fn render_message(&mut self, oi: usize) -> Result<bool, Fail> {
1982 let o = &self.b.outputs[oi];
1983 let opt = |k: &str| o.options.get(k).and_then(Json::as_str).map(str::to_string);
1984 let (text_src, file_src, title_src) = (opt("text"), opt("file"), opt("title"));
1985 let max_rows = o
1986 .options
1987 .get("max_rows")
1988 .and_then(Json::as_u64)
1989 .unwrap_or(crate::message::DEFAULT_MAX_ROWS);
1990 let results = self.results_for(oi, max_rows)?;
1991 if !self.when_holds(oi, &results)? {
1992 return Ok(false);
1993 }
1994 for (q, r) in results.iter().filter(|(_, r)| r.capped()) {
1995 self.ui.warn(&format!(
1996 " results.{q}.rows holds the first {} of {} rows (`max_rows`); aggregate in SQL, or raise `max_rows`",
1997 thousands(r.rows.len() as u64),
1998 thousands(r.row_count)
1999 ));
2000 }
2001 let ctx = self.template_context(oi, &results);
2002 let renderer = self.renderer(None)?;
2003 let file = self.report.file.clone();
2004 let text = match (text_src, file_src) {
2005 (Some(t), _) => renderer
2006 .render_with(&file, &t, ctx.clone(), true)
2007 .map_err(|e| e.message)?,
2008 (None, Some(f)) => {
2009 let root = &self.project.root;
2010 let path = crate::project::find_template(root, &f)
2011 .ok_or_else(|| format!("message file `{f}` doesn't exist"))?;
2012 let src = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
2013 let shown = rel(root, &path);
2014 renderer
2015 .render_with(&shown, &src, ctx.clone(), true)
2016 .map_err(|e| e.to_string())?
2017 }
2018 (None, None) => crate::message::default_text(&results, self.locale()),
2019 };
2020 let text = text.trim().to_string();
2021 if text.is_empty() {
2022 self.skip(oi, "the message is empty");
2023 return Ok(false);
2024 }
2025 let title = match title_src {
2026 Some(t) => renderer
2027 .render_with(&file, &t, ctx, false)
2028 .map_err(|e| e.message)?,
2029 None => format!("{}: {}", self.report.name, self.date),
2030 };
2031 let title = title.split_whitespace().collect::<Vec<_>>().join(" ");
2032 let name = self.file_names(oi);
2033 let path = self.run_dir.join(&name);
2034 if let Some(prev) = (0..oi).find(|&j| self.outs[j].files.iter().any(|(f, _)| f == &path)) {
2035 return Err(format!(
2036 "it writes {name}, as {} does; give them different `name:`s",
2037 self.b.outputs[prev].label(prev)
2038 )
2039 .into());
2040 }
2041 std::fs::create_dir_all(&self.run_dir).map_err(|e| e.to_string())?;
2042 std::fs::write(&path, crate::message::file_body(&title, &text)).map_err(|e| e.to_string())?;
2043 self.outs[oi].files.push((path, None));
2044 let shown_title = crate::secrets::mask_message_title(&title);
2046 let shown_text = crate::secrets::mask_markdown(&text);
2047 if self.opts.preview.is_some() {
2048 let mut notes = Vec::new();
2049 if let Some(n) = self.opts.preview {
2050 notes.push(format!(
2051 "numbers come from a sample of at most {} rows per query (--preview)",
2052 thousands(n)
2053 ));
2054 }
2055 if let Some(w) = &self.b.outputs[oi].when {
2056 notes.push(format!("`when` passed ({w})"));
2057 }
2058 let length = text.chars().count();
2059 notes.push(format!("{} characters", thousands(length as u64)));
2060 notes.extend(self.destination_limits(oi, length));
2061 self.ui.message(&ShownMessage {
2062 output: self.b.outputs[oi].name.clone(),
2063 title: shown_title.into_owned(),
2064 text: shown_text.into_owned(),
2065 notes,
2066 });
2067 } else {
2068 self.ui.step(
2069 Level::Info,
2070 "Message",
2071 &crate::message::excerpt(&title, &text, 100),
2072 None,
2073 );
2074 }
2075 self.outs[oi].message = Some((title, text));
2076 Ok(true)
2077 }
2078
2079 fn destination_limits(&self, oi: usize, length: usize) -> Vec<String> {
2081 let mut out = Vec::new();
2082 let Some(pool) = &self.pool else { return out };
2083 for d in &self.outs[oi].dests {
2084 let Some(t) = self.project.profiles.target(Role::Destination, &d.profile) else {
2085 out.push(format!("{}: delivers nowhere on this target", d.profile));
2086 continue;
2087 };
2088 let described = pool.connections.described(Role::Destination, &t.kind);
2089 let takes = described
2090 .as_ref()
2091 .is_some_and(|x| x.capabilities.iter().any(|c| c == CAP_MESSAGE));
2092 let limit = described.and_then(|x| x.message_limit);
2093 out.push(match (takes, limit) {
2094 (false, _) => format!("{} ({}): delivers the .md file", d.profile, t.kind),
2095 (true, Some(l)) if length as u64 > l => format!(
2096 "{} ({}): {} of {} characters: over the limit, so it will be cut short",
2097 d.profile,
2098 t.kind,
2099 thousands(length as u64),
2100 thousands(l)
2101 ),
2102 (true, Some(l)) => format!(
2103 "{} ({}): {} of {} characters",
2104 d.profile,
2105 t.kind,
2106 thousands(length as u64),
2107 thousands(l)
2108 ),
2109 (true, None) => format!("{} ({}): posts the message", d.profile, t.kind),
2110 });
2111 }
2112 out
2113 }
2114
2115 fn locale(&self) -> crate::numbers::Locale {
2117 self.b
2118 .locale
2119 .as_deref()
2120 .and_then(|l| crate::numbers::Locale::parse(l).ok())
2121 .unwrap_or_default()
2122 }
2123
2124 fn render_query(
2126 &mut self,
2127 renderer: &Renderer,
2128 q: &QueryEntry,
2129 connection: &str,
2130 two_tabs: &mut Option<String>,
2131 ) -> Result<Vec<Statement>, Fail> {
2132 let mut out = Vec::new();
2133 let src = std::fs::read_to_string(self.project.root.join(&q.path))
2134 .map_err(|e| format!("{}: {e}", q.path.display()))?;
2135 let _ = renderer.take_sources();
2136 let sql = renderer
2137 .render(&q.path, &src)
2138 .map_err(|e: RenderError| e.to_string())?;
2139 let known = self
2142 .parsed
2143 .query(&q.query)
2144 .map(|p| p.sources.clone())
2145 .unwrap_or_default();
2146 if let Some((s, t)) = renderer
2147 .take_sources()
2148 .into_iter()
2149 .find(|(s, t)| !known.contains(&format!("{s}.{t}")))
2150 {
2151 return Err(format!(
2152 "{}: `source('{s}', '{t}')` was reached while rendering `{}`, but the parse pass didn't find it. The parse pass renders without data (`run_query()` returns no rows, `connection.*` nothing), so it can't see a `source()` that depends on them; call it where it's reached either way",
2153 q.path.display(),
2154 q.query
2155 ).into());
2156 }
2157 let sensitive = crate::secrets::contains_secret(&sql);
2158 std::fs::write(
2159 self.compiled_dir.join(format!("{}.sql", q.query)),
2160 crate::secrets::mask(&sql).as_bytes(),
2161 )
2162 .map_err(|e| e.to_string())?;
2163 {
2166 use sha2::Digest;
2167 let digest = sha2::Sha256::digest(sql.as_bytes());
2168 let hash = digest
2169 .iter()
2170 .map(|byte| format!("{byte:02x}"))
2171 .collect::<String>();
2172 std::fs::write(self.compiled_dir.join(format!("{}.sha256", q.query)), hash)
2173 .map_err(|e| e.to_string())?;
2174 }
2175 self.ui
2176 .step(Level::Debug, "Rendered", &q.path.display().to_string(), None);
2177 let parts = sqlsplit::split(&sql);
2180 if q.tab && parts.is_empty() {
2181 return Err(format!(
2182 "{}: `{}` makes a tab but has no statements; add a query, or `tab: false` if it's meant to be empty",
2183 q.path.display(),
2184 q.query
2185 ).into());
2186 }
2187 let n = parts.len();
2188 for (i, st) in parts.into_iter().enumerate() {
2189 let body = sqlsplit::strip_leading_comments(&st.text);
2190 let line = st.line + st.text[..st.text.len() - body.len()].matches('\n').count();
2191 let kind = sqlsplit::classify(&st.text);
2192 let last = i + 1 == n;
2193 if q.tab && !last && kind == StatementKind::Read && two_tabs.is_none() {
2194 *two_tabs = Some(format!(
2195 "{}:{line}: `{}` has more than one SELECT, but one .sql file makes one tab (from its last statement); put each tab's query in its own .sql file and list each in `queries:`",
2196 q.path.display(),
2197 q.query
2198 ));
2199 }
2200 out.push(Statement {
2201 query: q.query.clone(),
2202 connection: connection.to_string(),
2203 file: q.path.clone(),
2204 line,
2205 kind,
2206 text: st.text,
2207 sensitive,
2208 tab: q.tab && last,
2209 });
2210 }
2211 Ok(out)
2212 }
2213
2214 fn check_sessions(&self, pool: &Pool, connection: &str, n: usize) -> Result<(), Fail> {
2216 let session = pool.session(connection)?;
2217 let mut s = session.lock().unwrap();
2218 if !s
2219 .get()
2220 .map_err(|e| Fail::new(Code::ConnectionFailed, e))?
2221 .has(CAP_SESSIONS)
2222 {
2223 let kind = pool.kind(connection).unwrap_or_default();
2224 return Err(format!(
2225 "this Binding runs {n} statements on connection `{connection}`, but the `{kind}` source plugin can't hold one session across them; nothing was run"
2226 ).into());
2227 }
2228 Ok(())
2229 }
2230
2231 fn execute_statement(
2233 &mut self,
2234 session: &Arc<Mutex<Session>>,
2235 i: usize,
2236 st: &Statement,
2237 ) -> Result<(), Fail> {
2238 self.not_cancelled()?;
2239 let sql_log = self.ui.sql_log();
2240 sql_log(
2241 &format!("{}:{}", st.file.display(), st.line),
2242 &protected_sql(&st.text, st.sensitive),
2243 );
2244 let spool_path = self.run_dir.join(".spool").join(format!("{i}.arrow"));
2245 let mut writer: Option<FileWriter<File>> = None;
2246 let t = Instant::now();
2247 let exec = {
2248 let mut s = session.lock().unwrap();
2249 let (p, _log_scope) = s
2250 .statement(st.sensitive)
2251 .map_err(|e| Fail::new(Code::ConnectionFailed, e))?;
2252 p.execute(&st.text, self.row_limit(), |schema, batch| {
2253 if writer.is_none() {
2254 let f = File::create(&spool_path).map_err(|e| e.to_string())?;
2255 writer = Some(FileWriter::try_new(f, schema).map_err(|e| e.to_string())?);
2256 }
2257 writer.as_mut().unwrap().write(&batch).map_err(|e| e.to_string())
2258 })
2259 .map_err(|e| {
2260 let message = format!(
2261 "{}:{}: {}",
2262 st.file.display(),
2263 st.line,
2264 masked_source_error(&e, st.sensitive)
2265 );
2266 Fail::from_plugin(&e, message).or(Code::QueryFailed)
2267 })?
2268 };
2269 let what = match &exec {
2270 Execution::Result { rows, .. } => {
2271 format!("{} row{}", thousands(*rows), if *rows == 1 { "" } else { "s" })
2272 }
2273 Execution::NoResult {
2274 rows_affected: Some(n),
2275 } => format!("no result set ({n} affected)"),
2276 Execution::NoResult { .. } => "no result set".to_string(),
2277 };
2278 let at = format!("{}:{}", st.file.display(), st.line);
2279 self.ui.step(
2280 Level::Debug,
2281 "Executed",
2282 &format!("{at} {what}"),
2283 Some(t.elapsed()),
2284 );
2285 let (schema, rows) = match (st.tab, exec) {
2288 (true, Execution::Result { schema, rows }) => (schema, rows),
2289 (true, Execution::NoResult { .. }) => {
2290 return Err(Fail::new(
2291 Code::NoResultSet,
2292 format!(
2293 "{at}: `{}` makes a tab, but its last statement returned no result set; if the query only prepares data (a temp view, a SET), add `tab: false` to it in the YAML",
2294 st.query
2295 ),
2296 ));
2297 }
2298 (false, _) => {
2299 drop(writer);
2300 let _ = std::fs::remove_file(&spool_path);
2301 return Ok(());
2302 }
2303 };
2304 {
2305 let mut w = match writer {
2306 Some(w) => w,
2307 None => FileWriter::try_new(File::create(&spool_path).map_err(|e| e.to_string())?, &schema)
2308 .map_err(|e| e.to_string())?,
2309 };
2310 w.finish().map_err(|e| e.to_string())?;
2311 self.produced.push(Produced {
2312 query: st.query.clone(),
2313 connection: st.connection.clone(),
2314 schema,
2315 rows,
2316 spool: spool_path,
2317 name: String::new(),
2318 anchor: None,
2319 header: None,
2320 columns: Default::default(),
2321 autofit: None,
2322 style: None,
2323 });
2324 }
2325 Ok(())
2326 }
2327
2328 fn replay(&mut self, dir: &Path) -> Result<(), Fail> {
2332 let shown = dir.display().to_string();
2333 let dir = self
2334 .project
2335 .root
2336 .join(dir)
2337 .join(&self.report.name)
2338 .join(self.b.dir_name());
2339 if let Err(e) = crate::saved::read_index(&dir) {
2340 return Err(format!(
2341 "--from-results {shown}: no saved results for this report and Set ({e}); save them with `dre run {} --no-deliver --output-json {shown}`",
2342 self.report.name
2343 )
2344 .into());
2345 }
2346 self.replay_from(&dir, &format!("--from-results {shown}"))
2347 }
2348
2349 fn replay_from(&mut self, dir: &Path, shown: &str) -> Result<(), Fail> {
2352 let dir = dir.to_path_buf();
2353 let index = crate::saved::read_index(&dir).map_err(|e| format!("{shown}: {e}"))?;
2354 let queries: Vec<QueryEntry> = self.b.queries.iter().filter(|q| q.tab).cloned().collect();
2355 let missing: Vec<&str> = queries
2356 .iter()
2357 .filter(|q| index.find(&q.query).is_none())
2358 .map(|q| q.query.as_str())
2359 .collect();
2360 if !missing.is_empty() {
2361 return Err(format!(
2362 "{shown}: no saved result for {} (saved: {}); save them again with `--output-json`",
2363 missing
2364 .iter()
2365 .map(|q| format!("`{q}`"))
2366 .collect::<Vec<_>>()
2367 .join(", "),
2368 index
2369 .results
2370 .iter()
2371 .map(|r| r.query.as_str())
2372 .collect::<Vec<_>>()
2373 .join(", ")
2374 )
2375 .into());
2376 }
2377 for q in &queries {
2379 let saved = index.find(&q.query).and_then(|r| r.sql_sha256.clone());
2380 let rendered = self
2381 .connection_of(&q.query)
2382 .and_then(|conn| {
2383 let renderer = self.renderer(Some(&conn))?;
2384 let mut two = None;
2385 self.render_query(&renderer, q, &conn, &mut two)
2386 })
2387 .map(|_| {
2388 std::fs::read_to_string(self.compiled_dir.join(format!("{}.sha256", q.query)))
2389 .ok()
2390 .map(|h| h.trim().to_string())
2391 });
2392 match (rendered, saved) {
2393 (Ok(Some(now)), Some(then)) if now != then => self.ui.warn(&format!(
2394 " `{}`'s SQL changed since its results were saved; re-run with `--output-json` to refresh them",
2395 q.query
2396 )),
2397 (Err(e), _) => self.ui.warn(&format!(
2398 " `{}`: can't render its SQL to compare with the saved results ({}); using them as saved",
2399 q.query, e.message
2400 )),
2401 _ => {}
2402 }
2403 }
2404 std::fs::create_dir_all(self.run_dir.join(".spool")).map_err(|e| e.to_string())?;
2405 for (i, q) in queries.iter().enumerate() {
2406 let r = index.find(&q.query).expect("checked above");
2407 let spool = self.run_dir.join(".spool").join(format!("{i}.arrow"));
2408 let (schema, rows) = crate::saved::stream_to_spool(&dir.join(&r.file), &spool)?;
2409 self.produced.push(Produced {
2410 query: q.query.clone(),
2411 connection: r.connection.clone(),
2412 schema,
2413 rows,
2414 spool,
2415 name: String::new(),
2416 anchor: None,
2417 header: None,
2418 columns: Default::default(),
2419 autofit: None,
2420 style: None,
2421 });
2422 }
2423 self.ui.step(
2424 Level::Info,
2425 "Replayed",
2426 &format!("{} result(s) from {shown}; no query ran", queries.len()),
2427 None,
2428 );
2429 Ok(())
2430 }
2431
2432 fn render_destinations(&mut self) -> Result<(), Fail> {
2435 if self.opts.no_deliver {
2436 return Ok(());
2437 }
2438 if self.b.destinations().next().is_none() {
2439 return Ok(());
2440 }
2441 let renderer = self.renderer(None)?;
2442 let connections = self.pool.as_ref().expect("pool").connections.clone();
2443 let numbered: Vec<(usize, usize, &crate::project::Destination)> = self
2445 .b
2446 .outputs
2447 .iter()
2448 .enumerate()
2449 .flat_map(|(oi, o)| o.destinations.iter().map(move |d| (oi, d)))
2450 .enumerate()
2451 .map(|(i, (oi, d))| (i, oi, d))
2452 .collect();
2453 for (i, oi, d) in numbered {
2454 if self.b.outputs[oi].for_each.is_some() {
2456 continue;
2457 }
2458 let profile = self
2459 .parsed
2460 .destinations
2461 .get(i)
2462 .cloned()
2463 .flatten()
2464 .ok_or_else(|| format!("destination `{}` has no profile", d.profile))?;
2465 if matches!(
2467 self.project.profiles.entry(Role::Destination, &profile),
2468 Entry::Nowhere
2469 ) {
2470 self.outs[oi].dests.push(RenderedDest {
2471 profile,
2472 path: None,
2473 options: JsonMap::new(),
2474 attach: Vec::new(),
2475 hold: d.hold,
2476 rate: self.opts.rate.or(d.rate),
2477 });
2478 continue;
2479 }
2480 renderer.set_destination(connections.profile(&profile, Some("destination")).ok());
2481 let rendered = (|| {
2482 let path = match &d.path {
2483 Some(p) => Some(
2484 renderer
2485 .render(&self.report.file, p)
2486 .map_err(|e| format!("output path: {e}"))?,
2487 ),
2488 None => None,
2489 };
2490 let mut options = JsonMap::new();
2491 for (k, v) in &d.options {
2492 let v = render_json(&renderer, &self.report.file, v)
2493 .map_err(|e| format!("destination `{profile}` option `{k}`: {e}"))?;
2494 options.insert(k.clone(), v);
2495 }
2496 Ok::<_, Fail>(RenderedDest {
2497 profile: profile.clone(),
2498 path,
2499 options,
2500 attach: d.attach.clone(),
2501 hold: d.hold,
2502 rate: self.opts.rate.or(d.rate),
2503 })
2504 })();
2505 renderer.set_destination(None);
2506 self.outs[oi].dests.push(rendered?);
2507 }
2508 Ok(())
2509 }
2510
2511 fn name_result_sets(&mut self) -> Result<(), Fail> {
2513 let entries: BTreeMap<&str, &QueryEntry> =
2514 self.b.queries.iter().map(|q| (q.query.as_str(), q)).collect();
2515 for p in &mut self.produced {
2516 let q = entries[p.query.as_str()];
2517 p.name = q.tab_name.clone().unwrap_or_else(|| q.query.clone());
2518 p.anchor = q.anchor.clone();
2519 p.header = q.header;
2520 p.columns = q.columns.clone();
2521 p.autofit = q.autofit;
2522 p.style = q.style.clone();
2523 }
2524 for o in self.b.outputs.iter().filter(|o| o.format == "xlsx") {
2525 let mut seen: BTreeMap<String, String> = BTreeMap::new();
2526 for p in self.produced.iter().filter(|p| o.feeds(&p.query)) {
2527 check_sheet_name(&p.name)?;
2528 if let Some(prev) = seen.insert(p.name.to_lowercase(), p.query.clone()) {
2529 return Err(format!(
2530 "sheet name `{}` is used twice (by `{prev}` and `{}`); Excel sheet names must be unique",
2531 p.name, p.query
2532 ).into());
2533 }
2534 }
2535 }
2536 Ok(())
2537 }
2538
2539 fn plan(&mut self) -> BindingPlan {
2541 let compiled = self
2542 .b
2543 .queries
2544 .iter()
2545 .map(|q| {
2546 rel(
2547 &self.project.root,
2548 &self.compiled_dir.join(format!("{}.sql", q.query)),
2549 )
2550 })
2551 .collect();
2552 let mut outputs = Vec::new();
2553 for oi in 0..self.outs.len() {
2554 let file = self.file_names(oi);
2555 let o = &self.b.outputs[oi];
2556 let destinations = self.outs[oi]
2557 .dests
2558 .iter()
2559 .map(|d| {
2560 let out = self.project.profiles.target(Role::Destination, &d.profile);
2561 PlannedDestination {
2562 profile: d.profile.clone(),
2563 kind: out.map(|o| o.kind.clone()),
2564 target: Some(self.project.profiles.target_of(Role::Destination, &d.profile)),
2565 path: d.path.clone(),
2566 delivers: out.is_some(),
2567 }
2568 })
2569 .collect();
2570 outputs.push(PlannedOutput {
2571 name: o.name.clone(),
2572 format: o.format.clone(),
2573 output: rel(&self.project.root, &self.run_dir.join(file)),
2574 destinations,
2575 });
2576 }
2577 let first = outputs.first().cloned();
2578 let pool = self.pool.clone().expect("pool");
2579 let queries = self
2580 .parsed
2581 .queries
2582 .iter()
2583 .map(|q| {
2584 let connection = q.connection.clone().unwrap_or_default();
2585 PlannedQuery {
2586 query: q.query.clone(),
2587 kind: pool.kind(&connection).unwrap_or_default(),
2588 target: self.project.profiles.target_of(Role::Connection, &connection),
2589 connection,
2590 sources: q.sources.clone(),
2591 }
2592 })
2593 .collect();
2594 BindingPlan {
2595 report: self.report.name.clone(),
2596 set: self.b.set.clone(),
2597 compiled,
2598 profile: self.parsed.inherited.clone(),
2599 queries,
2600 target: self.target.clone(),
2601 format: first.as_ref().map(|o| o.format.clone()).unwrap_or_default(),
2602 output: first.as_ref().map(|o| o.output.clone()).unwrap_or_default(),
2603 destinations: first.map(|o| o.destinations).unwrap_or_default(),
2604 outputs,
2605 schedules: self.b.schedules.clone(),
2606 }
2607 }
2608
2609 fn file_names(&mut self, oi: usize) -> String {
2614 let first = oi == 0;
2615 let (output_path, output_name) = if first {
2616 (self.opts.output_path.clone(), self.opts.output_name.clone())
2617 } else {
2618 (None, None)
2619 };
2620 for d in self.outs[oi].dests.iter_mut().filter(|d| d.path.is_some()) {
2621 if let Some(p) = &output_path {
2622 d.path = Some(p.clone());
2623 }
2624 if let (Some(n), Some(r)) = (&output_name, d.path.as_mut()) {
2625 *r = match r.rfind(['/', '\\']) {
2626 Some(i) => format!("{}{n}", &r[..=i]),
2627 None => n.clone(),
2628 };
2629 }
2630 }
2631 let o = &self.b.outputs[oi];
2632 let ext = match &o.extension {
2633 Some(e) => e.as_str(),
2634 None => extension(&o.format),
2635 };
2636 let stem = o.name.clone().unwrap_or_else(|| self.report.name.clone());
2637 let from_remote = self.outs[oi]
2638 .dests
2639 .iter()
2640 .find_map(|d| d.path.as_deref())
2641 .and_then(|r| r.rsplit(['/', '\\']).next())
2642 .filter(|s| !s.is_empty())
2643 .map(str::to_string);
2644 output_name.or(from_remote).unwrap_or_else(|| match ext {
2645 "" => stem,
2646 ext => format!("{stem}.{ext}"),
2647 })
2648 }
2649
2650 fn format(&mut self, oi: usize, filename: &str) -> Result<(), Fail> {
2652 self.not_cancelled()?;
2653 std::fs::create_dir_all(&self.run_dir).map_err(|e| e.to_string())?;
2654 let b = self.b;
2655 let out = &b.outputs[oi];
2656 let mine: Vec<usize> = (0..self.produced.len())
2657 .filter(|&i| {
2658 out.feeds(&self.produced[i].query) && !self.outs[oi].dropped.contains(&self.produced[i].query)
2659 })
2660 .collect();
2661 if mine.is_empty() {
2662 let what = if self.outs.len() > 1 {
2663 format!("{} gets no tab", out.label(oi))
2664 } else {
2665 "no query makes a tab (every query has `tab: false`)".to_string()
2666 };
2667 self.ui.warn(&format!(" {what}; no output file was written"));
2668 return Ok(());
2669 }
2670 let multi = out.format == "xlsx" || out.template.is_some() || !is_single_table(&out.format);
2671 let groups: Vec<(String, Vec<usize>)> = if multi || mine.len() == 1 {
2672 vec![(filename.to_string(), mine)]
2673 } else {
2674 let (stem, ext) = split_ext(filename);
2675 let mut g = Vec::new();
2676 for i in mine {
2677 let r = &self.produced[i];
2678 if r.name.contains(['/', '\\']) {
2679 return Err(format!("`{}` can't be used in a file name", r.name).into());
2680 }
2681 g.push((format!("{stem}_{}{ext}", r.name), vec![i]));
2682 }
2683 g
2684 };
2685 for (name, _) in &groups {
2687 let path = self.run_dir.join(name);
2688 if let Some(prev) = (0..oi).find(|&j| self.outs[j].files.iter().any(|(f, _)| f == &path)) {
2689 return Err(format!(
2690 "{} and {} both write {name}; give them different `name:`s or destination paths",
2691 self.b.outputs[prev].label(prev),
2692 out.label(oi)
2693 )
2694 .into());
2695 }
2696 }
2697 let plugin = find_plugin(self.project, PluginKind::Format, &out.format)
2698 .map_err(|e| Fail::new(Code::FormatFailed, e.to_string()))?;
2699 let mut p = plugin
2700 .start(self.ui.plugin_log(), Some(&self.project.root))
2701 .map_err(|e| e.to_string())?;
2702 let template = match &out.template {
2703 _ if crate::document::FORMATS.contains(&out.format.as_str()) => {
2704 Some(self.document_payload(out.template.as_ref())?)
2705 }
2706 Some(t) => Some(self.template_payload(t)?),
2707 None => None,
2708 };
2709 for (name, idxs) in groups {
2710 let t = Instant::now();
2711 let path = self.run_dir.join(&name);
2712 let metas: Vec<ResultSetMeta> = idxs
2713 .iter()
2714 .map(|&i| {
2715 let r = &self.produced[i];
2716 ResultSetMeta {
2717 name: r.name.clone(),
2718 query: r.query.clone(),
2719 result_index: 1,
2720 anchor: r.anchor.clone(),
2721 header: r.header,
2722 columns: r.columns.clone(),
2723 autofit: r.autofit,
2724 style: r.style.clone(),
2725 }
2726 })
2727 .collect();
2728 p.write_begin(
2729 &path.to_string_lossy(),
2730 &out.format,
2731 out.options.clone(),
2732 metas,
2733 template.clone(),
2734 )
2735 .map_err(|e| e.to_string())?;
2736 for &i in &idxs {
2737 let r = &self.produced[i];
2738 let reader = FileReader::try_new(File::open(&r.spool).map_err(|e| e.to_string())?, None)
2739 .map_err(|e| e.to_string())?;
2740 let schema = r.schema.clone();
2741 let mut any = false;
2743 for b in reader {
2744 let b = b.map_err(|e| e.to_string())?;
2745 any = true;
2746 p.send_batch(&b)
2747 .map_err(|e| format!("{} format: {e}", out.format))?;
2748 }
2749 if !any {
2750 p.send_batch(&RecordBatch::new_empty(schema))
2751 .map_err(|e| format!("{} format: {e}", out.format))?;
2752 }
2753 p.send(&dre_protocol::msg::Request::ResultSetEnd {})
2754 .map_err(|e| format!("{} format: {e}", out.format))?;
2755 }
2756 let (files, warnings) = p
2757 .write_finish_with_warnings()
2758 .map_err(|e| Fail::from_plugin(&e, format!("{} format: {e}", out.format)))?;
2759 for w in warnings {
2760 self.ui.warn(&format!(" {} format: {w}", out.format));
2761 }
2762 for f in files {
2763 let f = PathBuf::from(f);
2764 let size = std::fs::metadata(&f).map(|m| m.len()).unwrap_or(0);
2765 let shown = rel(&self.project.root, &f);
2766 self.ui.step(
2767 Level::Debug,
2768 "Wrote",
2769 &format!("{} ({})", shown.display(), human_bytes(size)),
2770 Some(t.elapsed()),
2771 );
2772 self.outs[oi].files.push((f, None));
2773 }
2774 }
2775 let _ = p.close();
2776 Ok(())
2777 }
2778
2779 fn template_payload(&mut self, t: &crate::project::Template) -> Result<Json, Fail> {
2780 let file = crate::project::resolve_template(&self.project.root, &self.report.file, &t.file)?;
2781 let renderer = self.renderer(None)?;
2782 let mut values = JsonMap::new();
2783 for b in &t.bindings {
2784 if let (Some(cell), Some(v)) = (&b.cell, &b.value) {
2785 let rendered = renderer
2786 .render(&self.report.file, v)
2787 .map_err(|e| format!("template value: {e}"))?;
2788 values.insert(format!("{}!{}", b.sheet, cell), Json::String(rendered));
2789 }
2790 }
2791 Ok(json!({"file": file.to_string_lossy(), "bindings": t.bindings, "values": values}))
2792 }
2793
2794 fn document_payload(&mut self, t: Option<&crate::project::Template>) -> Result<Json, Fail> {
2797 let file = match t {
2798 Some(t) => Some(crate::project::resolve_template(
2799 &self.project.root,
2800 &self.report.file,
2801 &t.file,
2802 )?),
2803 None => None,
2804 };
2805 let run =
2806 crate::parse::run_context(self.project, self.report, self.b, &self.inputs(), self.started_at);
2807 let ctx = crate::document::Context::new(
2808 file.as_deref(),
2809 &self.project.root,
2810 &run,
2811 self.vars.clone(),
2812 self.opts.vars.clone(),
2813 );
2814 Ok(serde_json::to_value(ctx).map_err(|e| e.to_string())?)
2815 }
2816
2817 fn deliver(&mut self, oi: usize) -> Result<(), Fail> {
2820 let result = self.deliver_output(oi);
2821 let o = &mut self.outs[oi];
2822 o.status = Some(match &result {
2823 Err(_) => OutputStatus::Failed,
2824 Ok(()) if o.deliveries.iter().any(|d| d.status == "delivered") => OutputStatus::Delivered,
2825 Ok(()) if o.deliveries.iter().any(|d| d.status == "held") => OutputStatus::Held,
2826 Ok(()) => OutputStatus::Kept,
2827 });
2828 o.error = result.as_ref().err().map(|f| f.message.clone());
2829 result
2830 }
2831
2832 fn deliver_output(&mut self, oi: usize) -> Result<(), Fail> {
2833 if self.outs[oi].dests.is_empty() {
2834 self.outs[oi].delivery_note = Some("no destination declared; output stays in target/".into());
2835 let dir = rel(&self.project.root, &self.run_dir);
2836 self.ui.step(
2837 Level::Debug,
2838 "Kept",
2839 &format!("no destination declared: output stays in {}", dir.display()),
2840 None,
2841 );
2842 return Ok(());
2843 }
2844 if self.outs[oi].files.is_empty() {
2845 return Ok(());
2846 }
2847 let dests = std::mem::take(&mut self.outs[oi].dests);
2848 let mut failures = Vec::new();
2849 let mut nowhere = Vec::new();
2850 for d in &dests {
2851 let profiles = &self.project.profiles;
2852 let target = profiles.target_of(Role::Destination, &d.profile);
2853 let kind = profiles
2855 .target(Role::Destination, &d.profile)
2856 .map(|o| o.kind.clone());
2857 self.attempts = 1;
2858 if d.hold || self.opts.hold {
2859 self.hold(oi, d);
2860 self.outs[oi].deliveries.push(Delivery {
2861 profile: d.profile.clone(),
2862 kind,
2863 target,
2864 status: "held",
2865 location: None,
2866 attempts: None,
2867 error_code: None,
2868 error: None,
2869 });
2870 continue;
2871 }
2872 let (status, location, error) =
2873 match self.deliver_one(oi, d).map_err(|e| e.or(Code::DeliveryFailed)) {
2874 Ok(Some(loc)) => ("delivered", Some(loc), None),
2875 Ok(None) => {
2876 nowhere.push(nowhere_note(&d.profile, &target));
2877 ("not_delivered", None, None)
2878 }
2879 Err(e) => {
2880 failures.push(e.clone());
2881 ("failed", None, Some(e))
2882 }
2883 };
2884 let record = Delivery {
2885 profile: d.profile.clone(),
2886 kind,
2887 target,
2888 status,
2889 location,
2890 attempts: Some(self.attempts).filter(|n| *n > 1),
2891 error_code: error.as_ref().map(|e| e.code.clone()),
2892 error: error.map(|e| e.message),
2893 };
2894 self.outs[oi].deliveries.push(record);
2895 }
2896 let o = &mut self.outs[oi];
2897 o.dests = dests;
2898 if o.files.iter().all(|(_, d)| d.is_none()) && !nowhere.is_empty() {
2899 o.delivery_note = Some(nowhere.join("; "));
2900 }
2901 match failures.len() {
2902 0 => Ok(()),
2903 1 if o.dests.len() == 1 => Err(failures.remove(0)),
2904 n => Err(Fail::new(
2905 failures[0].code.clone(),
2906 format!(
2907 "{n} of {} destinations failed: {}",
2908 o.dests.len(),
2909 failures
2910 .iter()
2911 .map(|f| f.message.as_str())
2912 .collect::<Vec<_>>()
2913 .join("; ")
2914 ),
2915 )),
2916 }
2917 }
2918
2919 fn hold(&mut self, oi: usize, d: &RenderedDest) {
2921 let attach: Vec<crate::release::HeldFile> = d
2922 .attach
2923 .iter()
2924 .filter_map(|name| {
2925 self.b
2926 .outputs
2927 .iter()
2928 .position(|o| o.name.as_deref() == Some(name))
2929 })
2930 .flat_map(|j| {
2931 self.outs[j]
2932 .files
2933 .iter()
2934 .map(|(f, _)| crate::release::held_file(f))
2935 })
2936 .collect();
2937 let idx = self.outs[oi].deliveries.len();
2938 self.held.push(crate::release::HeldEntry {
2939 id: format!(
2940 "{}/{idx}",
2941 self.b.outputs[oi]
2942 .name
2943 .clone()
2944 .unwrap_or_else(|| format!("output{}", oi + 1))
2945 ),
2946 output: self.b.outputs[oi].name.clone(),
2947 key: None,
2948 profile: d.profile.clone(),
2949 path: d.path.clone(),
2950 options: d.options.clone(),
2951 attach,
2952 rate: d.rate,
2953 message: self.outs[oi].message.clone(),
2954 files: self.outs[oi]
2955 .files
2956 .iter()
2957 .map(|(f, _)| crate::release::HeldFile {
2958 path: f.clone(),
2959 sha256: crate::release::sha256_file(f).unwrap_or_default(),
2960 })
2961 .collect(),
2962 });
2963 self.ui.step(
2964 Level::Info,
2965 "Held",
2966 &format!(
2967 "{} to `{}`: held for `dre release`",
2968 self.b.outputs[oi].label(oi),
2969 d.profile
2970 ),
2971 None,
2972 );
2973 }
2974
2975 fn deliver_one(&mut self, oi: usize, d: &RenderedDest) -> Result<Option<String>, Fail> {
2978 self.not_cancelled()?;
2980 let profiles = &self.project.profiles;
2981 let out = match profiles.entry(Role::Destination, &d.profile) {
2982 Entry::Use(o) => o,
2983 Entry::Nowhere => {
2984 let note = nowhere_note(&d.profile, &profiles.target_of(Role::Destination, &d.profile));
2985 self.ui.step(Level::Info, "Kept", ¬e, None);
2986 return Ok(None);
2987 }
2988 Entry::Missing => {
2990 return Err(Fail::new(
2991 Code::MissingTargetEntry,
2992 profiles.missing_entry(Role::Destination, &d.profile),
2993 ));
2994 }
2995 Entry::Unknown => {
2996 return Err(format!("destination profile `{}` isn't in profiles.yml", d.profile).into());
2997 }
2998 };
2999 let kind = out.kind.clone();
3000 let connection = render_connection(out)?;
3001 let files = &self.outs[oi].files;
3002 let targets: Vec<DeliveryFile> = files
3003 .iter()
3004 .map(|(f, _)| {
3005 let name = f.file_name().unwrap().to_string_lossy().to_string();
3006 let r = d.path.as_deref().map(|r| {
3007 if files.len() == 1 {
3008 r.to_string()
3009 } else {
3010 match r.rfind(['/', '\\']) {
3011 Some(i) => format!("{}{name}", &r[..=i]),
3012 None => name.clone(),
3013 }
3014 }
3015 });
3016 DeliveryFile {
3017 local_path: f.to_string_lossy().to_string(),
3018 remote_path: r,
3019 }
3020 })
3021 .collect();
3022 for t in &targets {
3024 if let Some(r) = &t.remote_path
3025 && let Some(other) = self.store.claim(&d.profile, r, &self.label())
3026 {
3027 return Err(Fail::new(
3028 Code::DeliveryFailed,
3029 format!(
3030 "`{other}` delivers to `{r}` on destination `{}` in this run too; Bindings running at once (`threads:`) need different paths (put the Set or a var in the path)",
3031 d.profile
3032 ),
3033 ));
3034 }
3035 }
3036 let mut locations = Vec::new();
3037 if kind == LOCAL_TYPE {
3038 let errors = crate::options::local_option_errors(&d.options);
3039 if !errors.is_empty() {
3040 return Err(format!("destination `{}`: {}", d.profile, errors.join("; ")).into());
3041 }
3042 let (rules, _) = dre_protocol::delivery::Rules::from_settings(
3045 dre_protocol::delivery::Rules {
3046 retries: 0,
3047 ..Default::default()
3048 },
3049 &JsonMap::new(),
3050 &d.options,
3051 &[],
3052 )?;
3053 for (i, f) in targets.iter().enumerate() {
3054 let t = Instant::now();
3055 let r = f
3056 .remote_path
3057 .as_ref()
3058 .ok_or("the local destination needs `output.destination.path`")?;
3059 let dst = self.project.root.join(r).to_string_lossy().to_string();
3060 let delivered = dre_protocol::delivery::deliver(
3061 &mut dre_protocol::delivery::LocalStore,
3062 Path::new(&f.local_path),
3063 &dst,
3064 &rules,
3065 )
3066 .map_err(|e| {
3067 let message = format!("delivery to {dst} failed: {e}; the output is still in target/");
3068 if e.exists {
3069 let code = crate::codes::ErrorCode::Plugin {
3071 code: "local/file-exists".into(),
3072 kind: crate::codes::Kind::Delivery,
3073 };
3074 Fail::new(code, message)
3075 } else {
3076 Fail::new(Code::DeliveryFailed, message)
3077 }
3078 })?;
3079 let loc = delivered.path;
3080 self.ui.step(Level::Debug, "Delivered", &loc, Some(t.elapsed()));
3081 self.outs[oi].files[i].1.get_or_insert_with(|| loc.clone());
3082 locations.push(loc);
3083 }
3084 return Ok(Some(locations.join(", ")));
3085 }
3086 let failed = |e: dre_protocol::host::HostError| {
3087 Fail::from_plugin(
3088 &e,
3089 format!("delivery through `{kind}` failed: {e}; the output is still in target/"),
3090 )
3091 };
3092 let plugin = find_plugin(self.project, PluginKind::Destination, &kind)
3093 .map_err(|e| Fail::new(Code::DeliveryFailed, e.to_string()))?;
3094 let mut p = plugin
3095 .start(self.ui.plugin_log(), Some(&self.project.root))
3096 .map_err(|e| e.to_string())?;
3097 let is_message = self.b.outputs[oi].is_message();
3098 if is_message
3101 && p.has(CAP_MESSAGE)
3102 && let Some((title, text)) = self.outs[oi].message.clone()
3103 {
3104 let t = Instant::now();
3105 let message = dre_protocol::msg::Message {
3106 title,
3107 text,
3108 html: None,
3109 path: targets[0].local_path.clone(),
3110 };
3111 let attach: Vec<DeliveryFile> = d
3112 .attach
3113 .iter()
3114 .filter_map(|name| {
3115 self.b
3116 .outputs
3117 .iter()
3118 .position(|o| o.name.as_deref() == Some(name))
3119 })
3120 .flat_map(|j| self.outs[j].files.iter())
3121 .map(|(f, _)| DeliveryFile {
3122 local_path: f.to_string_lossy().to_string(),
3123 remote_path: None,
3124 })
3125 .collect();
3126 let loc = p
3127 .deliver_message(&message, &attach, connection, d.options.clone())
3128 .map_err(failed)?;
3129 self.attempts = p.last_attempts();
3130 self.ui.step(Level::Debug, "Delivered", &loc, Some(t.elapsed()));
3131 self.outs[oi].files[0].1.get_or_insert_with(|| loc.clone());
3132 let _ = p.close();
3133 return Ok(Some(loc));
3134 }
3135 if !is_message && p.has(CAP_MESSAGE_ONLY) {
3136 let _ = p.close();
3137 return Err(format!(
3138 "`{kind}` only takes messages, but this output is `{}`; deliver the file elsewhere and link it from a message",
3139 self.b.outputs[oi].format
3140 ).into());
3141 }
3142 let batches: Vec<Vec<usize>> = if targets.len() > 1 && p.has(CAP_MULTI_FILE) {
3143 vec![(0..targets.len()).collect()]
3144 } else {
3145 (0..targets.len()).map(|i| vec![i]).collect()
3146 };
3147 for batch in batches {
3148 let t = Instant::now();
3149 let files: Vec<DeliveryFile> = batch.iter().map(|&i| targets[i].clone()).collect();
3150 let loc = p
3151 .deliver_files(&files, connection.clone(), d.options.clone())
3152 .map_err(failed)?;
3153 self.attempts = self.attempts.max(p.last_attempts());
3154 self.ui.step(Level::Debug, "Delivered", &loc, Some(t.elapsed()));
3155 for i in batch {
3156 self.outs[oi].files[i].1.get_or_insert_with(|| loc.clone());
3157 }
3158 locations.push(loc);
3159 }
3160 let _ = p.close();
3161 Ok(Some(locations.join(", ")))
3162 }
3163
3164 fn snapshot(&self) -> Json {
3165 let sets: Vec<Json> = self
3166 .produced
3167 .iter()
3168 .map(|p| {
3169 json!({
3170 "name": p.name,
3171 "columns": p.schema.fields().iter().map(|f| json!({
3172 "name": f.name(),
3173 "type": f.data_type().to_string(),
3174 "type_shape": crate::schema::type_shape(f.data_type()),
3175 })).collect::<Vec<_>>(),
3176 })
3177 })
3178 .collect();
3179 json!({ "result_sets": sets })
3180 }
3181
3182 fn schema_drift(&mut self) -> Vec<String> {
3183 let path = self.schema_dir.join("last_success.json");
3184 let mut current = self.snapshot();
3185 let Ok(prev) = std::fs::read_to_string(&path) else {
3186 crate::schema::redact(&mut current);
3187 self.protected_snapshot = Some(current);
3188 return Vec::new();
3189 };
3190 let Ok(mut prev) = serde_json::from_str::<Json>(&prev) else {
3191 crate::schema::redact(&mut current);
3192 self.protected_snapshot = Some(current);
3193 return Vec::new();
3194 };
3195 let original = prev.clone();
3196 crate::schema::redact(&mut prev);
3197 crate::schema::redact(&mut current);
3198 if crate::secrets::enabled() {
3199 crate::schema::align_protection(&mut prev, &mut current);
3200 }
3201 if prev != original {
3202 let protected =
3203 serde_json::to_string_pretty(&prev).expect("serializing a serde_json::Value cannot fail");
3204 if let Err(error) = crate::fs::write_atomic(&path, protected.as_bytes()) {
3205 self.ui.warn(&format!(
3206 "can't protect the existing schema snapshot at {}: {error}; continuing without rewriting it",
3207 path.display()
3208 ));
3209 }
3210 }
3211 let drift = crate::schema::drift(&prev, ¤t);
3212 self.protected_snapshot = Some(current);
3213 drift
3214 }
3215
3216 fn write_snapshot(&self) -> std::io::Result<()> {
3217 std::fs::create_dir_all(&self.schema_dir)?;
3218 let snapshot = self.protected_snapshot.clone().unwrap_or_else(|| {
3219 let mut snapshot = self.snapshot();
3220 crate::schema::redact(&mut snapshot);
3221 snapshot
3222 });
3223 crate::fs::write_atomic(
3224 &self.schema_dir.join("last_success.json"),
3225 serde_json::to_string_pretty(&snapshot)?.as_bytes(),
3226 )
3227 }
3228
3229 fn write_results(&self, status: &Status, error: Option<&Fail>) -> std::io::Result<()> {
3230 std::fs::create_dir_all(&self.run_dir)?;
3231 if !self.held.is_empty() {
3232 crate::release::write_held(&self.run_dir, &self.run_id, &self.held)?;
3233 }
3234 let mut outputs: Vec<run_results::OutputFile> = Vec::new();
3236 let mut output_results: Vec<run_results::OutputResult> = Vec::new();
3237 for (o, run) in self.b.outputs.iter().zip(&self.outs) {
3238 let files: Vec<run_results::OutputFileRef> = run
3239 .files
3240 .iter()
3241 .map(|(f, delivered)| run_results::OutputFileRef {
3242 path: record_path(self.project, f),
3243 size: std::fs::metadata(f).map(|m| m.len()).unwrap_or(0),
3244 delivered_to: delivered.clone(),
3245 })
3246 .collect();
3247 outputs.extend(files.iter().map(|f| run_results::OutputFile {
3248 path: f.path.clone(),
3249 size: f.size,
3250 delivered_to: f.delivered_to.clone(),
3251 output: o.name.clone(),
3252 }));
3253 let queries: Vec<String> = self
3254 .b
3255 .queries
3256 .iter()
3257 .filter(|q| q.tab && o.feeds(&q.query))
3258 .map(|q| q.query.clone())
3259 .collect();
3260 let status = match (run.status, status) {
3261 (Some(s), _) => s,
3262 (None, Status::Error) => OutputStatus::Failed,
3263 (None, _) => OutputStatus::Kept,
3264 }
3265 .as_str();
3266 output_results.push(run_results::OutputResult {
3267 name: o.name.clone(),
3268 format: o.format.clone(),
3269 queries,
3270 status,
3271 error: run.error.clone(),
3272 files,
3273 delivery: run.delivery_note.clone(),
3274 deliveries: run.deliveries.clone(),
3275 when: run.when,
3276 reason: run.reason.clone(),
3277 no_data: run.no_data.clone(),
3278 dropped: run.dropped.clone(),
3279 for_each: run.for_each.clone(),
3280 message: run.message.clone().map(|(title, text)| run_results::Message {
3281 title: crate::secrets::mask_message_title(&title).into_owned(),
3282 text: crate::secrets::mask_markdown(&text).into_owned(),
3283 }),
3284 });
3285 }
3286 let order = (0..self.outs.len())
3288 .filter(|&i| !self.b.outputs[i].is_message())
3289 .chain((0..self.outs.len()).filter(|&i| self.b.outputs[i].is_message()));
3290 let deliveries: Vec<Delivery> = order
3291 .flat_map(|i| self.outs[i].deliveries.iter().cloned())
3292 .collect();
3293 let notes: Vec<&str> = self
3294 .outs
3295 .iter()
3296 .filter_map(|o| o.delivery_note.as_deref())
3297 .collect();
3298 let delivery = (!notes.is_empty()).then(|| notes.join("; "));
3299 let results = RunResults {
3300 schema_version: run_results::SCHEMA_VERSION,
3301 run_id: self.run_id.clone(),
3302 report: self.report.name.clone(),
3303 set: self.b.set.clone(),
3304 binding: self.b.dir_name().to_string(),
3305 managed: self.report.managed,
3306 profile: self.parsed.inherited.clone(),
3307 connections: self
3308 .parsed
3309 .connections()
3310 .into_iter()
3311 .map(str::to_string)
3312 .collect(),
3313 target: self.target.clone(),
3314 schedule: self.opts.schedule.clone(),
3315 schedule_vars: self.schedule_vars.clone(),
3316 vars: self.rendered_vars.clone(),
3317 run_date: self.date.to_string(),
3318 scheduled_at: self.opts.scheduled_at.map(rfc3339),
3319 timezone: self.calendar.tz.name().to_string(),
3320 params: self.opts.params(self.date),
3321 status: *status,
3322 error: error.map(|e| e.message.clone()),
3323 error_code: error.map(|e| e.code.clone()),
3324 error_kind: error.map(|e| e.code.kind()),
3325 preview: self.opts.preview.is_some(),
3326 row_limit: self.opts.preview,
3327 started_at: self.started_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
3328 duration_ms: self.started.elapsed().as_millis() as u64,
3329 result_sets: self
3330 .produced
3331 .iter()
3332 .map(|p| run_results::ResultSet {
3333 name: p.name.clone(),
3334 query: p.query.clone(),
3335 connection: p.connection.clone(),
3336 rows: p.rows,
3337 columns: p.schema.fields().iter().map(|f| f.name().clone()).collect(),
3338 })
3339 .collect(),
3340 outputs,
3341 output_results,
3342 delivery,
3343 deliveries,
3344 schema_drift: self.drift.clone(),
3345 target_path: self.project.target_dir.clone(),
3346 settings: self.project.settings.clone(),
3347 manifest_checksum: self.opts.manifest_checksum.clone(),
3348 retry_of: self.retry.as_ref().map(|r| r.original.clone()),
3349 release_of: None,
3350 released_by: None,
3351 released: Vec::new(),
3352 };
3353 self.store.write_results(&self.run_dir, &results)
3354 }
3355
3356 fn live_check(&mut self, statements: &[Statement]) -> Result<(), Fail> {
3359 let pool = self.pool.clone().expect("pool");
3360 let mut failures = Vec::new();
3361 let mut unexecuted_setup: BTreeMap<String, (PathBuf, usize)> = BTreeMap::new();
3362 let mut uncheckable: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
3363 let sql_log = self.ui.sql_log();
3364 for st in statements {
3365 if uncheckable.contains(&st.connection) {
3366 continue;
3367 }
3368 let session = pool.session(&st.connection)?;
3369 let mut s = session.lock().unwrap();
3370 if !s.get()?.has(dre_protocol::CAP_CHECK) {
3371 let kind = pool.kind(&st.connection).unwrap_or_default();
3372 self.ui.warn(&format!(
3373 " not checkable: the `{kind}` source plugin (connection `{}`) can't check statements without running them",
3374 st.connection
3375 ));
3376 uncheckable.insert(st.connection.clone());
3377 continue;
3378 }
3379 let verb = if st.kind == StatementKind::TempCreate {
3380 ""
3381 } else {
3382 " (check)"
3383 };
3384 sql_log(
3385 &format!("{}:{}{verb}", st.file.display(), st.line),
3386 &protected_sql(&st.text, st.sensitive),
3387 );
3388 if st.kind == StatementKind::TempCreate {
3389 let result = {
3390 let (process, _log_scope) = s.statement(st.sensitive)?;
3391 process.execute(&st.text, None, |_, _| Ok(()))
3392 };
3393 if let Err(e) = result {
3394 failures.push(format!(
3395 "{}:{}: {}",
3396 st.file.display(),
3397 st.line,
3398 masked_source_error(&e, st.sensitive)
3399 ));
3400 }
3401 continue;
3402 }
3403 let result = {
3404 let (process, _log_scope) = s.statement(st.sensitive)?;
3405 process.check(&st.text)
3406 };
3407 if let Err(e) = result {
3408 let mut msg = format!(
3409 "{}:{}: {}",
3410 st.file.display(),
3411 st.line,
3412 masked_source_error(&e, st.sensitive)
3413 );
3414 if let Some((f, l)) = unexecuted_setup.get(&st.connection) {
3415 msg.push_str(&format!(
3416 " (may be a false positive: the setup statement at {}:{l} wasn't executed during the check)",
3417 f.display()
3418 ));
3419 }
3420 failures.push(msg);
3421 }
3422 if st.kind == StatementKind::Other {
3423 unexecuted_setup.insert(st.connection.clone(), (st.file.clone(), st.line));
3424 }
3425 }
3426 self.check_source_columns(&pool, &mut failures);
3427 if failures.is_empty() {
3428 Ok(())
3429 } else {
3430 Err(Fail::new(Code::QueryFailed, failures.join("\n ")))
3431 }
3432 }
3433
3434 fn check_source_columns(&mut self, pool: &Arc<Pool>, failures: &mut Vec<String>) {
3437 type Checked = std::collections::BTreeSet<(PathBuf, String, String, String)>;
3439 static CHECKED: std::sync::LazyLock<Mutex<Checked>> = std::sync::LazyLock::new(Mutex::default);
3440 for q in self.parsed.queries.clone() {
3441 let Some(conn) = q.connection.clone() else {
3442 continue;
3443 };
3444 for key in &q.sources {
3445 let Some(r) = self.parsed.sources.get(key).cloned() else {
3446 continue;
3447 };
3448 let Some(declared) = self
3449 .project
3450 .sources
3451 .get(&r.source)
3452 .and_then(|s| s.table(&r.table))
3453 .map(|t| t.columns.clone())
3454 .filter(|c| !c.is_empty())
3455 else {
3456 continue;
3457 };
3458 let id = (
3459 self.project.root.clone(),
3460 key.clone(),
3461 conn.clone(),
3462 self.project.profiles.target_of(Role::Connection, &conn),
3463 );
3464 if !CHECKED.lock().unwrap().insert(id) {
3465 continue;
3466 }
3467 let quote = if r.quoting.any() {
3468 let kind = pool.kind(&conn).unwrap_or_default();
3469 match pool.connections.identifier_quote(&kind) {
3470 Ok(q) => q,
3471 Err(e) => {
3472 failures.push(format!("source `{key}`: can't quote its name: {e}"));
3473 continue;
3474 }
3475 }
3476 } else {
3477 None
3478 };
3479 let rel = r.relation(quote.as_deref());
3480 let runner = PoolRunner {
3481 pool: pool.clone(),
3482 default: Some(conn.clone()),
3483 log: self.ui.sql_log(),
3484 };
3485 let actual = match runner.columns(&format!("select * from {rel} where 1=0"), None) {
3486 Ok(c) => c,
3487 Err(e) => {
3488 failures.push(format!("source `{key}` ({rel} on connection `{conn}`): {e}"));
3489 continue;
3490 }
3491 };
3492 for c in &declared {
3493 let Some(a) = actual.iter().find(|a| a.name.eq_ignore_ascii_case(&c.name)) else {
3494 failures.push(format!(
3495 "source `{key}`: declared column `{}` isn't in {rel} on connection `{conn}` (it has: {})",
3496 c.name,
3497 actual.iter().map(|a| a.name.as_str()).collect::<Vec<_>>().join(", ")
3498 ));
3499 continue;
3500 };
3501 let Some(dt) = &c.data_type else { continue };
3502 match crate::coltypes::matches(dt, &a.data_type) {
3503 Some(true) => {}
3504 Some(false) => failures.push(format!(
3505 "source `{key}`: column `{}` is declared `{dt}`, but {rel} on connection `{conn}` returns {}",
3506 c.name, a.data_type
3507 )),
3508 None => self.ui.warn(&format!(
3509 " source `{key}`: column `{}`'s `data_type: {dt}` isn't a type DRE can compare, so only its presence was checked",
3510 c.name
3511 )),
3512 }
3513 }
3514 }
3515 }
3516 }
3517}
3518
3519struct Session {
3521 path: Result<crate::plugins::Located, String>,
3522 connection: Result<JsonMap<String, Json>, String>,
3523 cwd: PathBuf,
3524 log: LogSink,
3525 opaque_log: Arc<AtomicBool>,
3528 unmanaged: bool,
3530 read_only: bool,
3531 loaded: bool,
3533 proc_: Option<PluginProcess>,
3534}
3535
3536impl Session {
3537 fn new(
3538 path: Result<crate::plugins::Located, String>,
3539 connection: Result<JsonMap<String, Json>, String>,
3540 cwd: PathBuf,
3541 log: LogSink,
3542 unmanaged: bool,
3543 ) -> Session {
3544 let opaque_log = Arc::new(AtomicBool::new(false));
3545 let protect = Arc::clone(&opaque_log);
3546 let downstream = log;
3547 let log: LogSink = Arc::new(move |plugin, line| {
3548 let line = crate::secrets::mask_source_error(line, protect.load(Ordering::SeqCst));
3549 downstream(plugin, &line);
3550 });
3551 Session {
3552 path,
3553 connection,
3554 cwd,
3555 log,
3556 opaque_log,
3557 unmanaged,
3558 read_only: unmanaged,
3559 loaded: false,
3560 proc_: None,
3561 }
3562 }
3563
3564 fn want_read_only(&mut self, ro: bool) {
3565 if self.read_only != ro {
3566 self.close();
3568 self.read_only = ro;
3569 }
3570 }
3571
3572 fn get(&mut self) -> Result<&mut PluginProcess, String> {
3573 if self.proc_.is_none() {
3574 let plugin = self.path.clone()?;
3575 let connection = self.connection.clone()?;
3576 let mut p = plugin
3577 .start(self.log.clone(), Some(&self.cwd))
3578 .map_err(|e| masked_source_error(&e, false))?;
3579 let ro = self.unmanaged && self.read_only && p.has(CAP_READ_ONLY);
3580 p.open(connection, ro).map_err(|e| {
3581 let protected = masked_source_error(&e, false);
3582 format!("can't open the source connection: {protected}")
3583 })?;
3584 self.proc_ = Some(p);
3585 }
3586 Ok(self.proc_.as_mut().unwrap())
3587 }
3588
3589 fn statement(&mut self, sensitive: bool) -> Result<(&mut PluginProcess, OpaqueLogScope), String> {
3590 let opaque_log = Arc::clone(&self.opaque_log);
3591 let process = self.get()?;
3592 let scope = OpaqueLogScope::new(opaque_log, sensitive);
3593 Ok((process, scope))
3594 }
3595
3596 fn close(&mut self) {
3597 if let Some(p) = self.proc_.take() {
3598 let _ = p.close();
3599 }
3600 }
3601}
3602
3603struct OpaqueLogScope(Arc<AtomicBool>);
3604
3605impl OpaqueLogScope {
3606 fn new(active: Arc<AtomicBool>, sensitive: bool) -> Self {
3607 active.store(sensitive, Ordering::SeqCst);
3608 Self(active)
3609 }
3610}
3611
3612impl Drop for OpaqueLogScope {
3613 fn drop(&mut self) {
3614 self.0.store(false, Ordering::SeqCst);
3615 }
3616}
3617
3618fn nowhere_note(profile: &str, target: &str) -> String {
3620 format!(
3621 "destination `{profile}`: `{target}` delivers nowhere (`deliver: false`); output stays in target/"
3622 )
3623}
3624
3625struct Pool {
3628 connections: Arc<ProfileConnections>,
3629 cwd: PathBuf,
3630 log: LogSink,
3631 unmanaged: bool,
3632 sessions: Mutex<BTreeMap<String, Arc<Mutex<Session>>>>,
3633}
3634
3635impl Pool {
3636 fn new(connections: Arc<ProfileConnections>, cwd: PathBuf, log: LogSink, unmanaged: bool) -> Pool {
3637 Pool {
3638 connections,
3639 cwd,
3640 log,
3641 unmanaged,
3642 sessions: Mutex::default(),
3643 }
3644 }
3645
3646 fn target(&self, name: &str) -> Result<ProfileTarget, String> {
3648 let profiles = &self.connections.profiles;
3649 match profiles.entry(Role::Connection, name) {
3650 Entry::Use(o) => Ok(o.clone()),
3651 Entry::Unknown => Err(format!(
3652 "connection `{name}` isn't under `{}:` in {}",
3653 profiles.section_key(Role::Connection),
3654 profiles.path.display()
3655 )),
3656 Entry::Missing | Entry::Nowhere => Err(profiles.missing_entry(Role::Connection, name)),
3658 }
3659 }
3660
3661 fn kind(&self, name: &str) -> Result<String, String> {
3663 Ok(self.target(name)?.kind)
3664 }
3665
3666 fn session(&self, name: &str) -> Result<Arc<Mutex<Session>>, String> {
3667 if let Some(s) = self.sessions.lock().unwrap().get(name) {
3668 return Ok(s.clone());
3669 }
3670 let output = self.target(name)?;
3671 let id = crate::project::PluginId::new(PluginKind::Source, output.kind.clone());
3672 let path = crate::plugins::locate_in(&self.connections.root, &self.connections.plugins, &id)
3673 .map_err(String::from);
3674 let session = Arc::new(Mutex::new(Session::new(
3675 path,
3676 render_connection(&output),
3677 self.cwd.clone(),
3678 self.log.clone(),
3679 self.unmanaged,
3680 )));
3681 self.sessions
3682 .lock()
3683 .unwrap()
3684 .insert(name.to_string(), session.clone());
3685 Ok(session)
3686 }
3687
3688 fn close_all(&self) {
3689 for s in self.sessions.lock().unwrap().values() {
3690 if let Ok(mut s) = s.lock() {
3691 s.close();
3692 }
3693 }
3694 }
3695}
3696
3697struct PoolRunner {
3700 pool: Arc<Pool>,
3701 default: Option<String>,
3702 log: LogSink,
3703}
3704
3705impl PoolRunner {
3706 fn session(&self, profile: Option<&str>) -> Result<Arc<Mutex<Session>>, String> {
3707 let name = profile.or(self.default.as_deref()).ok_or(
3708 "no connection here: pass `profile=`, or give the report a `profile:` (this isn't a query's SQL)",
3709 )?;
3710 self.pool.session(name)
3711 }
3712}
3713
3714impl QueryRunner for PoolRunner {
3715 fn run_query(&self, sql: &str, max_rows: u64, profile: Option<&str>) -> Result<QueryRows, String> {
3716 let session = self.session(profile)?;
3717 let mut s = session.lock().map_err(|_| "session lock poisoned".to_string())?;
3718 let sensitive = crate::secrets::contains_secret(sql);
3719 if s.unmanaged
3722 && let Some(bad) = sqlsplit::split(sql)
3723 .into_iter()
3724 .find(|st| sqlsplit::classify(&st.text) != StatementKind::Read)
3725 {
3726 return Err(format!(
3727 "run_query() in an unmanaged report may only read, but got `{}`; give the report a YAML to declare it",
3728 masked_sql_summary(&bad.text, 60, sensitive)
3729 ));
3730 }
3731 (self.log)("run_query()", &protected_sql(sql, sensitive));
3732 let (p, _log_scope) = s.statement(sensitive)?;
3733 let mut out = QueryRows::default();
3734 let mut too_many = false;
3735 let exec = p
3736 .execute(sql, Some(max_rows + 1), |schema, batch| {
3737 if out.columns.is_empty() {
3738 out.columns = schema.fields().iter().map(|f| f.name().clone()).collect();
3739 }
3740 if out.rows.len() as u64 + batch.num_rows() as u64 > max_rows {
3741 too_many = true;
3742 return Ok(());
3743 }
3744 out.rows.extend(crate::values::batch_rows(&batch));
3745 Ok(())
3746 })
3747 .map_err(|e| format!("run_query() failed: {}", masked_source_error(&e, sensitive)))?;
3748 if too_many {
3749 return Err(format!(
3750 "run_query() returned more than {max_rows} rows; it's meant for small lookups — raise the cap with `run_query(sql, max_rows=N)` or `run_query_max_rows` in dre_project.yml"
3751 ));
3752 }
3753 if let Execution::Result { schema, .. } = exec
3754 && out.columns.is_empty()
3755 {
3756 out.columns = schema.fields().iter().map(|f| f.name().clone()).collect();
3757 }
3758 Ok(out)
3759 }
3760
3761 fn columns(&self, sql: &str, profile: Option<&str>) -> Result<Vec<Column>, String> {
3762 let session = self.session(profile)?;
3763 let mut s = session.lock().map_err(|_| "session lock poisoned".to_string())?;
3764 let sensitive = crate::secrets::contains_secret(sql);
3765 (self.log)("columns()", &protected_sql(sql, sensitive));
3766 let (p, _log_scope) = s.statement(sensitive)?;
3767 let mut schema: Option<SchemaRef> = None;
3768 let exec = p
3769 .execute(sql, Some(1), |sch, _| {
3770 schema.get_or_insert_with(|| sch.clone());
3771 Ok(())
3772 })
3773 .map_err(|e| masked_source_error(&e, sensitive))?;
3774 if let Execution::Result { schema: sch, .. } = exec {
3775 schema.get_or_insert(sch);
3776 }
3777 let schema = schema.ok_or("the query returned no result set")?;
3778 Ok(schema
3779 .fields()
3780 .iter()
3781 .map(|f| Column {
3782 name: f.name().clone(),
3783 data_type: f.data_type().to_string(),
3784 })
3785 .collect())
3786 }
3787
3788 fn load(&self, name: &str, table: &Table) -> Result<Option<(String, Option<String>)>, String> {
3789 let session = self.session(None)?;
3790 let mut s = session.lock().map_err(|_| "session lock poisoned".to_string())?;
3791 s.want_read_only(false);
3793 if !s.get()?.has(CAP_LOAD) {
3794 return Ok(None);
3795 }
3796 let batch = table.to_batch()?;
3797 let schema = batch.schema();
3798 (self.log)(
3799 &format!("lookup `{name}`"),
3800 &format!(
3801 "-- {} rows loaded through the plugin's `load` request",
3802 batch.num_rows()
3803 ),
3804 );
3805 let r = s.get()?.load(name, &schema, [batch]).map_err(|e| e.to_string())?;
3806 s.loaded = true;
3807 Ok(Some(r))
3808 }
3809}
3810
3811fn render_json(renderer: &Renderer, file: &Path, v: &Json) -> Result<Json, RenderError> {
3813 Ok(match v {
3814 Json::String(s) => Json::String(renderer.render(file, s)?),
3815 Json::Array(a) => Json::Array(
3816 a.iter()
3817 .map(|v| render_json(renderer, file, v))
3818 .collect::<Result<_, _>>()?,
3819 ),
3820 Json::Object(o) => Json::Object(
3821 o.iter()
3822 .map(|(k, v)| Ok((k.clone(), render_json(renderer, file, v)?)))
3823 .collect::<Result<_, RenderError>>()?,
3824 ),
3825 other => other.clone(),
3826 })
3827}
3828
3829pub(crate) fn render_connection(output: &ProfileTarget) -> Result<JsonMap<String, Json>, String> {
3831 let mut env = minijinja::Environment::new();
3832 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
3833 env.add_function("env_var", |name: String, default: Option<String>| -> Result<String, minijinja::Error> {
3834 std::env::var(&name).ok().or(default).ok_or_else(|| {
3835 minijinja::Error::new(
3836 minijinja::ErrorKind::UndefinedError,
3837 format!("`env_var('{name}')`: environment variable `{name}` is not set and no default is given"),
3838 )
3839 })
3840 });
3841 fn walk(env: &minijinja::Environment<'_>, v: &Json) -> Result<Json, String> {
3842 Ok(match v {
3843 Json::String(s) if crate::preflight::is_templated(s) => Json::String(
3844 env.render_str(s, ())
3845 .map_err(|e| format!("profiles.yml: {}", e.detail().unwrap_or("render error")))?,
3846 ),
3847 Json::Array(a) => Json::Array(a.iter().map(|x| walk(env, x)).collect::<Result<_, _>>()?),
3848 Json::Object(o) => Json::Object(
3849 o.iter()
3850 .map(|(k, x)| Ok((k.clone(), walk(env, x)?)))
3851 .collect::<Result<_, String>>()?,
3852 ),
3853 other => other.clone(),
3854 })
3855 }
3856 match walk(&env, &Json::Object(output.fields.clone()))? {
3857 Json::Object(o) => Ok(o),
3858 _ => unreachable!(),
3859 }
3860}
3861
3862pub fn find_plugin(
3865 project: &Project,
3866 kind: PluginKind,
3867 name: &str,
3868) -> Result<crate::plugins::Located, crate::plugins::LocateError> {
3869 crate::plugins::locate(project, &crate::project::PluginId::new(kind, name))
3870}
3871
3872struct ProfileConnections {
3876 profiles: Profiles,
3878 root: PathBuf,
3879 plugins: Vec<crate::project::PluginRequirement>,
3880 log: LogSink,
3881}
3882
3883static DESCRIBED: std::sync::LazyLock<Mutex<BTreeMap<DescribeKey, Option<Described>>>> =
3886 std::sync::LazyLock::new(Mutex::default);
3887
3888type DescribeKey = (PathBuf, Role, String);
3890
3891#[derive(Clone)]
3892struct Described {
3893 secrets: Vec<String>,
3894 identifier_quote: Option<String>,
3895 string_literal: Option<String>,
3896 capabilities: Vec<String>,
3897 message_limit: Option<u64>,
3899}
3900
3901const SECRET_NAME_WORDS: &[&str] = &["password", "secret", "token", "key", "credential"];
3902
3903impl ProfileConnections {
3904 fn view(&self, role: Role, name: &str) -> Result<Connection, String> {
3905 let target = self.profiles.target_of(role, name);
3906 let out: ProfileTarget = match self.profiles.entry(role, name) {
3907 Entry::Use(o) => o.clone(),
3908 Entry::Nowhere => {
3909 return Err(format!(
3910 "destination `{name}`: `{target}` delivers nowhere (`deliver: false`), so it has no settings"
3911 ));
3912 }
3913 Entry::Missing => return Err(self.profiles.missing_entry(role, name)),
3914 Entry::Unknown => {
3915 return Err(format!(
3916 "no {} profile `{name}` in {}",
3917 role.as_str(),
3918 self.profiles.path.display()
3919 ));
3920 }
3921 };
3922 let mut secrets: Vec<String> = out
3923 .fields
3924 .iter()
3925 .filter(|(_, v)| v.as_str().is_some_and(|s| s.contains(crate::secrets::PREFIX)))
3926 .map(|(k, _)| k.clone())
3927 .collect();
3928 match self.described(role, &out.kind) {
3929 Some(d) => secrets.extend(d.secrets),
3930 None => secrets.extend(
3931 out.fields
3932 .keys()
3933 .filter(|k| {
3934 let k = k.to_lowercase();
3935 SECRET_NAME_WORDS.iter().any(|w| k.contains(w))
3936 })
3937 .cloned(),
3938 ),
3939 }
3940 crate::secrets::register_secret_fields(&out.fields, &secrets);
3941 let fields = render_connection(&out)?;
3942 Ok(Connection {
3943 profile: name.to_string(),
3944 target,
3945 kind: out.kind.clone(),
3946 fields,
3947 secrets,
3948 })
3949 }
3950
3951 fn described(&self, role: Role, kind: &str) -> Option<Described> {
3953 let key = (self.root.clone(), role, kind.to_string());
3954 if let Some(v) = DESCRIBED.lock().unwrap().get(&key) {
3955 return v.clone();
3956 }
3957 let plugin_kind = match role {
3958 Role::Connection => PluginKind::Source,
3959 Role::Destination => PluginKind::Destination,
3960 };
3961 let asked = (|| {
3962 if kind == LOCAL_TYPE {
3963 return Some(Described {
3964 secrets: Vec::new(),
3965 identifier_quote: None,
3966 string_literal: None,
3967 capabilities: Vec::new(),
3968 message_limit: None,
3969 });
3970 }
3971 let id = crate::project::PluginId::new(plugin_kind, kind);
3972 let plugin = crate::plugins::locate_in(&self.root, &self.plugins, &id).ok()?;
3973 let mut p = plugin.start(self.log.clone(), Some(&self.root)).ok()?;
3974 let capabilities = p.info().capabilities.clone();
3975 let d = p.description().ok();
3976 let _ = p.close();
3977 let d = d?;
3978 Some(Described {
3979 secrets: d
3980 .connection_fields
3981 .into_iter()
3982 .filter(|f| f.secret)
3983 .map(|f| f.name)
3984 .collect(),
3985 identifier_quote: d.identifier_quote,
3986 string_literal: d.string_literal,
3987 capabilities,
3988 message_limit: d.message_limit,
3989 })
3990 })();
3991 DESCRIBED.lock().unwrap().insert(key, asked.clone());
3992 asked
3993 }
3994}
3995
3996impl Connections for ProfileConnections {
3997 fn profile(&self, name: &str, role: Option<&str>) -> Result<Connection, String> {
3998 let role = match role {
3999 Some("connection") => Role::Connection,
4000 Some(_) => Role::Destination,
4001 None => {
4002 let c = self.profiles.get(Role::Connection, name).is_some();
4003 let d = self.profiles.get(Role::Destination, name).is_some();
4004 match (c, d) {
4005 (true, true) => {
4006 return Err(format!(
4007 "`{name}` is both a connection and a destination profile; say which with `profile('{name}', role='connection')` or `role='destination'`"
4008 ));
4009 }
4010 (true, false) => Role::Connection,
4011 _ => Role::Destination,
4012 }
4013 }
4014 };
4015 self.view(role, name)
4016 }
4017
4018 fn identifier_quote(&self, kind: &str) -> Result<Option<String>, String> {
4019 match self.described(Role::Connection, kind) {
4020 Some(d) => Ok(d.identifier_quote),
4021 None => Err(format!(
4022 "the `{kind}` source plugin couldn't be asked for its identifier quote character"
4023 )),
4024 }
4025 }
4026
4027 fn string_literal(&self, kind: &str) -> Result<Option<String>, String> {
4028 Ok(self
4029 .described(Role::Connection, kind)
4030 .and_then(|d| d.string_literal))
4031 }
4032}
4033
4034fn check_sheet_name(n: &str) -> Result<(), String> {
4035 if n.is_empty() || n.chars().count() > 31 {
4036 return Err(format!("sheet name `{n}` must be 1 to 31 characters long"));
4037 }
4038 if let Some(c) = n.chars().find(|c| "[]:*?/\\".contains(*c)) {
4039 return Err(format!(
4040 "sheet name `{n}` contains `{c}`, which Excel doesn't allow in sheet names"
4041 ));
4042 }
4043 if n.starts_with('\'') || n.ends_with('\'') {
4044 return Err(format!("sheet name `{n}` can't start or end with an apostrophe"));
4045 }
4046 if n.eq_ignore_ascii_case("history") {
4047 return Err("`History` is reserved by Excel and can't be a sheet name".into());
4048 }
4049 Ok(())
4050}
4051
4052fn is_single_table(format: &str) -> bool {
4053 matches!(format, "csv" | "delimited" | "fixed_width" | "parquet")
4054}
4055
4056fn extension(format: &str) -> &str {
4057 match format {
4058 "delimited" | "fixed_width" => "txt",
4059 crate::project::MESSAGE_FORMAT => "md",
4060 f => f,
4061 }
4062}
4063
4064fn split_ext(name: &str) -> (&str, &str) {
4065 match name.rfind('.') {
4066 Some(i) if i > 0 => (&name[..i], &name[i..]),
4067 _ => (name, ""),
4068 }
4069}
4070
4071fn rel(root: &Path, p: &Path) -> PathBuf {
4072 crate::slash(p.strip_prefix(root).unwrap_or(p))
4073}
4074
4075pub fn rfc3339(t: chrono::DateTime<chrono::Utc>) -> String {
4077 t.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
4078}
4079
4080fn record_path(project: &Project, p: &Path) -> PathBuf {
4084 match p.strip_prefix(&project.root) {
4085 Ok(r) => crate::slash(r),
4086 Err(_) => rel(&project.target_dir, p),
4087 }
4088}
4089
4090impl BindingRun<'_> {
4091 fn summary_line(&self) -> String {
4093 let rows: u64 = self.produced.iter().map(|p| p.rows).sum();
4094 let mut parts = Vec::new();
4095 if !self.produced.is_empty() {
4096 let n = self.produced.len();
4097 parts.push(format!(
4098 "{n} result set{}, {} row{}",
4099 if n == 1 { "" } else { "s" },
4100 thousands(rows),
4101 if rows == 1 { "" } else { "s" }
4102 ));
4103 }
4104 let names: Vec<String> = self
4105 .files()
4106 .map(|(f, _)| f.file_name().unwrap_or_default().to_string_lossy().to_string())
4107 .collect();
4108 if !names.is_empty() {
4109 parts.push(format!("→ {}", names.join(", ")));
4110 }
4111 let delivered: Vec<&str> = self
4113 .outs
4114 .iter()
4115 .flat_map(|o| o.deliveries.iter())
4116 .filter_map(|d| d.location.as_deref())
4117 .collect();
4118 let notes: Vec<&str> = self
4119 .outs
4120 .iter()
4121 .filter(|o| !o.dests.is_empty())
4122 .filter_map(|o| o.delivery_note.as_deref())
4123 .collect();
4124 if !delivered.is_empty() {
4125 parts.push(format!("→ {}", delivered.join(", ")));
4126 } else if !notes.is_empty() && self.opts.preview.is_none() {
4127 parts.push(format!("({})", notes.join("; ")));
4128 }
4129 parts.join(" ")
4130 }
4131
4132 fn files(&self) -> impl Iterator<Item = &(PathBuf, Option<String>)> {
4134 self.outs.iter().flat_map(|o| o.files.iter())
4135 }
4136}
4137
4138pub fn thousands(n: u64) -> String {
4140 let s = n.to_string();
4141 let mut out = String::new();
4142 for (i, c) in s.chars().enumerate() {
4143 if i > 0 && (s.len() - i).is_multiple_of(3) {
4144 out.push(',');
4145 }
4146 out.push(c);
4147 }
4148 out
4149}
4150
4151fn human_bytes(n: u64) -> String {
4152 match n {
4153 n if n < 1024 => format!("{n} B"),
4154 n if n < 1024 * 1024 => format!("{:.1} KB", n as f64 / 1024.0),
4155 n => format!("{:.1} MB", n as f64 / (1024.0 * 1024.0)),
4156 }
4157}
4158
4159pub fn clean(t: &Path) -> std::io::Result<bool> {
4161 if t.exists() {
4162 std::fs::remove_dir_all(t)?;
4163 Ok(true)
4164 } else {
4165 Ok(false)
4166 }
4167}