1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
//! Universal lint rules. Spec §12.1.
//!
//! Most of §12.1 is already enforced by the parser (unsupported kinds,
//! duplicate qnames, schema qualification, ALTER outside the FK whitelist).
//! Rules that the parser can't enforce:
//!
//! - **`managed_schemas_match`** — every schema declared in source has a
//! matching `managed.schemas` entry, and vice versa.
//! - **`closed_world_references`** — every FK target table exists in source.
//! - **`view_shadows_table`** — a view or MV must not share a qname with a
//! table (PG would reject the conflict at apply time).
//! - **`mv_no_unique_index`** — an MV without a unique index cannot be
//! refreshed concurrently.
//! - **`view_body_references_unmanaged_schema`** — a view body dependency
//! targets a schema outside `[managed].schemas` and outside built-ins.
//! - **`type-shadows-table`** — a user-defined type's qname collides with a
//! table, view, or MV qname (PG uses one namespace for relations and types).
//! - **`enum-value-collision`** — an enum has duplicate value labels.
//! - **`composite-attribute-collision`** — a composite has duplicate attribute
//! names.
//! - **`domain-check-references-unmanaged-type`** — a domain's CHECK expression
//! references a schema not in `[managed].schemas` and not a PG built-in.
//! - **`pl-pgsql-dynamic-sql`** — a PL/pgSQL function or procedure uses
//! `EXECUTE` (dynamic SQL) without any `-- @pgevolve dep: <qname>` directive.
//! - **`procedure-contains-commit`** — a procedure body contains
//! `COMMIT`/`ROLLBACK`; pgevolve will run it outside a transaction.
//! - **`function-references-unmanaged-schema`** — a function or procedure body
//! dependency targets a schema outside `[managed].schemas` and outside
//! built-ins.
//! - **`extension-version-unpinned`** — fires when a source-declared
//! extension lacks a `VERSION` clause.
//! - **`extension-references-unmanaged-schema`** — fires when
//! `CREATE EXTENSION ... WITH SCHEMA s` references a schema not in
//! the source catalog.
//! - **`trigger-references-unmanaged-table`** — fires when a trigger's target
//! table is not declared in source as a table, view, or materialized view.
//! - **`trigger-references-unmanaged-function`** — fires when a trigger's
//! execute function is not declared in source.
//! - **`force-rls-without-policies`** — fires (Warning) when a table has
//! `FORCE ROW LEVEL SECURITY` enabled but no policies defined. PG denies
//! every row in that state — almost always a configuration mistake.
//! - **`publication-feature-requires-pg-version`** — fires (Error) when source
//! uses a PG 15+ publication feature (`FOR TABLES IN SCHEMA`, row filters,
//! column lists) and `[managed].min_pg_version < 15`. Run via
//! [`check_plan_time_catalog`].
//! - **`column-references-unmanaged-collation`** — fires (Warning) when a
//! column, domain, range, or composite attribute references a collation that
//! is neither a Postgres built-in nor declared in `source.collations`. Run
//! via [`check_plan_time_catalog`].
//! - **`range-type-references-unmanaged-subtype`** — fires (Warning) when a
//! `CREATE TYPE … AS RANGE` declares a subtype that is neither a known
//! built-in scalar nor a managed user-defined type. Run via
//! [`check_plan_time_catalog`].
//! - **`nondeterministic-collation-requires-pg-12`** — fires (Error) when a
//! source collation has `deterministic = false` and
//! `[managed].min_pg_version < 12`. Run via [`check_plan_time_catalog`].
//! - **`builtin-provider-requires-pg-17`** — fires (Error) when a source
//! collation uses `provider = builtin` and `[managed].min_pg_version < 17`.
//! Run via [`check_plan_time_catalog`].
//!
//! Drift-detection rules (comparing source vs live catalog, run via [`run_drift_lints`]):
//!
//! - **`unmanaged-publication`** — catalog reports a publication source doesn't
//! declare. Standard v0.3.x lenient-drift pattern.
//! - **`unmanaged-collation`** — catalog reports a collation source doesn't
//! declare. Standard v0.3.x lenient-drift pattern.
//! - **`publication-captures-unmanaged-table`** — a `FOR ALL TABLES` or
//! `FOR TABLES IN SCHEMA` publication implicitly captures tables not in source.
//! - **`publication-row-filter-references-unmanaged-column`** — a row filter
//! expression references a column not declared on the target table in source.
//! - **`table-access-method-change`** — fires (Warning) when an existing table's
//! access method differs between source and live. pgevolve never rewrites a
//! table's access method; the operator must run `ALTER TABLE … SET ACCESS
//! METHOD` manually (PG 15+) if intended.
//!
//! Changeset-level rules (inspecting the diff, run via [`check_changeset`]):
//!
//! - **`storage-downgrade-not-retroactive`** — fires when a `SET STORAGE`
//! change reduces toastability (e.g., EXTERNAL → PLAIN). Existing `TOASTed`
//! values keep their current placement until rewritten by UPDATE or VACUUM
//! FULL.
//! - **`compression-change-not-retroactive`** — fires on any `SET COMPRESSION`
//! change. Existing `TOASTed` values keep their original codec until rewritten
//! by UPDATE or VACUUM FULL.
//! - **`grants-to-unmanaged-role`** — fires (Warning) when the catalog has
//! grants to roles not declared in source. The differ already filtered these
//! out of REVOKE (lenient drift policy); the lint surfaces them so operators
//! can decide whether to manage them or accept the drift.
//! - **`revoke-from-owner`** — fires (Error) when a REVOKE step targets the
//! object's owner. PG silently rejects (owner has implicit privileges); we
//! pre-empt with a clear plan-time error.
//!
//! Cluster changeset-level rules (run via [`check_cluster_changeset`]):
//!
//! - **`role-loses-superuser`** — fires when `AlterRoleAttributes` flips
//! `SUPERUSER` from `true` to `false`. Losing superuser is rarely routine;
//! surfacing it lets operators catch unintended downgrades.
//! - **`role-membership-cycle`** — builds the post-apply membership graph from
//! source IR + pending grants/revokes and checks that no pending grant creates
//! a cycle. Postgres rejects cycles at apply time; pre-plan detection gives a
//! better error.
//! - **`tablespace-location-drift`** — fires (Warning) when a tablespace exists
//! in both source and live with a different `LOCATION`. Postgres cannot
//! relocate tablespaces; pgevolve never auto-changes it, so the finding is
//! advisory — the operator must recreate the tablespace manually.
//!
//! Cluster-aware source-tree rules (run via [`check_universal_with_cluster`]):
//!
//! - **`grant-references-unknown-role`** — fires when a grantee role name (on
//! any object grant, owner field, or default-privilege grant) is not declared
//! in the linked cluster project's roles. Silently no-ops when no
//! `[cluster].project` is configured.
use ManagedConfig;
use Finding;
use rules;
use SourceTree;
use crateChangeSet;
use crateCatalog;
/// All rule IDs that carry [`crate::lint::Severity::LintAtPlan`].
///
/// Preflight uses this to warn about waivers that reference unknown rule IDs
/// (typos, stale waivers for renamed rules). Add new `LintAtPlan` rule IDs
/// here when introducing them; remove stale ones when a rule is deleted.
pub const LINT_AT_PLAN_RULES: & = &;
/// Run every universal rule.
/// Run all drift-detection rules that compare `source` against a `target`
/// catalog (e.g. the live database). Returns a list of [`Finding`]s.
///
/// This is the entry point for lint rules that need both the source and target
/// catalogs, as opposed to [`check_universal`] which only needs the source tree.
/// Run all changeset-level lint rules against `cs`.
///
/// These rules inspect the diff produced by the differ — they fire on
/// structural changes (e.g., `SetColumnStorage`, `SetColumnCompression`) rather
/// than on the source catalog directly.
/// Run catalog-level lint rules that fire at plan time.
///
/// This is a subset of [`check_universal`] — rules that inspect the source
/// catalog (desired state) and do not require the full `SourceTree` context
/// (managed config, file locations, etc.). Intended for callers such as the
/// conformance pipeline that have a `Catalog` but not a full `SourceTree`.
///
/// `min_pg_version` is the minimum Postgres major version the project targets
/// (from `[managed].min_pg_version`; default `14`). Used to gate PG-version-
/// specific source features at lint time.
///
/// Currently includes:
/// - **`force-rls-without-policies`** — fires when a table has FORCE ROW LEVEL
/// SECURITY but no policies defined.
/// - **`publication-feature-requires-pg-version`** — fires (Error) when source
/// uses a PG 15+ publication feature with `min_pg_version < 15`.
/// - **`subscription-references-undeclared-publication`** — fires (Warning) when
/// a source subscription's PUBLICATION list names a publication not declared
/// in source.
/// - **`subscription-feature-requires-pg-version`** — fires (Error) when source
/// uses a PG-version-gated subscription option below `min_pg_version`.
/// - **`subscription-password-in-source`** — fires (Error) when a source
/// subscription CONNECTION contains a plaintext `password=` value instead of
/// a `${ENV_VAR}` reference.
/// - **`column-references-unmanaged-collation`** — fires (Warning) when a
/// column / domain / range / composite-attribute references a collation that
/// is neither a Postgres built-in nor declared in source.
/// - **`range-type-references-unmanaged-subtype`** — fires (Warning) when a
/// `CREATE TYPE … AS RANGE` declares a subtype that is neither a known
/// Postgres built-in scalar nor a managed user-defined type.
/// - **`nondeterministic-collation-requires-pg-12`** — fires (Error) when a
/// source collation has `deterministic = false` and `min_pg_version < 12`.
/// - **`builtin-provider-requires-pg-17`** — fires (Error) when a source
/// collation uses `provider = builtin` and `min_pg_version < 17`.
/// Like [`check_universal`] but also runs cluster-aware source-tree lints.
///
/// `cluster_role_names`: the set of role names declared in the linked cluster
/// project's roles/*.sql, or `None` if no `[cluster].project` is configured.
/// When `None`, cluster-aware rules silently no-op so per-DB independence is
/// preserved.
/// Run all cluster changeset-level lint rules.
///
/// Mirrors [`check_changeset`] for per-DB lints. Takes both `source` (desired
/// state) and `target` (live state) cluster catalogs for catalog-comparison
/// rules, plus `cs` (the pending cluster changeset) for changeset rules.
///
/// Rules run here:
/// - **`role-loses-superuser`** — fires when `AlterRoleAttributes` flips
/// `SUPERUSER` from `true` to `false`.
/// - **`role-membership-cycle`** — detects post-apply membership graph cycles.
/// - **`tablespace-location-drift`** — fires when a tablespace exists in both
/// source and target with a different `LOCATION` (Postgres cannot relocate
/// tablespaces; the user must recreate manually).