Skip to main content

rok_sqlc/
report.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3
4/// What a [`generate`](crate::generate) run did, or would do in a dry run.
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
6#[non_exhaustive]
7pub struct Report {
8    /// Files written (in a dry run: files that would be written).
9    pub written: Vec<PathBuf>,
10    /// Files whose contents were already up to date.
11    pub unchanged: Vec<PathBuf>,
12    /// Existing files rok-sqlc did not generate and would not overwrite.
13    pub refused: Vec<RefusedFile>,
14    /// Stale files: generated files that are no longer part of the plan.
15    /// Removed after a successful run; a dry run or check only lists them.
16    pub stale: Vec<PathBuf>,
17    /// Check only: planned files that do not exist.
18    pub missing: Vec<PathBuf>,
19    /// Check only: existing planned files whose contents differ from the plan.
20    pub changed: Vec<PathBuf>,
21    /// Unsupported tables: skipped when lenient, fatal otherwise.
22    pub table_failures: Vec<TableFailure>,
23    /// Unsupported queries: skipped when lenient, fatal otherwise.
24    pub query_failures: Vec<QueryFailure>,
25}
26
27impl Report {
28    /// Whether the run found drift: missing, changed, or stale files.
29    pub fn has_drift(&self) -> bool {
30        !(self.missing.is_empty() && self.changed.is_empty() && self.stale.is_empty())
31    }
32}
33
34/// An existing file that generation would not overwrite.
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct RefusedFile {
38    pub path: PathBuf,
39    pub reason: String,
40}
41
42/// A table that cannot be generated.
43#[derive(Debug, Clone, PartialEq, Eq)]
44#[non_exhaustive]
45pub struct TableFailure {
46    /// The table's SQL name.
47    pub table: String,
48    pub reason: String,
49    /// The schema file (migration) that defines the table.
50    pub path: Option<PathBuf>,
51    /// 1-based line of the table's `CREATE TABLE` in that file.
52    pub line: Option<usize>,
53}
54
55impl TableFailure {
56    /// `path:line: ` when the failure is located, else empty.
57    pub(crate) fn location(&self) -> String {
58        location(self.path.as_deref(), self.line)
59    }
60}
61
62impl fmt::Display for TableFailure {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(
65            f,
66            "{}table `{}`: {}",
67            self.location(),
68            self.table,
69            self.reason
70        )
71    }
72}
73
74/// A path as rok-sqlc prints it: relative to the current directory when it
75/// lies under it, without a leading `./`, and with `/` as the separator on
76/// every platform, so diagnostics and file lists read the same everywhere.
77pub(crate) fn display_path(path: &Path) -> String {
78    let current = std::env::current_dir().ok();
79    let shown = current
80        .as_deref()
81        .and_then(|current| path.strip_prefix(current).ok())
82        .filter(|relative| !relative.as_os_str().is_empty())
83        .unwrap_or(path);
84    let mut text = shown.display().to_string();
85    if std::path::MAIN_SEPARATOR == '\\' {
86        text = text.replace('\\', "/");
87    }
88    while let Some(rest) = text.strip_prefix("./") {
89        text = rest.to_owned();
90    }
91    text
92}
93
94/// The `path:line: ` prefix of a located diagnostic.
95fn location(path: Option<&Path>, line: Option<usize>) -> String {
96    match (path, line) {
97        (Some(path), Some(line)) => format!("{}:{line}: ", display_path(path)),
98        _ => String::new(),
99    }
100}
101
102/// A query that cannot be generated.
103#[derive(Debug, Clone, PartialEq, Eq)]
104#[non_exhaustive]
105pub struct QueryFailure {
106    /// The query name, or `line N` when no valid name was annotated.
107    pub query: String,
108    pub reason: String,
109    /// The query file, when the failure is located in it.
110    pub path: Option<PathBuf>,
111    /// 1-based line of the query's `-- name:` annotation (or of the
112    /// offending line when there is no valid annotation), when known.
113    pub line: Option<usize>,
114}
115
116impl QueryFailure {
117    /// `path:line: ` when the failure is located, else empty.
118    pub(crate) fn location(&self) -> String {
119        location(self.path.as_deref(), self.line)
120    }
121}
122
123impl fmt::Display for QueryFailure {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(
126            f,
127            "{}query `{}`: {}",
128            self.location(),
129            self.query,
130            self.reason
131        )
132    }
133}
134
135/// A failed [`generate`](crate::generate) run. Nothing was written.
136///
137/// [`Error::report`] holds what was known when the run stopped: the
138/// unsupported tables and queries, or the refused files.
139pub struct Error {
140    inner: anyhow::Error,
141    report: Box<Report>,
142}
143
144impl Error {
145    pub(crate) fn new(inner: anyhow::Error, report: Report) -> Self {
146        Self {
147            inner,
148            report: Box::new(report),
149        }
150    }
151
152    /// What was known when the run stopped.
153    pub fn report(&self) -> &Report {
154        &self.report
155    }
156}
157
158impl fmt::Display for Error {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        fmt::Display::fmt(&self.inner, f)
161    }
162}
163
164impl fmt::Debug for Error {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        fmt::Debug::fmt(&self.inner, f)
167    }
168}
169
170impl std::error::Error for Error {
171    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172        self.inner.source()
173    }
174}
175
176/// The result of a [`generate`](crate::generate) run.
177pub type Result<T, E = Error> = std::result::Result<T, E>;