Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4e57995

Browse files
authoredMay 19, 2025
chore(schema_cache): add query for triggers (#398)
1 parent 2ada420 commit 4e57995

14 files changed

+378
-9
lines changed
 

‎.sqlx/query-df57cc22f7d63847abce1d0d15675ba8951faa1be2ea6b2bf6714b1aa9127a6f.json

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/pgt_schema_cache/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pgt_diagnostics.workspace = true
2020
serde.workspace = true
2121
serde_json.workspace = true
2222
sqlx.workspace = true
23+
strum = { workspace = true }
2324
tokio.workspace = true
2425

2526
[dev-dependencies]

‎crates/pgt_schema_cache/src/columns.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl From<char> for ColumnClassKind {
3737
}
3838
}
3939

40-
#[derive(Debug, Clone, PartialEq, Eq)]
40+
#[derive(Debug, PartialEq, Eq)]
4141
pub struct Column {
4242
pub name: String,
4343

‎crates/pgt_schema_cache/src/functions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl From<Option<JsonValue>> for FunctionArgs {
5858
}
5959
}
6060

61-
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61+
#[derive(Debug, Default, Serialize, Deserialize)]
6262
pub struct Function {
6363
/// The Id (`oid`).
6464
pub id: i64,

‎crates/pgt_schema_cache/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod policies;
88
mod schema_cache;
99
mod schemas;
1010
mod tables;
11+
mod triggers;
1112
mod types;
1213
mod versions;
1314

@@ -16,3 +17,4 @@ pub use functions::{Behavior, Function, FunctionArg, FunctionArgs};
1617
pub use schema_cache::SchemaCache;
1718
pub use schemas::Schema;
1819
pub use tables::{ReplicaIdentity, Table};
20+
pub use triggers::{Trigger, TriggerAffected, TriggerEvent};

‎crates/pgt_schema_cache/src/policies.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl From<PolicyQueried> for Policy {
5454
}
5555
}
5656

57-
#[derive(Debug, Clone, PartialEq, Eq)]
57+
#[derive(Debug, PartialEq, Eq)]
5858
pub struct Policy {
5959
name: String,
6060
table_name: String,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- we need to join tables from the pg_catalog since "TRUNCATE" triggers are
2+
-- not available in the information_schema.trigger table.
3+
select
4+
t.tgname as "name!",
5+
c.relname as "table_name!",
6+
p.proname as "proc_name!",
7+
n.nspname as "schema_name!",
8+
t.tgtype as "details_bitmask!"
9+
from
10+
pg_catalog.pg_trigger t
11+
left join pg_catalog.pg_proc p on t.tgfoid = p.oid
12+
left join pg_catalog.pg_class c on t.tgrelid = c.oid
13+
left join pg_catalog.pg_namespace n on c.relnamespace = n.oid
14+
where
15+
-- triggers enforcing constraints (e.g. unique fields) should not be included.
16+
t.tgisinternal = false and
17+
t.tgconstraint = 0;

‎crates/pgt_schema_cache/src/schema_cache.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use sqlx::postgres::PgPool;
22

3+
use crate::Trigger;
34
use crate::columns::Column;
45
use crate::functions::Function;
56
use crate::policies::Policy;
@@ -8,7 +9,7 @@ use crate::tables::Table;
89
use crate::types::PostgresType;
910
use crate::versions::Version;
1011

11-
#[derive(Debug, Clone, Default)]
12+
#[derive(Debug, Default)]
1213
pub struct SchemaCache {
1314
pub schemas: Vec<Schema>,
1415
pub tables: Vec<Table>,
@@ -17,18 +18,20 @@ pub struct SchemaCache {
1718
pub versions: Vec<Version>,
1819
pub columns: Vec<Column>,
1920
pub policies: Vec<Policy>,
21+
pub triggers: Vec<Trigger>,
2022
}
2123

2224
impl SchemaCache {
2325
pub async fn load(pool: &PgPool) -> Result<SchemaCache, sqlx::Error> {
24-
let (schemas, tables, functions, types, versions, columns, policies) = futures_util::try_join!(
26+
let (schemas, tables, functions, types, versions, columns, policies, triggers) = futures_util::try_join!(
2527
Schema::load(pool),
2628
Table::load(pool),
2729
Function::load(pool),
2830
PostgresType::load(pool),
2931
Version::load(pool),
3032
Column::load(pool),
3133
Policy::load(pool),
34+
Trigger::load(pool),
3235
)?;
3336

3437
Ok(SchemaCache {
@@ -39,6 +42,7 @@ impl SchemaCache {
3942
versions,
4043
columns,
4144
policies,
45+
triggers,
4246
})
4347
}
4448

‎crates/pgt_schema_cache/src/schemas.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use sqlx::PgPool;
22

33
use crate::schema_cache::SchemaCacheItem;
44

5-
#[derive(Debug, Clone, Default)]
5+
#[derive(Debug, Default)]
66
pub struct Schema {
77
pub id: i64,
88
pub name: String,

‎crates/pgt_schema_cache/src/tables.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl From<String> for ReplicaIdentity {
2323
}
2424
}
2525

26-
#[derive(Debug, Clone, Default, PartialEq, Eq)]
26+
#[derive(Debug, Default, PartialEq, Eq)]
2727
pub struct Table {
2828
pub id: i64,
2929
pub schema: String,
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
use crate::schema_cache::SchemaCacheItem;
2+
use strum::{EnumIter, IntoEnumIterator};
3+
4+
#[derive(Debug, PartialEq, Eq)]
5+
pub enum TriggerAffected {
6+
Row,
7+
Statement,
8+
}
9+
10+
impl From<i16> for TriggerAffected {
11+
fn from(value: i16) -> Self {
12+
let is_row = 0b0000_0001;
13+
if value & is_row == is_row {
14+
Self::Row
15+
} else {
16+
Self::Statement
17+
}
18+
}
19+
}
20+
21+
#[derive(Debug, PartialEq, Eq, EnumIter)]
22+
pub enum TriggerEvent {
23+
Insert,
24+
Delete,
25+
Update,
26+
Truncate,
27+
}
28+
29+
struct TriggerEvents(Vec<TriggerEvent>);
30+
31+
impl From<i16> for TriggerEvents {
32+
fn from(value: i16) -> Self {
33+
Self(
34+
TriggerEvent::iter()
35+
.filter(|variant| {
36+
#[rustfmt::skip]
37+
let mask = match variant {
38+
TriggerEvent::Insert => 0b0000_0100,
39+
TriggerEvent::Delete => 0b0000_1000,
40+
TriggerEvent::Update => 0b0001_0000,
41+
TriggerEvent::Truncate => 0b0010_0000,
42+
};
43+
mask & value == mask
44+
})
45+
.collect(),
46+
)
47+
}
48+
}
49+
50+
#[derive(Debug, PartialEq, Eq, EnumIter)]
51+
pub enum TriggerTiming {
52+
Before,
53+
After,
54+
Instead,
55+
}
56+
57+
impl TryFrom<i16> for TriggerTiming {
58+
type Error = ();
59+
fn try_from(value: i16) -> Result<Self, ()> {
60+
TriggerTiming::iter()
61+
.find(|variant| {
62+
match variant {
63+
TriggerTiming::Instead => {
64+
let mask = 0b0100_0000;
65+
mask & value == mask
66+
}
67+
TriggerTiming::Before => {
68+
let mask = 0b0000_0010;
69+
mask & value == mask
70+
}
71+
TriggerTiming::After => {
72+
let mask = 0b1011_1101;
73+
// timing is "AFTER" if neither INSTEAD nor BEFORE bit are set.
74+
mask | value == mask
75+
}
76+
}
77+
})
78+
.ok_or(())
79+
}
80+
}
81+
82+
pub struct TriggerQueried {
83+
name: String,
84+
table_name: String,
85+
schema_name: String,
86+
proc_name: String,
87+
details_bitmask: i16,
88+
}
89+
90+
#[derive(Debug, PartialEq, Eq)]
91+
pub struct Trigger {
92+
name: String,
93+
table_name: String,
94+
schema_name: String,
95+
proc_name: String,
96+
affected: TriggerAffected,
97+
timing: TriggerTiming,
98+
events: Vec<TriggerEvent>,
99+
}
100+
101+
impl From<TriggerQueried> for Trigger {
102+
fn from(value: TriggerQueried) -> Self {
103+
Self {
104+
name: value.name,
105+
table_name: value.table_name,
106+
proc_name: value.proc_name,
107+
schema_name: value.schema_name,
108+
affected: value.details_bitmask.into(),
109+
timing: value.details_bitmask.try_into().unwrap(),
110+
events: TriggerEvents::from(value.details_bitmask).0,
111+
}
112+
}
113+
}
114+
115+
impl SchemaCacheItem for Trigger {
116+
type Item = Trigger;
117+
118+
async fn load(pool: &sqlx::PgPool) -> Result<Vec<Self::Item>, sqlx::Error> {
119+
let results = sqlx::query_file_as!(TriggerQueried, "src/queries/triggers.sql")
120+
.fetch_all(pool)
121+
.await?;
122+
123+
Ok(results.into_iter().map(|r| r.into()).collect())
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use pgt_test_utils::test_database::get_new_test_db;
130+
use sqlx::Executor;
131+
132+
use crate::{
133+
SchemaCache,
134+
triggers::{TriggerAffected, TriggerEvent, TriggerTiming},
135+
};
136+
137+
#[tokio::test]
138+
async fn loads_triggers() {
139+
let test_db = get_new_test_db().await;
140+
141+
let setup = r#"
142+
create table public.users (
143+
id serial primary key,
144+
name text
145+
);
146+
147+
create or replace function public.log_user_insert()
148+
returns trigger as $$
149+
begin
150+
-- dummy body
151+
return new;
152+
end;
153+
$$ language plpgsql;
154+
155+
create trigger trg_users_insert
156+
before insert on public.users
157+
for each row
158+
execute function public.log_user_insert();
159+
160+
create trigger trg_users_update
161+
after update or insert on public.users
162+
for each statement
163+
execute function public.log_user_insert();
164+
165+
create trigger trg_users_delete
166+
before delete on public.users
167+
for each row
168+
execute function public.log_user_insert();
169+
"#;
170+
171+
test_db
172+
.execute(setup)
173+
.await
174+
.expect("Failed to setup test database");
175+
176+
let cache = SchemaCache::load(&test_db)
177+
.await
178+
.expect("Failed to load Schema Cache");
179+
180+
let triggers: Vec<_> = cache
181+
.triggers
182+
.iter()
183+
.filter(|t| t.table_name == "users")
184+
.collect();
185+
assert_eq!(triggers.len(), 3);
186+
187+
let insert_trigger = triggers
188+
.iter()
189+
.find(|t| t.name == "trg_users_insert")
190+
.unwrap();
191+
assert_eq!(insert_trigger.schema_name, "public");
192+
assert_eq!(insert_trigger.table_name, "users");
193+
assert_eq!(insert_trigger.timing, TriggerTiming::Before);
194+
assert_eq!(insert_trigger.affected, TriggerAffected::Row);
195+
assert!(insert_trigger.events.contains(&TriggerEvent::Insert));
196+
assert_eq!(insert_trigger.proc_name, "log_user_insert");
197+
198+
let update_trigger = triggers
199+
.iter()
200+
.find(|t| t.name == "trg_users_update")
201+
.unwrap();
202+
assert_eq!(insert_trigger.schema_name, "public");
203+
assert_eq!(insert_trigger.table_name, "users");
204+
assert_eq!(update_trigger.timing, TriggerTiming::After);
205+
assert_eq!(update_trigger.affected, TriggerAffected::Statement);
206+
assert!(update_trigger.events.contains(&TriggerEvent::Update));
207+
assert!(update_trigger.events.contains(&TriggerEvent::Insert));
208+
assert_eq!(update_trigger.proc_name, "log_user_insert");
209+
210+
let delete_trigger = triggers
211+
.iter()
212+
.find(|t| t.name == "trg_users_delete")
213+
.unwrap();
214+
assert_eq!(insert_trigger.schema_name, "public");
215+
assert_eq!(insert_trigger.table_name, "users");
216+
assert_eq!(delete_trigger.timing, TriggerTiming::Before);
217+
assert_eq!(delete_trigger.affected, TriggerAffected::Row);
218+
assert!(delete_trigger.events.contains(&TriggerEvent::Delete));
219+
assert_eq!(delete_trigger.proc_name, "log_user_insert");
220+
}
221+
222+
#[tokio::test]
223+
async fn loads_instead_and_truncate_triggers() {
224+
let test_db = get_new_test_db().await;
225+
226+
let setup = r#"
227+
create table public.docs (
228+
id serial primary key,
229+
content text
230+
);
231+
232+
create view public.docs_view as
233+
select * from public.docs;
234+
235+
create or replace function public.docs_instead_of_update()
236+
returns trigger as $$
237+
begin
238+
-- dummy body
239+
return new;
240+
end;
241+
$$ language plpgsql;
242+
243+
create trigger trg_docs_instead_update
244+
instead of update on public.docs_view
245+
for each row
246+
execute function public.docs_instead_of_update();
247+
248+
create or replace function public.docs_truncate()
249+
returns trigger as $$
250+
begin
251+
-- dummy body
252+
return null;
253+
end;
254+
$$ language plpgsql;
255+
256+
create trigger trg_docs_truncate
257+
after truncate on public.docs
258+
for each statement
259+
execute function public.docs_truncate();
260+
"#;
261+
262+
test_db
263+
.execute(setup)
264+
.await
265+
.expect("Failed to setup test database");
266+
267+
let cache = SchemaCache::load(&test_db)
268+
.await
269+
.expect("Failed to load Schema Cache");
270+
271+
let triggers: Vec<_> = cache
272+
.triggers
273+
.iter()
274+
.filter(|t| t.table_name == "docs" || t.table_name == "docs_view")
275+
.collect();
276+
assert_eq!(triggers.len(), 2);
277+
278+
let instead_trigger = triggers
279+
.iter()
280+
.find(|t| t.name == "trg_docs_instead_update")
281+
.unwrap();
282+
assert_eq!(instead_trigger.schema_name, "public");
283+
assert_eq!(instead_trigger.table_name, "docs_view");
284+
assert_eq!(instead_trigger.timing, TriggerTiming::Instead);
285+
assert_eq!(instead_trigger.affected, TriggerAffected::Row);
286+
assert!(instead_trigger.events.contains(&TriggerEvent::Update));
287+
assert_eq!(instead_trigger.proc_name, "docs_instead_of_update");
288+
289+
let truncate_trigger = triggers
290+
.iter()
291+
.find(|t| t.name == "trg_docs_truncate")
292+
.unwrap();
293+
assert_eq!(truncate_trigger.schema_name, "public");
294+
assert_eq!(truncate_trigger.table_name, "docs");
295+
assert_eq!(truncate_trigger.timing, TriggerTiming::After);
296+
assert_eq!(truncate_trigger.affected, TriggerAffected::Statement);
297+
assert!(truncate_trigger.events.contains(&TriggerEvent::Truncate));
298+
assert_eq!(truncate_trigger.proc_name, "docs_truncate");
299+
}
300+
}

‎crates/pgt_schema_cache/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl From<Option<JsonValue>> for Enums {
3636
}
3737
}
3838

39-
#[derive(Debug, Clone, Default)]
39+
#[derive(Debug, Default)]
4040
pub struct PostgresType {
4141
pub id: i64,
4242
pub name: String,

‎crates/pgt_schema_cache/src/versions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use sqlx::PgPool;
22

33
use crate::schema_cache::SchemaCacheItem;
44

5-
#[derive(Debug, Clone, Default)]
5+
#[derive(Debug, Default)]
66
pub struct Version {
77
pub version: Option<String>,
88
pub version_num: Option<i64>,

0 commit comments

Comments
 (0)
Please sign in to comment.