1use std::collections::BTreeMap;
15use std::path::PathBuf;
16use std::sync::{Arc, Mutex};
17
18use chrono::{DateTime, NaiveDate, Utc};
19use serde_json::{Map as JsonMap, Value as Json};
20
21use crate::dates::Calendar;
22use crate::project::{Binding, Project, Report};
23use crate::render::{Limited, Mode, Renderer, RendererConfig, ResolvedSource, RunContext, SourceResolver};
24
25pub const PARSE_FAILED: &str = "parse-failed";
27
28#[derive(Debug, Clone, Default)]
30pub struct Inputs {
31 pub target: String,
32 pub cli_vars: BTreeMap<String, String>,
34 pub date: Option<NaiveDate>,
36 pub scheduled_at: Option<DateTime<Utc>>,
38 pub timezone: Option<String>,
40 pub schedule: Option<String>,
42 pub started_at: Option<DateTime<Utc>>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Problem {
49 pub code: &'static str,
50 pub file: PathBuf,
51 pub line: Option<usize>,
52 pub message: String,
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct ParsedQuery {
58 pub query: String,
59 pub profile: Option<String>,
61 pub connection: Option<String>,
63 pub sources: Vec<String>,
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct ParsedBinding {
71 pub inherited: Option<String>,
73 pub queries: Vec<ParsedQuery>,
74 pub destinations: Vec<Option<String>>,
76 pub sources: BTreeMap<String, ResolvedSource>,
78 pub errors: Vec<Problem>,
79 pub warnings: Vec<Problem>,
80}
81
82impl ParsedBinding {
83 pub fn query(&self, name: &str) -> Option<&ParsedQuery> {
84 self.queries.iter().find(|q| q.query == name)
85 }
86
87 pub fn connections(&self) -> Vec<&str> {
89 let mut out: Vec<&str> = Vec::new();
90 for c in self.queries.iter().filter_map(|q| q.connection.as_deref()) {
91 if !out.contains(&c) {
92 out.push(c);
93 }
94 }
95 out
96 }
97
98 pub fn source_keys(&self) -> Vec<&str> {
100 let mut out: Vec<&str> = Vec::new();
101 for s in self.queries.iter().flat_map(|q| q.sources.iter()) {
102 if !out.contains(&s.as_str()) {
103 out.push(s);
104 }
105 }
106 out
107 }
108}
109
110pub fn calendar(
113 project: &Project,
114 report: &Report,
115 timezone: Option<&str>,
116 schedule: Option<&str>,
117) -> Calendar {
118 let schedule_tz = schedule
119 .and_then(|n| project.schedules.iter().find(|e| e.name == n))
120 .and_then(|e| e.timezone.as_deref());
121 let tz = timezone
122 .or(schedule_tz)
123 .or(report.timezone.as_deref())
124 .and_then(|t| crate::dates::parse_tz(t).ok())
125 .unwrap_or(chrono_tz::Tz::UTC);
126 Calendar {
127 tz,
128 week_start: project.week_start,
129 numbering: project.week_numbering,
130 }
131}
132
133pub fn run_date(cal: &Calendar, date: Option<NaiveDate>, scheduled_at: Option<DateTime<Utc>>) -> NaiveDate {
135 date.or(scheduled_at.map(|t| t.with_timezone(&cal.tz).date_naive()))
136 .unwrap_or_else(|| cal.today())
137}
138
139pub fn run_context(
141 project: &Project,
142 report: &Report,
143 b: &Binding,
144 inputs: &Inputs,
145 now: DateTime<Utc>,
146) -> RunContext {
147 let calendar = calendar(
148 project,
149 report,
150 inputs.timezone.as_deref(),
151 inputs.schedule.as_deref(),
152 );
153 RunContext {
154 report: report.name.clone(),
155 set: b.set.clone(),
156 target: inputs.target.clone(),
157 schedule: inputs.schedule.clone(),
158 date: run_date(&calendar, inputs.date, inputs.scheduled_at),
159 now: inputs.scheduled_at.unwrap_or(now),
160 scheduled_at: inputs.scheduled_at,
161 calendar,
162 }
163}
164
165type Resolved = BTreeMap<(String, String), Result<ResolvedSource, String>>;
167
168pub fn resolver(project: &Project, limited: Arc<Limited>) -> Option<SourceResolver> {
170 if project.sources.is_empty() {
171 return None;
172 }
173 let sources = project.sources.clone();
174 let cache: Arc<Mutex<Resolved>> = Arc::default();
175 Some(Arc::new(move |source: &str, table: &str| {
176 let key = (source.to_string(), table.to_string());
177 if let Some(r) = cache.lock().unwrap().get(&key) {
178 return r.clone();
179 }
180 let r = (|| {
181 let Some(s) = sources.get(source) else {
182 return Err(format!(
183 "no source `{source}` is declared (declared: {})",
184 sources.keys().cloned().collect::<Vec<_>>().join(", ")
185 ));
186 };
187 let Some(t) = s.table(table) else {
188 return Err(format!(
189 "source `{source}` has no table `{table}` (it has: {})",
190 s.tables
191 .iter()
192 .map(|t| t.name.as_str())
193 .collect::<Vec<_>>()
194 .join(", ")
195 ));
196 };
197 let at = |key: &str| format!("source `{source}`: `{key}`");
198 let render = |key: &str, v: &str| -> Result<String, String> {
199 let out = limited.render(&at(key), v)?;
200 if out.is_empty() {
201 return Err(format!("{} `{v}` renders empty", at(key)));
202 }
203 Ok(out)
204 };
205 Ok(ResolvedSource {
206 source: source.to_string(),
207 table: table.to_string(),
208 profile: s.profile.as_deref().map(|p| render("profile", p)).transpose()?,
209 database: s.database.as_deref().map(|d| render("database", d)).transpose()?,
210 schema: render("schema", s.schema.as_deref().unwrap_or(&s.name))?,
211 identifier: render("identifier", t.identifier.as_deref().unwrap_or(&t.name))?,
212 quoting: t.quoting.over(&s.quoting),
213 })
214 })();
215 cache.lock().unwrap().insert(key, r.clone());
216 r
217 }))
218}
219
220pub fn binding(
223 project: &Project,
224 report: &Report,
225 b: &Binding,
226 vars: &JsonMap<String, Json>,
227 inputs: &Inputs,
228) -> ParsedBinding {
229 let ctx_name = match &b.set {
230 Some(s) => format!("report `{}`, Set `{s}`", report.name),
231 None => format!("report `{}`", report.name),
232 };
233 let context = run_context(
234 project,
235 report,
236 b,
237 inputs,
238 inputs.started_at.unwrap_or_else(Utc::now),
239 );
240 let limited = Arc::new(Limited::new(
241 context.clone(),
242 vars.clone(),
243 inputs.cli_vars.clone(),
244 ));
245 let mut out = ParsedBinding::default();
246 let error =
247 |out: &mut ParsedBinding, code: &'static str, file: PathBuf, line: Option<usize>, msg: String| {
248 out.errors.push(Problem {
249 code,
250 file,
251 line,
252 message: msg,
253 })
254 };
255 let render_profile = |out: &mut ParsedBinding,
258 what: &str,
259 raw: &str,
260 at: (&PathBuf, Option<usize>),
261 ctx: Option<&str>|
262 -> Option<String> {
263 let message = match limited.render(what, raw) {
264 Ok(v) if !v.is_empty() => return Some(v),
265 Ok(_) => format!("{what} `{raw}` renders empty"),
266 Err(e) => e,
267 };
268 out.errors.push(Problem {
269 code: "invalid-profile-value",
270 file: at.0.clone(),
271 line: at.1,
272 message: match ctx {
273 Some(c) => format!("{c}: {message}"),
274 None => message,
275 },
276 });
277 None
278 };
279 let inherited_at = b.profile_at.clone().unwrap_or(crate::project::ProfileAt {
280 file: report.file.clone(),
281 line: None,
282 key: "`profile`".into(),
283 });
284 if let Some(raw) = &b.profile {
285 out.inherited = render_profile(
286 &mut out,
287 &inherited_at.key,
288 raw,
289 (&inherited_at.file, inherited_at.line),
290 None,
291 );
292 }
293 let inherited_failed = b.profile.is_some() && out.inherited.is_none();
294 let sources = resolver(project, limited.clone());
295 let renderer = Renderer::new(RendererConfig {
296 root: &project.root,
297 macros: &project.macros,
298 context,
299 vars: vars.clone(),
300 cli_vars: inputs.cli_vars.clone(),
301 runner: None,
302 connections: None,
303 mode: Mode::Parse,
304 connection: None,
305 source_type: String::new(),
306 sources: sources.clone(),
307 run_query_max_rows: project.run_query_max_rows,
308 sql: project.sql.clone(),
309 lookups: project.lookups.clone(),
310 lookup_inline_max_rows: project.lookup_inline_max_rows,
311 packages: project.packages.clone(),
312 project_name: project.name.clone(),
313 dispatch: project.dispatch.clone(),
314 });
315 let renderer = match renderer {
316 Ok(r) => Some(r),
317 Err(e) => {
318 error(
319 &mut out,
320 PARSE_FAILED,
321 e.file.clone(),
322 e.line,
323 format!("{ctx_name}: {}", e.message),
324 );
325 None
326 }
327 };
328 for q in &b.queries {
329 let mut pq = ParsedQuery {
330 query: q.query.clone(),
331 ..Default::default()
332 };
333 let mut failed = inherited_failed;
334 if let Some(raw) = &q.profile {
335 pq.profile = render_profile(
336 &mut out,
337 &format!("`profile` of query `{}`", q.query),
338 raw,
339 (&report.file, None),
340 Some(&ctx_name),
341 );
342 failed |= pq.profile.is_none();
343 }
344 let Some(r) = &renderer else {
345 out.queries.push(pq);
346 continue;
347 };
348 let src = match std::fs::read_to_string(project.root.join(&q.path)) {
349 Ok(s) => s,
350 Err(e) => {
351 error(
352 &mut out,
353 PARSE_FAILED,
354 q.path.clone(),
355 None,
356 format!("can't read it: {e}"),
357 );
358 out.queries.push(pq);
359 continue;
360 }
361 };
362 let _ = r.take_sources();
363 if let Err(e) = r.render(&q.path, &src) {
364 error(
365 &mut out,
366 PARSE_FAILED,
367 e.file.clone(),
368 e.line,
369 format!("{ctx_name}: query `{}` doesn't parse: {}", q.query, e.message),
370 );
371 out.queries.push(pq);
372 continue;
373 }
374 let mut explicit: Vec<(String, String)> = Vec::new();
375 if let Some(p) = &pq.profile {
376 explicit.push((format!("query `{}` sets `profile: {p}`", q.query), p.clone()));
377 }
378 for (s, t) in r.take_sources() {
379 let resolve = sources
380 .as_ref()
381 .expect("source() succeeded, so sources are declared");
382 let rs = resolve(&s, &t).expect("resolved during the render");
383 let key = rs.key();
384 if let Some(p) = &rs.profile {
385 explicit.push((format!("source `{key}` is on connection `{p}`"), p.clone()));
386 }
387 out.sources.entry(key.clone()).or_insert(rs);
388 pq.sources.push(key);
389 }
390 pq.connection = match explicit.split_first() {
391 Some(((first_why, first), rest)) => {
392 if let Some((why, _)) = rest.iter().find(|(_, p)| p != first) {
393 error(
394 &mut out,
395 "connection-conflict",
396 q.path.clone(),
397 None,
398 format!(
399 "{ctx_name}: query `{}` can't run on one connection: {first_why}, but {why}; one query runs on one connection",
400 q.query
401 ),
402 );
403 None
404 } else {
405 Some(first.clone())
406 }
407 }
408 None => out.inherited.clone(),
409 };
410 let conflicted = out.errors.iter().any(|e| e.file == q.path);
412 if pq.connection.is_none() && !failed && !conflicted {
413 error(
414 &mut out,
415 "no-connection",
416 report.file.clone(),
417 None,
418 format!(
419 "{ctx_name}: query `{}` has no connection: give it `profile:`, use a source with a `profile:`, or give the report one (`profile:` on the report or Set, a folder's `+profile`, or `default_profile` in dre_project.yml)",
420 q.query
421 ),
422 );
423 }
424 out.queries.push(pq);
425 }
426 for d in &b.output.destinations {
427 let p = render_profile(
428 &mut out,
429 "destination `profile`",
430 &d.profile,
431 (&report.file, None),
432 Some(&ctx_name),
433 );
434 out.destinations.push(p);
435 }
436 setup_warnings(report, b, &mut out);
437 out
438}
439
440fn setup_warnings(report: &Report, b: &Binding, out: &mut ParsedBinding) {
444 let conn = |name: &str| out.query(name).and_then(|q| q.connection.clone());
445 let mut warnings = Vec::new();
446 for (i, q) in b.queries.iter().enumerate() {
447 if q.tab {
448 continue;
449 }
450 let Some(c) = conn(&q.query) else { continue };
451 let later: Vec<(String, String)> = b.queries[i + 1..]
452 .iter()
453 .filter(|l| l.tab)
454 .filter_map(|l| Some((l.query.clone(), conn(&l.query)?)))
455 .collect();
456 if later.is_empty() || later.iter().any(|(_, lc)| *lc == c) {
457 continue;
458 }
459 let (other, oc) = &later[0];
460 warnings.push(Problem {
461 code: "setup-on-other-connection",
462 file: report.file.clone(),
463 line: None,
464 message: format!(
465 "report `{}`: setup query `{}` (`tab: false`) runs on connection `{c}`, but the tabs after it run elsewhere (`{other}` on `{oc}`); its temp tables and `SET`s are only visible on `{c}`",
466 report.name, q.query
467 ),
468 });
469 }
470 out.warnings.extend(warnings);
471}