1use std::fmt;
13
14use serde::Serialize;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Kind {
20 Usage,
22 Config,
24 Plugin,
26 Refused,
28 Connection,
30 Auth,
32 Query,
34 Delivery,
36 Internal,
38 Cancelled,
40 TimedOut,
42}
43
44impl Kind {
45 pub fn exit_code(self) -> u8 {
50 match self {
51 Kind::Usage | Kind::Config | Kind::Plugin | Kind::Refused => 2,
52 Kind::TimedOut => 124,
53 Kind::Cancelled => 130,
54 Kind::Connection | Kind::Auth | Kind::Query | Kind::Delivery | Kind::Internal => 1,
55 }
56 }
57
58 pub fn as_str(self) -> &'static str {
59 match self {
60 Kind::Usage => "usage",
61 Kind::Config => "config",
62 Kind::Plugin => "plugin",
63 Kind::Refused => "refused",
64 Kind::Connection => "connection",
65 Kind::Auth => "auth",
66 Kind::Query => "query",
67 Kind::Delivery => "delivery",
68 Kind::Internal => "internal",
69 Kind::Cancelled => "cancelled",
70 Kind::TimedOut => "timed_out",
71 }
72 }
73}
74
75impl Kind {
76 pub fn parse(name: &str) -> Option<Kind> {
78 [
79 Kind::Usage,
80 Kind::Config,
81 Kind::Plugin,
82 Kind::Refused,
83 Kind::Connection,
84 Kind::Auth,
85 Kind::Query,
86 Kind::Delivery,
87 Kind::Internal,
88 Kind::Cancelled,
89 Kind::TimedOut,
90 ]
91 .into_iter()
92 .find(|k| k.as_str() == name)
93 }
94}
95
96impl fmt::Display for Kind {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.write_str(self.as_str())
99 }
100}
101
102macro_rules! codes {
103 ($( $(#[$doc:meta])* $variant:ident = $slug:literal, $kind:ident, $summary:literal, $explanation:literal; )*) => {
104 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
106 pub enum Code {
107 $( $(#[$doc])* $variant, )*
108 }
109
110 impl Code {
111 pub const ALL: &'static [Code] = &[$( Code::$variant, )*];
113
114 pub fn as_str(self) -> &'static str {
116 match self { $( Code::$variant => $slug, )* }
117 }
118
119 pub fn kind(self) -> Kind {
120 match self { $( Code::$variant => Kind::$kind, )* }
121 }
122
123 pub fn summary(self) -> &'static str {
125 match self { $( Code::$variant => $summary, )* }
126 }
127
128 pub fn explanation(self) -> &'static str {
130 match self { $( Code::$variant => $explanation, )* }
131 }
132 }
133 };
134}
135
136impl Code {
137 pub fn parse(slug: &str) -> Option<Code> {
139 Code::ALL.iter().copied().find(|c| c.as_str() == slug)
140 }
141}
142
143impl fmt::Display for Code {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.write_str(self.as_str())
146 }
147}
148
149impl Serialize for Code {
150 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
151 s.serialize_str(self.as_str())
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Hash)]
158pub enum ErrorCode {
159 Core(Code),
160 Plugin { code: String, kind: Kind },
161}
162
163impl ErrorCode {
164 pub fn as_str(&self) -> &str {
165 match self {
166 ErrorCode::Core(c) => c.as_str(),
167 ErrorCode::Plugin { code, .. } => code,
168 }
169 }
170
171 pub fn kind(&self) -> Kind {
172 match self {
173 ErrorCode::Core(c) => c.kind(),
174 ErrorCode::Plugin { kind, .. } => *kind,
175 }
176 }
177}
178
179impl From<Code> for ErrorCode {
180 fn from(c: Code) -> ErrorCode {
181 ErrorCode::Core(c)
182 }
183}
184
185impl PartialEq<Code> for ErrorCode {
186 fn eq(&self, other: &Code) -> bool {
187 *self == ErrorCode::Core(*other)
188 }
189}
190
191impl fmt::Display for ErrorCode {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.write_str(self.as_str())
194 }
195}
196
197impl Serialize for ErrorCode {
198 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
199 s.serialize_str(self.as_str())
200 }
201}
202
203impl PartialEq<&str> for Code {
204 fn eq(&self, other: &&str) -> bool {
205 self.as_str() == *other
206 }
207}
208
209impl PartialEq<str> for Code {
210 fn eq(&self, other: &str) -> bool {
211 self.as_str() == other
212 }
213}
214
215codes! {
216 YamlSyntax = "yaml-syntax", Config,
218 "A YAML file doesn't parse.",
219 "The file isn't valid YAML: an unclosed bracket or quote, a tab used for indentation, a key written twice. The message names the line. Fix the file; `dre validate` reports every file's problems in one pass.";
220 IoError = "io-error", Internal,
221 "A file couldn't be read.",
222 "DRE couldn't read a file it found (permissions, a broken link, a file that vanished). The message has the operating system's reason.";
223 ProjectFileMissing = "project-file-missing", Usage,
224 "There's no dre_project.yml.",
225 "DRE looks for `dre_project.yml` in `--project-dir` (default: the current folder). Run the command from the project root, pass `--project-dir`, or create a project with `dre new`.";
226 InvalidProject = "invalid-project", Config,
227 "dre_project.yml has the wrong shape.",
228 "`dre_project.yml` must be a map of keys (`name:`, `default_profile:`, ...). See the dre_project.yml reference.";
229 InvalidReport = "invalid-report", Config,
230 "A report file has the wrong shape.",
231 "A report YAML file must be a map of report keys (`queries:`, `output:`, ...). See the report reference.";
232 UnknownKey = "unknown-key", Config,
233 "A key DRE doesn't know.",
234 "The file has a key that isn't part of its format, usually a typo or a key in the wrong place (a report key in dre_project.yml). The message lists the keys that are allowed there; the YAML references list them all.";
235 RemovedKey = "removed-key", Config,
236 "A key that was removed.",
237 "The key belonged to an older DRE. The message says what replaces it.";
238 InvalidField = "invalid-field", Config,
239 "A key has a value of the wrong type or shape.",
240 "The value doesn't fit the key: a string where a list is expected, a number out of range, an unknown choice. The message says what the key takes.";
241 MissingField = "missing-field", Config,
242 "A required key is missing.",
243 "The file leaves out a key it needs, e.g. `name:` in dre_project.yml, or the name of a report outside a report folder.";
244 InvalidFolderConfig = "invalid-folder-config", Config,
245 "Folder config under `reports:` has the wrong shape.",
246 "Each folder under `reports:` in dre_project.yml is a map: `+` keys are settings, other keys are subfolders.";
247 UnknownFolder = "unknown-folder", Config,
248 "Folder config names a folder that doesn't exist.",
249 "Folder config in dre_project.yml (`reports:`) sets something for a folder that isn't under `reports/`. Check the spelling and nesting, or remove it.";
250 InvalidTimezone = "invalid-timezone", Config,
251 "A timezone isn't a known IANA name.",
252 "`timezone:` takes an IANA name such as `Australia/Sydney` or `UTC`, not an offset or abbreviation.";
253 InvalidLocale = "invalid-locale", Config,
254 "A locale isn't one the number filters know.",
255 "`locale:` takes a tag such as `en`, `de-DE` or `fr`; the message lists what's known.";
256 InvalidTargetPath = "invalid-target-path", Config,
257 "The target path isn't usable.",
258 "The target path (`--target-path`, `DRE_TARGET_PATH` or `target_path:`) must be a local or mounted folder outside the project's sources, not a URL. To copy outputs to object storage, deliver them with a destination.";
259 TargetPathOnWorkspace = "target-path-on-workspace", Config,
260 "The target path is in Databricks Workspace files.",
261 "Workspace files (`/Workspace/...`) have shown flushing and rename problems, so DRE's atomic writes and the `current` pointer of its run folders can't be relied on there. On Databricks, point the target path at a Unity Catalog Volume (`/Volumes/<catalog>/<schema>/<volume>/dre/target`), or at local disk when nothing needs to outlive the job. A warning, so a run there still works as before.";
262 TargetPathUnwritable = "target-path-unwritable", Config,
263 "The target path can't be written.",
264 "DRE couldn't create or write the target folder (permissions, a read-only mount, a full disk). The message has the reason.";
265 UnrecognizedYaml = "unrecognized-yaml", Config,
266 "A YAML file isn't a report, Set, plugin or schedule file.",
267 "DRE recognises project YAML files by their keys. A file outside `reports/` that's none of a report (`queries:`), Sets, timings, schedules, `plugins:` or `sources:` is ignored. Move it, or fix its keys.";
268 DuplicateReportName = "duplicate-report-name", Config,
270 "Two reports have the same name.",
271 "Report names are unique across the project: a report's name is its folder's (or its `name:`), and an unmanaged report is named after its `.sql` file. Rename one of them.";
272 ConflictingDeclaration = "conflicting-declaration", Config,
273 "Two fragments of a report set the same key.",
274 "A report may be split over several YAML files, but each key is set in one of them only. Remove the key from one file.";
275 MissingQueries = "missing-queries", Config,
276 "A report has no queries.",
277 "A managed report needs a non-empty `queries:` list naming `.sql` files under `reports/`. A YAML file that only configures a report must name one that declares `queries:`.";
278 InvalidQueryName = "invalid-query-name", Config,
279 "A query is named with a path or extension.",
280 "`queries:` entries are bare `.sql` file names without folders or the extension: `orders`, not `reports/sales/orders.sql`.";
281 UnknownQuery = "unknown-query", Config,
282 "A query name doesn't match a .sql file or one of the report's queries.",
283 "The name in `queries:` (or a Set's `exclude:`/`queries:`, an output's `queries:`, `tab_names:`) doesn't match a `.sql` file under `reports/`, or isn't one of the report's queries. Check the spelling.";
284 DuplicateSqlName = "duplicate-sql-name", Config,
285 "Two .sql files have the same name.",
286 "Queries are found by file name across `reports/`, so two `.sql` files with one name are ambiguous. Rename one.";
287 UnusedQuery = "unused-query", Config,
288 "A query feeds no output.",
289 "Every output names its queries, and this one is in none of them. Add it to an output's `queries:`, or give it `tab: false` if it only prepares later queries (temp tables, `SET`s).";
290 DuplicateOutput = "duplicate-output", Config,
291 "Two outputs would have the same name or file.",
292 "Outputs of one report need unique `name:`s, and two unnamed outputs of one format would write the same file. Give each output a `name:`.";
293 InvalidOutputOption = "invalid-output-option", Config,
294 "An output option is wrong.",
295 "A shared output key (`template`, `extension`, ...) or a format's option has a bad value, or applies to another format. The message names the option.";
296 InvalidDestinationOption = "invalid-destination-option", Config,
297 "A destination option is wrong.",
298 "An `output.destination` entry has a bad value for one of its options (`attach:` naming no output, a plugin's option of the wrong type). The message names the option.";
299 InvalidConnectionSetting = "invalid-connection-setting", Config,
300 "A profile entry's connection setting is wrong.",
301 "The entry's plugin checked its settings without connecting: a required field is missing, a value has the wrong form or isn't one of the allowed values, or two settings can't go together. The message names the setting; fix it in `profiles.yml`.";
302 UnknownConnectionKey = "unknown-connection-key", Config,
303 "A profile entry has a key its plugin doesn't take.",
304 "The key is ignored, so a misspelt one does nothing. The message suggests the nearest known key. `dre validate --strict` makes it an error.";
305 InvalidTemplate = "invalid-template", Config,
306 "An xlsx template binding is wrong.",
307 "`output.template` needs a `file:`, and each binding a `sheet:` with either a table block (`query`, optional `anchor`, `columns`) or a single cell (`cell` with `value`, or `query` + `column`).";
308 MissingTemplate = "missing-template", Config,
309 "A template file doesn't exist.",
310 "`output.template.file` names a file that isn't in the project. Paths are relative to the project root.";
311 InvalidCell = "invalid-cell", Config,
312 "A cell reference isn't valid.",
313 "Cells are written like `A1` or `AB12`.";
314 JinjaSyntax = "jinja-syntax", Config,
315 "A template doesn't compile.",
316 "A Jinja template in SQL or YAML has a syntax error: an unclosed `{{` or `{%`, an unknown tag. The message has the line.";
317 ParseFailed = "parse-failed", Config,
318 "A query doesn't render.",
319 "Rendering a query without a database failed: a macro error, a bad `source()` or `ref()`, a function used where it can't run. The message has the cause.";
320 CompileFailed = "compile-failed", Config,
321 "A report doesn't compile.",
322 "`dre compile` or `validate` couldn't render a Binding's SQL. The message has the cause.";
323 CompileNeedsRun = "compile-needs-run", Config,
324 "A report can only be checked by running it.",
325 "The report queries the database while rendering (`run_query()`, `columns()`), so `validate` can't compile it offline. `dre run` (or `validate --live`) checks it.";
326 UnknownRef = "unknown-ref", Config,
327 "`ref()` names nothing.",
328 "`ref('name')` must name a `.sql` file under `reports/` or a lookup under `lookups/`.";
329 RefCycle = "ref-cycle", Config,
330 "`ref()` calls go round in a circle.",
331 "Shared SQL files `ref()` each other in a loop. The message shows the cycle; break it.";
332 DuplicateRefName = "duplicate-ref-name", Config,
333 "A lookup and a .sql file have the same name.",
334 "`ref()` names are shared by `.sql` files and lookups, so they must be unique. Rename one.";
335 UnresolvedVar = "unresolved-var", Config,
336 "`var()` has no value.",
337 "No level sets the variable: `--var`, the schedule, the Set, the report, the folders, or the project. Set it at one of them, or give `var()` a default.";
338 UnsetEnvVar = "unset-env-var", Config,
339 "`env_var()` names an unset variable.",
340 "The environment variable isn't set and `env_var()` has no default. Set it, or give a default: `env_var('NAME', 'fallback')`.";
341 UnknownRunAttribute = "unknown-run-attribute", Config,
342 "`run.<x>` isn't part of the run context.",
343 "Templates can use `run.report`, `run.set`, `run.target`, `run.schedule`, `run.date` (with its navigation, e.g. `.prev_month`) and `run.now`.";
344 RemovedTemplateName = "removed-template-name", Config,
345 "A template uses a name that was removed.",
346 "The function or variable was renamed in an earlier release; the message says what to use.";
347 UnmanagedReport = "unmanaged-report", Config,
348 "A .sql file runs as an unmanaged report.",
349 "A `.sql` file no YAML lists in `queries:` (and no `ref()` uses) is an unmanaged report: it runs with the folder's settings into a csv. That's for quick tests; add a YAML to make it a managed report.";
350 UnmanagedSideEffect = "unmanaged-side-effect", Refused,
351 "An unmanaged report runs a statement it may not.",
352 "Unmanaged reports may only run `SELECT`/`WITH` and create temp tables or views, so a stray `.sql` file can't change data. Rewrite the statement, or give the report a YAML.";
353 DuplicateSet = "duplicate-set", Config,
355 "A Set is declared or listed twice.",
356 "Set names are unique in `sets.yml` files, and a report lists each Set once.";
357 UnknownSet = "unknown-set", Config,
358 "A report names a Set that doesn't exist.",
359 "A name in a report's `sets:` must be declared in `sets.yml` (or be a map that declares it inline).";
360 UnknownDefaultSet = "unknown-default-set", Config,
361 "`default_set` isn't one of the report's Sets.",
362 "`default_set` (in the report or dre_project.yml) must name one of the report's `sets:`.";
363 ExcludeAndQueries = "exclude-and-queries", Config,
364 "A Set uses both `exclude:` and `queries:`.",
365 "A Set either leaves queries out (`exclude:`) or replaces them (`queries:`), not both.";
366 ProfileAndSets = "profile-and-sets", Config,
367 "A report sets both `profile:` and `sets:`.",
368 "With Sets, each Set chooses its connection; give the report one or the other.";
369 InvalidProfiles = "invalid-profiles", Config,
370 "profiles.yml has the wrong shape.",
371 "profiles.yml is a map with `connections:` and/or `destinations:`, each a map of profiles. See the profiles.yml reference.";
372 InvalidProfile = "invalid-profile", Config,
373 "A profile in profiles.yml is wrong.",
374 "Each profile has `targets:`, a map of entries with a `type:` (or `{deliver: false}` for a destination), and optionally `target:`, the entry it uses by default.";
375 InvalidProfileValue = "invalid-profile-value", Config,
376 "A value that chooses a connection uses something only a connection has.",
377 "`profile:` and `default_profile` are rendered before any connection is open, so they can't use `connection.*`, `run_query()` or `columns()`.";
378 ProfilesSourcesRenamed = "profiles-sources-renamed", Config,
379 "profiles.yml uses the old `sources:` section.",
380 "DRE 0.2 renamed profiles.yml's `sources:` to `connections:`. Rename it.";
381 ProfilesMissing = "profiles-missing", Config,
382 "The project uses profiles but there's no profiles.yml.",
383 "DRE looks for profiles.yml in `--profiles-dir`, `DRE_PROFILES_DIR`, the project directory, then `~/.dre`. Create one with `dre init`, or point DRE at yours.";
384 UnknownProfile = "unknown-profile", Config,
385 "A profile isn't defined in profiles.yml.",
386 "The connection or destination profile isn't under `connections:`/`destinations:` in the profiles.yml DRE found. Check the name, or `dre validate -v` for where DRE looked.";
387 TargetMismatch = "target-mismatch", Config,
388 "The profiles are on another target than the run.",
389 "Every profile the run uses chooses a different entry than the run's target (`--target`, `DRE_TARGET`, else `dev`). Pass `--target` or set `DRE_TARGET` so `target.name` matches.";
390 NoConnection = "no-connection", Config,
391 "A query has no connection.",
392 "Nothing gives the query a 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).";
393 ConnectionConflict = "connection-conflict", Config,
394 "A query would need two connections.",
395 "A query uses sources (or a `profile:`) on different connections, but one query runs on one connection. Split it, or move the tables to one system.";
396 MissingTargetEntry = "missing-target-entry", Config,
397 "A profile has no entry for the run's target.",
398 "Each profile the run uses picks an entry by `--target`, else `DRE_TARGET`, else its own `target:`, else `dev`. Add that entry to the profile's `targets:`, or choose another target. A destination can deliver nowhere on a target with `{deliver: false}`.";
399 SetupOnOtherConnection = "setup-on-other-connection", Config,
400 "A setup statement runs on another connection than the queries after it.",
401 "A `tab: false` query prepares state (temp tables, `SET`s) that only exists on its own connection, but a later query reads it on another. Run them on one connection.";
402 InvalidSource = "invalid-source", Config,
404 "A source declaration is wrong.",
405 "`sources:` is dbt's format: a list of sources, each with a `name:` and `tables:`, optionally `database`, `schema`, `profile`, `quoting`, `tags`, `meta`. See the sources reference.";
406 DuplicateSource = "duplicate-source", Config,
407 "Two sources have the same name.",
408 "Source names are unique across the project's YAML files.";
409 DuplicateSourceTable = "duplicate-source-table", Config,
410 "A source declares a table twice.",
411 "Table names are unique within a source.";
412 DuplicateSourceColumn = "duplicate-source-column", Config,
413 "A table declares a column twice.",
414 "Column names are unique within a table, compared case-insensitively.";
415 SourceKeyNotSupported = "source-key-not-supported", Config,
416 "A dbt source key DRE doesn't use yet.",
417 "The key is valid dbt but DRE ignores it for now (freshness, loader, tests, ...). The declaration still works.";
418 UnknownSource = "unknown-source", Usage,
419 "A selector names a source that doesn't exist.",
420 "`source:<name>` selects reports using that source; the name must be declared under `sources:`.";
421 AmbiguousSelector = "ambiguous-selector", Usage,
423 "A selector matches a report and a folder.",
424 "A bare name can mean a report or a folder. Use `folder:<path>` or the report's full dotted path.";
425 InvalidSelector = "invalid-selector", Usage,
426 "A selector can't be resolved.",
427 "The selector (`-s`, `--set`, `--schedule`) doesn't resolve to Bindings to run. The message says why; `dre ls` lists what exists.";
428 SelectorMatchesNothing = "selector-matches-nothing", Usage,
429 "A selector or schedule matches no report.",
430 "The selector (`-s`, a schedule's `select:` or `report:`) names no report, folder, tag or Set. `dre ls` lists what exists.";
431 InvalidSchedule = "invalid-schedule", Config,
432 "A schedule is wrong.",
433 "Each schedules.yml entry has a `name`, `report:` (optionally `set:`) or `select:`, and a timing: `timing:` naming one in timings.yml, or its own `cron`, `every` or `rrule`.";
434 DuplicateScheduleName = "duplicate-schedule-name", Config,
435 "Two schedules have the same name.",
436 "Schedule names are unique across the project.";
437 ScheduleMoved = "schedule-moved", Config,
438 "A schedule is set where schedules are no longer written.",
439 "Schedules moved from report and folder YAML (`schedule:`, `+schedule`) to schedules.yml, as named entries.";
440 ScheduleNoTime = "schedule-no-time", Config,
441 "A timing has no time of day.",
442 "An `every` or `rrule` timing without `at:` (or `BYHOUR`) fires at midnight. Add `at:` to fire at another time.";
443 ScheduleNeedsAnchor = "schedule-needs-anchor", Config,
444 "A timing needs a start date.",
445 "`every`, and an rrule with `INTERVAL` above 1, a `COUNT` or a day taken from its start, need `starting:` to know which days to fire on.";
446 ScheduleSeconds = "schedule-seconds", Config,
447 "A cron expression has seconds.",
448 "DRE's cron expressions have five fields (minute to weekday); a sixth field of seconds isn't supported.";
449 ScheduleTooFrequent = "schedule-too-frequent", Config,
450 "A timing fires more often than DRE allows.",
451 "Schedules fire at most every minute; the message has the interval.";
452 SchedulePathClash = "schedule-path-clash", Config,
453 "Two schedules of one Binding deliver to the same path.",
454 "With the same vars and date, two schedules would write the same file and one would overwrite the other. Give them different vars, or a path that tells them apart.";
455 ScheduleTimezoneMismatch = "schedule-timezone-mismatch", Config,
456 "A schedule fires in another timezone than its report renders in.",
457 "The run date is the report's timezone's date at the firing time, which may not be the day you expect. Set `timezone:` on the schedule to fire and render in one.";
458 InvalidTiming = "invalid-timing", Config,
459 "A timing in timings.yml is wrong.",
460 "Each timing is a map with exactly one of `cron`, `every` or `rrule`, and optionally `starting`, `at`, `except`, `also` and `timezone`.";
461 DuplicateTimingName = "duplicate-timing-name", Config,
462 "Two timings have the same name.",
463 "Timing names are unique across the project.";
464 UnknownTiming = "unknown-timing", Config,
465 "A schedule names a timing that doesn't exist.",
466 "`timing:` must name an entry of timings.yml; the message lists them.";
467 UnusedTiming = "unused-timing", Config,
468 "A timing isn't used by any schedule.",
469 "Remove it, or use it with `timing:` in a schedule.";
470 InvalidLookup = "invalid-lookup", Config,
472 "A lookup or its config is wrong.",
473 "A lookup is a csv, xlsx, xls, json or jsonl file under `lookups/` (or a `.yml` of rows), with an optional `<name>.yml` config of `columns`, `sheet` and `load`. The message names the problem.";
474 DuplicateLookup = "duplicate-lookup", Config,
475 "Two lookups have the same name.",
476 "Lookups are named after their file, without the extension; two files with one name are ambiguous.";
477 RunFailed = "run-failed", Internal,
479 "A Binding failed for a reason without its own code.",
480 "Something went wrong while running the Binding that isn't one of the other run errors. The message has the cause; please report it if it looks like a bug.";
481 RunCancelled = "run-cancelled", Cancelled,
482 "The run was stopped by Ctrl-C or a termination signal.",
483 "DRE received Ctrl-C (SIGINT) or a termination signal (SIGTERM; on Windows Ctrl-Break, closing the console, logging off or shutting down), from you or from the orchestrator cancelling the job. It asked every running plugin to stop, waited up to 8 seconds, then stopped them; a source that can cancel its query on the server (Postgres, Databricks, DuckDB) did. The Binding that was running is recorded as `cancelled` in its `run_results.json`, Bindings that hadn't started don't run, and nothing is delivered after the cancel. `dre` exits 130 after Ctrl-C and 143 after a termination. A second Ctrl-C stops at once.";
484 RunInProgress = "run-in-progress", Refused,
485 "The same Binding is already running.",
486 "Another `dre run` of this report and Set holds its lock (the `lock` file in `target/run/<report>/<set or default>/`), on this machine or, with a shared target path, on another. A Binding never runs twice at once, so the second run stops at once and changes nothing: it deletes no files, delivers nothing and leaves the drift snapshot alone. The message names the run in progress (its id, start time, host and process). Wait for it to finish. If that run is gone (a crashed machine, a killed container on another host), `dre unlock <report> [--binding <set>]` shows the lock and removes it. A lock left by a process on this machine that has gone is taken over automatically, with a warning.";
487 RunTimedOut = "run-timed-out", TimedOut,
488 "The run took longer than its timeout.",
489 "The run's timeout (`dre run --timeout`, `DRE_RUN_TIMEOUT` or `flags: run_timeout` in dre_project.yml; off unless set) ran out. DRE stopped the run as it does for a termination signal: it asked every running plugin to stop, waited up to 8 seconds, then stopped them. The Binding that was running is recorded as `timed_out` in its `run_results.json`, the ones that hadn't started don't run, nothing is delivered after, and `dre` exits 124. Raise the timeout if the run is just slow, or look at which statement was running (the log names it).";
490 ConnectionFailed = "connection-failed", Connection,
491 "A connection couldn't be opened.",
492 "The source plugin couldn't connect: the host is unreachable, the credentials are refused, the warehouse is unavailable. The message has the plugin's reason. Trying again can work when the cause is temporary.";
493 QueryFailed = "query-failed", Query,
494 "A query failed on the database.",
495 "The database rejected or failed a statement: a syntax error, a missing table, a permission. The message names the file and line.";
496 NoResultSet = "no-result-set", Config,
497 "A query that makes a tab returned no result set.",
498 "The query's last statement returned no rows to write (it created a view, set a variable). If it only prepares data for later queries, give it `tab: false` in the YAML.";
499 ForEachFailed = "for-each-failed", Config,
500 "A per-row output can't be split by its key.",
501 "Before anything renders, a `for_each` output's driver must give one row per key (no duplicates), every query of the output must have the key columns, the rows' file names must differ, the population must fit `max_population_rows`, and with `on_orphan_rows: fail` no row may have a key missing from the driver. The message says which. Nothing was written or delivered.";
502 RowsFailed = "rows-failed", Delivery,
503 "Rows of a per-row output failed.",
504 "One or more rows of a `for_each` output failed to render or deliver. With `on_row_error: continue` (the default) the other rows were made and delivered; with `fail`, nothing was delivered. Each failed row's key, code and message are in `run_results.json` (`output_results[].for_each.failed_keys`) and in the `<output>.rows.jsonl` sidecar. Fix the cause and run `dre run <report> --retry-failed <run id>` to redo only those rows, from the same saved results.";
505 ReleaseFailed = "release-failed", Delivery,
506 "`dre release` couldn't deliver every held delivery.",
507 "`dre release` delivers a run's held deliveries from the files in its run folder. It fails when the run isn't in the target path (keep it with `keep_runs` for the review window), has no `held.json`, or when a delivery failed or was refused because its file changed since the run rendered it (the SHA-256 recorded at the run must match). Delivered entries are recorded, so releasing again sends only what's still held.";
508 RetryFailed = "retry-failed", Usage,
509 "`--retry-failed` can't redo that run.",
510 "`dre run --retry-failed <run id>` redoes a run's failed and never-started rows from the query results it saved in its run folder. The run must still be in the target path (`keep_runs` prunes older runs; `dre history` lists them) with its `results/` folder. A run whose per-row outputs have `keep_results: false` saved none, and a retry of it runs the queries again.";
511 NoData = "no-data", Query,
512 "An output with `on_empty: fail` had no data.",
513 "Every query of the output returned no rows (or a query with `on_empty: fail` returned none), and `on_empty: fail` makes that a failure, so nothing was delivered. Check the data upstream; if empty is acceptable, use `on_empty: skip` (deliver nothing) or `continue` (deliver it empty). A `count(*)` returning 0 is one row, not empty. See \"When there's no data\" in docs/building-reports.md.";
514 RenderFailed = "render-failed", Config,
515 "A template failed to render during the run.",
516 "A destination, message or `when:` template failed to render with the run's results. The message has the cause.";
517 FormatFailed = "format-failed", Plugin,
518 "An output couldn't be written in its format.",
519 "The format plugin failed to write the output (bad options, a template it can't fill, a result it can't represent). The message has the plugin's reason.";
520 DeliveryFailed = "delivery-failed", Delivery,
521 "An output couldn't be delivered.",
522 "The destination plugin failed to deliver the file or message. The message has the plugin's reason; the output stays in the target path.";
523 InvalidPluginDeclaration = "invalid-plugin-declaration", Config,
525 "A `plugins:` entry is wrong.",
526 "Each entry is a package name, `name: \"<version>\"`, or a map with `name:` and one of `github:`, `local:`, `registry:`.";
527 InvalidVersionConstraint = "invalid-version-constraint", Config,
528 "A plugin's version constraint doesn't parse.",
529 "Constraints are semver requirements such as `1.2.0`, `>=1.0` or `^1`.";
530 ConflictingPluginConstraints = "conflicting-plugin-constraints", Config,
531 "Two declarations of a plugin allow no common version.",
532 "A package declared in several files must have constraints some version satisfies. Align them.";
533 ConflictingPluginSources = "conflicting-plugin-sources", Config,
534 "A plugin is declared with two sources.",
535 "Every declaration of a package must agree on where it comes from (`github:`, `local:`, `registry:`).";
536 MovedPluginDeclaration = "moved-plugin-declaration", Config,
537 "Plugins are declared under an old key.",
538 "Plugin packages are declared under `plugins:` (usually in dependencies.yml), not `destinations:`, `formats:` or a list of names under `sources:`.";
539 UndeclaredPlugin = "undeclared-plugin", Plugin,
540 "The project uses a plugin no declared package provides.",
541 "A connection type, format or destination type needs a plugin package listed under `plugins:`. Add the package the message names, then run `dre deps`.";
542 PluginNotInstalled = "plugin-not-installed", Plugin,
543 "A declared plugin isn't installed.",
544 "Run `dre deps` (or any command without `--no-auto-install`) to install the project's plugins.";
545 PluginNotFound = "plugin-not-found", Plugin,
546 "There's no plugin to check options against.",
547 "The plugin isn't installed, so its options couldn't be checked. Run `dre deps`.";
548 PluginInstallFailed = "plugin-install-failed", Plugin,
549 "A plugin couldn't be installed.",
550 "Downloading or verifying a plugin package failed. The message has the reason (network, checksum, no build for this platform).";
551 OptionsUnchecked = "options-unchecked", Plugin,
552 "A plugin's options weren't checked.",
553 "The plugin is too old to describe its options, or couldn't be asked. Run `dre plugin update` to get a version that checks them.";
554 InvalidPackages = "invalid-packages", Config,
555 "`packages:` is wrong.",
556 "`packages:` (in dependencies.yml or packages.yml) is a list of macro packages, each with `git:` and `revision:`, or `local:`.";
557 InvalidPackage = "invalid-package", Config,
558 "A macro package is broken.",
559 "The package's folder or manifest couldn't be read. The message has the reason.";
560 DuplicatePackage = "duplicate-package", Config,
561 "Two macro packages have the same name.",
562 "Macros are called through the package's name, so names are unique.";
563 ConflictingPackages = "conflicting-packages", Config,
564 "A macro package is declared twice with different revisions.",
565 "Declare each package once, at one revision.";
566 PackageMissing = "package-missing", Config,
567 "A macro package isn't there.",
568 "A local package's folder doesn't exist, or a git package isn't installed; run `dre deps`.";
569 MisplacedPackages = "misplaced-packages", Config,
570 "`packages:` is in the wrong file.",
571 "Macro packages are declared in dependencies.yml or packages.yml at the project root.";
572 PackageNameClash = "package-name-clash", Config,
573 "A macro package's name is already taken.",
574 "Package names are Jinja variables, so one can't share a name with a DRE function, a project macro or the project itself.";
575}
576
577pub const REFERENCE_URL: &str = "https://getdre.com/docs/reference-error-codes/";
579
580pub fn reference_page() -> String {
583 let mut out = String::from(
584 "---\ntitle: \"Error codes reference\"\ndescription: \"Every code DRE reports, what it means and how to fix it.\"\nsection: reference\nposition: 12\n---\n\n\
585 # Error codes reference\n\n\
586 <!-- Generated from crates/dre-core/src/codes.rs. Edit the registry, not this page. -->\n\n\
587 Every problem DRE reports has a code: `error[unknown-key]: ...` in the console, `code` in \
588 `--log-format json` events, `dre validate --json` and `run_results.json`. `dre explain <code>` prints \
589 the explanation below. A plugin's own codes are namespaced by the plugin \
590 (`sftp/host-key-mismatch`) and documented with it. Codes are stable: one is never reused \
591 for something else.\n\n\
592 Each code has a kind, which decides `dre`'s exit code and whether trying again can help.\n",
593 );
594 let mut kinds: Vec<Kind> = Code::ALL.iter().map(|c| c.kind()).collect();
595 kinds.sort();
596 kinds.dedup();
597 for k in kinds {
598 out.push_str(&format!("\n## {}\n", heading(k)));
599 let mut codes: Vec<Code> = Code::ALL.iter().copied().filter(|c| c.kind() == k).collect();
600 codes.sort_by_key(|c| c.as_str());
601 for c in codes {
602 out.push_str(&format!(
603 "\n### {}\n\n{}\n\n{}\n",
604 c.as_str(),
605 c.summary(),
606 c.explanation()
607 ));
608 }
609 }
610 out
611}
612
613fn heading(k: Kind) -> &'static str {
614 match k {
615 Kind::Usage => "Usage: how the command was called",
616 Kind::Config => "Config: the project's files",
617 Kind::Plugin => "Plugins",
618 Kind::Refused => "Refused: unsafe statements",
619 Kind::Connection => "Connections",
620 Kind::Auth => "Authentication",
621 Kind::Query => "Queries",
622 Kind::Delivery => "Delivery",
623 Kind::Internal => "Internal",
624 Kind::Cancelled => "Cancelled runs",
625 Kind::TimedOut => "Timeouts",
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
634 fn slugs_are_unique_kebab_case_and_parse_back() {
635 let mut seen = std::collections::BTreeSet::new();
636 for c in Code::ALL {
637 let s = c.as_str();
638 assert!(seen.insert(s), "{s} twice");
639 assert!(
640 s.chars()
641 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-'),
642 "{s}"
643 );
644 assert_eq!(Code::parse(s), Some(*c));
645 assert!(
646 c.summary().ends_with('.') && c.explanation().ends_with(['.', ')']),
647 "{s}"
648 );
649 }
650 }
651
652 #[test]
653 fn codes_serialize_and_compare_as_their_slug() {
654 assert_eq!(serde_json::to_value(Code::UnknownKey).unwrap(), "unknown-key");
655 assert!(Code::UnknownKey == "unknown-key");
656 assert_eq!(Code::UnknownKey.kind(), Kind::Config);
657 }
658}