Skip to main content

pgevolve_core/parse/
error.rs

1//! Errors raised by the source parser.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7/// Position within a source file.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SourceLocation {
10    /// Path to the file (relative to the source root, when available).
11    pub file: PathBuf,
12    /// 1-based line number.
13    pub line: usize,
14    /// 1-based column.
15    pub column: usize,
16}
17
18impl SourceLocation {
19    /// Construct.
20    pub const fn new(file: PathBuf, line: usize, column: usize) -> Self {
21        Self { file, line, column }
22    }
23}
24
25impl std::fmt::Display for SourceLocation {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}:{}:{}", self.file.display(), self.line, self.column)
28    }
29}
30
31/// Errors raised by the source parser.
32#[derive(Debug, Error)]
33pub enum ParseError {
34    /// I/O error while reading a source file.
35    #[error("I/O error reading {path}: {source}")]
36    Io {
37        /// Path that failed.
38        path: PathBuf,
39        /// Underlying I/O error.
40        #[source]
41        source: std::io::Error,
42    },
43
44    /// `pg_query` rejected the SQL.
45    #[error("pg_query parse error at {location}: {message}")]
46    PgQuery {
47        /// Source location.
48        location: SourceLocation,
49        /// Message from `pg_query`.
50        message: String,
51    },
52
53    /// CREATE was for an object kind not supported in v0.1.
54    #[error(
55        "{location}: {kind} is not supported in pgevolve v0.1 — \
56         see docs §2 for the v0.1 object-kind list; expected to land in a later phase"
57    )]
58    UnsupportedObjectKind {
59        /// Source location.
60        location: SourceLocation,
61        /// Object kind name (e.g., "CREATE VIEW").
62        kind: &'static str,
63    },
64
65    /// CREATE was missing required schema qualification and no `-- @pgevolve schema=...`
66    /// directive applied.
67    #[error(
68        "{location}: object name must be schema-qualified, or the file must declare \
69         `-- @pgevolve schema=<name>`"
70    )]
71    UnqualifiedName {
72        /// Source location.
73        location: SourceLocation,
74    },
75
76    /// Generic structural error during AST → IR conversion.
77    #[error("{location}: {message}")]
78    Structural {
79        /// Source location.
80        location: SourceLocation,
81        /// Diagnostic message.
82        message: String,
83    },
84
85    /// IR construction failed (e.g., invalid identifier in source).
86    #[error("{location}: {source}")]
87    Ir {
88        /// Source location.
89        location: SourceLocation,
90        /// Underlying error.
91        #[source]
92        source: crate::ir::IrError,
93    },
94
95    /// A directive was malformed.
96    #[error("{location}: invalid pgevolve directive: {message}")]
97    InvalidDirective {
98        /// Source location.
99        location: SourceLocation,
100        /// Diagnostic.
101        message: String,
102    },
103
104    /// Two definitions of the same object qname were found.
105    #[error("duplicate object {qname} defined at {first} and {second}")]
106    DuplicateObject {
107        /// Object qname (rendered).
108        qname: String,
109        /// First definition location.
110        first: SourceLocation,
111        /// Second definition location.
112        second: SourceLocation,
113    },
114
115    /// One or more structural references could not be resolved in source IR.
116    #[error("AST resolution failed:\n{}", format_resolution_errors(.0))]
117    AstResolution(Vec<crate::parse::ast_resolution::AstResolutionError>),
118
119    /// AST canonicalization pass failed (view body normalization or reference
120    /// resolution error).
121    #[error("{0}")]
122    AstCanon(crate::parse::ast_canon::AstCanonError),
123
124    /// A string could not be parsed as a valid identifier.
125    #[error("invalid identifier {0:?}: {1}")]
126    InvalidIdentifier(String, String),
127
128    // ── Publication parse errors ─────────────────────────────────────────────
129    /// A publication with this name was declared more than once.
130    #[error("{1}: publication {0:?} declared more than once")]
131    DuplicatePublication(crate::identifier::Identifier, SourceLocation),
132
133    /// `FOR ALL TABLES` was combined with explicit object specs.
134    #[error(
135        "{1}: publication {0:?}: FOR ALL TABLES cannot be combined with \
136         FOR TABLE or FOR TABLES IN SCHEMA"
137    )]
138    PublicationAllTablesWithObjects(crate::identifier::Identifier, SourceLocation),
139
140    /// A publication object spec node had an unexpected shape.
141    #[error("{1}: publication {0:?}: malformed publication object spec node")]
142    PublicationObjectMalformed(crate::identifier::Identifier, SourceLocation),
143
144    /// `FOR TABLES IN CURRENT SCHEMA` is not declarative — pgevolve rejects it.
145    #[error(
146        "{1}: publication {0:?}: FOR TABLES IN CURRENT SCHEMA is not supported \
147         (not declarative; use explicit schema names)"
148    )]
149    PublicationCurrentSchemaForm(crate::identifier::Identifier, SourceLocation),
150
151    /// An unrecognized `PublicationObjSpecType` integer was encountered.
152    #[error(
153        "{2}: publication {1:?}: unknown publication object type {0} \
154         (expected 1=TABLE, 2=TABLES IN SCHEMA, 3=CUR_SCHEMA)"
155    )]
156    UnknownPublicationObjectType(i32, crate::identifier::Identifier, SourceLocation),
157
158    /// A table in a publication was not schema-qualified.
159    #[error("{1}: publication {0:?}: table must be schema-qualified")]
160    UnqualifiedPublicationTable(crate::identifier::Identifier, SourceLocation),
161
162    /// Failed to parse the row filter expression for a published table.
163    #[error("{3}: publication {0:?} table {1}: row filter parse error: {2}")]
164    PublicationFilterParse(
165        crate::identifier::Identifier,
166        crate::identifier::QualifiedName,
167        String,
168        SourceLocation,
169    ),
170
171    /// A `WITH (...)` option node for a publication had an unexpected shape.
172    #[error("{1}: publication {0:?}: malformed publication option node")]
173    PublicationOptionMalformed(crate::identifier::Identifier, SourceLocation),
174
175    /// An unrecognized key appeared in a publication `WITH (...)` clause.
176    #[error("{2}: publication {1:?}: unknown publication option {0:?}")]
177    UnknownPublicationOption(String, crate::identifier::Identifier, SourceLocation),
178
179    /// An unrecognized value appeared in a `publish = '...'` clause.
180    #[error(
181        "{2}: publication {1:?}: unknown publish kind {0:?} \
182         (valid: insert, update, delete, truncate)"
183    )]
184    UnknownPublishKind(String, crate::identifier::Identifier, SourceLocation),
185
186    /// A `publish = '...'` clause was empty (no DML kinds enabled).
187    #[error("{1}: publication {0:?}: empty publish list — at least one DML kind required")]
188    EmptyPublishBitset(crate::identifier::Identifier, SourceLocation),
189
190    /// A `CREATE PUBLICATION p` had no scope clause (empty selective).
191    #[error(
192        "{1}: publication {0:?}: no scope clause — add FOR ALL TABLES, \
193         FOR TABLE ..., or FOR TABLES IN SCHEMA ..."
194    )]
195    EmptyPublicationScope(crate::identifier::Identifier, SourceLocation),
196
197    /// `ALTER PUBLICATION p RENAME TO q` is not supported.
198    #[error("{1}: publication {0:?}: RENAME is not supported in pgevolve")]
199    PublicationRenameNotSupported(crate::identifier::Identifier, SourceLocation),
200
201    /// `ALTER PUBLICATION p ...` appeared before the matching `CREATE PUBLICATION p`.
202    #[error("{1}: publication {0:?}: ALTER PUBLICATION before CREATE PUBLICATION")]
203    AlterPublicationBeforeCreate(crate::identifier::Identifier, SourceLocation),
204
205    // ── Subscription parse errors ────────────────────────────────────────────
206    /// A subscription with this name was declared more than once.
207    #[error("{1}: subscription {0:?} declared more than once")]
208    DuplicateSubscription(crate::identifier::Identifier, SourceLocation),
209
210    /// `CREATE SUBSCRIPTION` had an empty CONNECTION string.
211    #[error("{1}: subscription {0:?}: CONNECTION string must not be empty")]
212    SubscriptionEmptyConnection(crate::identifier::Identifier, SourceLocation),
213
214    /// `CREATE SUBSCRIPTION` had an empty PUBLICATION list.
215    #[error("{1}: subscription {0:?}: PUBLICATION list must not be empty")]
216    SubscriptionEmptyPublications(crate::identifier::Identifier, SourceLocation),
217
218    /// A `WITH (...)` option node for a subscription had an unexpected shape.
219    #[error("{1}: subscription {0:?}: malformed subscription option node")]
220    SubscriptionOptionMalformed(crate::identifier::Identifier, SourceLocation),
221
222    /// An unrecognized key appeared in a subscription `WITH (...)` clause.
223    #[error("{2}: subscription {1:?}: unknown subscription option {0:?}")]
224    UnknownSubscriptionOption(String, crate::identifier::Identifier, SourceLocation),
225
226    /// An unrecognized value appeared in a `streaming = ...` clause.
227    #[error(
228        "{2}: subscription {1:?}: unknown streaming mode {0:?} \
229         (valid: off, on, parallel)"
230    )]
231    UnknownStreamingMode(String, crate::identifier::Identifier, SourceLocation),
232
233    /// An unrecognized value appeared in an `origin = ...` clause.
234    #[error(
235        "{2}: subscription {1:?}: unknown origin mode {0:?} \
236         (valid: any, none)"
237    )]
238    UnknownOriginMode(String, crate::identifier::Identifier, SourceLocation),
239
240    /// `ALTER SUBSCRIPTION … REFRESH PUBLICATION` is not supported.
241    #[error("{1}: subscription {0:?}: REFRESH PUBLICATION is not supported in pgevolve")]
242    SubscriptionRefreshNotSupported(crate::identifier::Identifier, SourceLocation),
243
244    /// `ALTER SUBSCRIPTION … SKIP (…)` is not supported.
245    #[error("{1}: subscription {0:?}: SKIP is not supported in pgevolve")]
246    SubscriptionSkipNotSupported(crate::identifier::Identifier, SourceLocation),
247
248    /// `ALTER SUBSCRIPTION … ENABLE/DISABLE` (standalone, without SET) is not
249    /// supported. Declare `WITH (enabled = true/false)` in source instead.
250    #[error(
251        "{1}: subscription {0:?}: standalone ENABLE/DISABLE is not supported — \
252         use WITH (enabled = true/false) in source"
253    )]
254    SubscriptionStandaloneEnableDisableNotSupported(crate::identifier::Identifier, SourceLocation),
255
256    /// `ALTER SUBSCRIPTION … RENAME TO …` is not supported.
257    #[error("{1}: subscription {0:?}: RENAME is not supported in pgevolve")]
258    SubscriptionRenameNotSupported(crate::identifier::Identifier, SourceLocation),
259
260    /// `ALTER SUBSCRIPTION p …` appeared before the matching `CREATE SUBSCRIPTION p`.
261    #[error("{1}: subscription {0:?}: ALTER SUBSCRIPTION before CREATE SUBSCRIPTION")]
262    AlterSubscriptionBeforeCreate(crate::identifier::Identifier, SourceLocation),
263}
264
265fn format_resolution_errors(errs: &[crate::parse::ast_resolution::AstResolutionError]) -> String {
266    let mut s = String::new();
267    for (i, e) in errs.iter().enumerate() {
268        if i > 0 {
269            s.push('\n');
270        }
271        s.push_str("  - ");
272        s.push_str(&e.to_string());
273    }
274    s
275}