pgrx_sql_entity_graph/pg_trigger/
mod.rs

1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10/*!
11
12`#[pg_trigger]` related macro expansion for Rust to SQL translation
13
14> Like all of the [`sql_entity_graph`][crate] APIs, this is considered **internal**
15> to the `pgrx` framework and very subject to change between versions. While you may use this, please do it with caution.
16
17*/
18pub mod attribute;
19pub mod entity;
20
21use crate::enrich::{ToEntityGraphTokens, ToRustCodeTokens};
22use crate::finfo::{finfo_v1_extern_c, finfo_v1_tokens};
23use crate::{CodeEnrichment, ToSqlConfig};
24use attribute::PgTriggerAttribute;
25use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
26use quote::{format_ident, quote};
27use syn::{spanned::Spanned, ItemFn, Token};
28
29#[derive(Debug, Clone)]
30pub struct PgTrigger {
31    func: syn::ItemFn,
32    to_sql_config: ToSqlConfig,
33}
34
35impl PgTrigger {
36    pub fn new(
37        func: ItemFn,
38        attributes: syn::punctuated::Punctuated<PgTriggerAttribute, Token![,]>,
39    ) -> Result<CodeEnrichment<Self>, syn::Error> {
40        if attributes.len() > 1 {
41            // FIXME: add a UI test for this
42            return Err(syn::Error::new(
43                func.span(),
44                "Multiple `sql` arguments found, it must be unique",
45            ));
46        };
47        let to_sql_config = attributes
48            .first()
49            .cloned()
50            .map(|PgTriggerAttribute::Sql(mut config)| {
51                if let Some(ref mut content) = config.content {
52                    let value = content.value();
53                    // FIXME: find out if we should be using synthetic spans, issue #1667
54                    let span = content.span();
55                    let updated_value = value
56                        .replace("@FUNCTION_NAME@", &(func.sig.ident.to_string() + "_wrapper"))
57                        + "\n";
58                    *content = syn::LitStr::new(&updated_value, span);
59                };
60                config
61            })
62            .unwrap_or_default();
63
64        if !to_sql_config.overrides_default() {
65            crate::ident_is_acceptable_to_postgres(&func.sig.ident)?;
66        }
67
68        Ok(CodeEnrichment(PgTrigger { func, to_sql_config }))
69    }
70
71    pub fn wrapper_tokens(&self) -> Result<ItemFn, syn::Error> {
72        let function_ident = self.func.sig.ident.clone();
73        let fcinfo_ident =
74            Ident::new("_fcinfo", Span::mixed_site().located_at(function_ident.span()));
75
76        let tokens = quote! {
77            fn _internal(fcinfo: ::pgrx::pg_sys::FunctionCallInfo) -> ::pgrx::pg_sys::Datum {
78                let fcinfo_ref = unsafe {
79                    // SAFETY:  The caller should be Postgres in this case and it will give us a valid "fcinfo" pointer
80                    fcinfo.as_ref().expect("fcinfo was NULL from Postgres")
81                };
82                let maybe_pg_trigger = unsafe { ::pgrx::trigger_support::PgTrigger::from_fcinfo(fcinfo_ref) };
83                let pg_trigger = maybe_pg_trigger.expect("PgTrigger::from_fcinfo failed");
84                let trigger_fn_result: Result<
85                    Option<::pgrx::heap_tuple::PgHeapTuple<'_, _>>,
86                    _,
87                > = #function_ident(&pg_trigger);
88
89
90                // The trigger "protocol" allows a function to return the null pointer, but NOT to
91                // set the isnull result flag.  This is why we return `Datum::from(0)` in the None cases
92                let trigger_retval = trigger_fn_result.expect("Trigger function panic");
93                match trigger_retval {
94                    None => unsafe { ::pgrx::pg_sys::Datum::from(0) },
95                    Some(trigger_retval) => match trigger_retval.into_trigger_datum() {
96                        None => unsafe { ::pgrx::pg_sys::Datum::from(0) },
97                        Some(datum) => datum,
98                    }
99                }
100            }
101            ::pgrx::pg_sys::submodules::panic::pgrx_extern_c_guard(move || _internal(#fcinfo_ident))
102        };
103
104        finfo_v1_extern_c(&self.func, fcinfo_ident, tokens)
105    }
106}
107
108impl ToEntityGraphTokens for PgTrigger {
109    fn to_entity_graph_tokens(&self) -> TokenStream2 {
110        let func_sig_ident = &self.func.sig.ident;
111        let sql_graph_entity_fn_name = format_ident!("__pgrx_internals_trigger_{}", func_sig_ident);
112        let function_name = func_sig_ident.to_string();
113        let to_sql_config = &self.to_sql_config;
114
115        quote! {
116            #[no_mangle]
117            #[doc(hidden)]
118            #[allow(unknown_lints, clippy::no_mangle_with_rust_abi, nonstandard_style)]
119            pub extern "Rust" fn #sql_graph_entity_fn_name() -> ::pgrx::pgrx_sql_entity_graph::SqlGraphEntity {
120                use core::any::TypeId;
121                extern crate alloc;
122                use alloc::vec::Vec;
123                use alloc::vec;
124                let submission = ::pgrx::pgrx_sql_entity_graph::PgTriggerEntity {
125                    function_name: #function_name,
126                    file: file!(),
127                    line: line!(),
128                    full_path: concat!(module_path!(), "::", stringify!(#func_sig_ident)),
129                    module_path: module_path!(),
130                    to_sql_config: #to_sql_config,
131                };
132                ::pgrx::pgrx_sql_entity_graph::SqlGraphEntity::Trigger(submission)
133            }
134        }
135    }
136}
137
138impl ToRustCodeTokens for PgTrigger {
139    fn to_rust_code_tokens(&self) -> TokenStream2 {
140        let wrapper_func = self.wrapper_tokens().expect("Generating wrapper function for trigger");
141        let finfo_func = finfo_v1_tokens(wrapper_func.sig.ident.clone()).unwrap();
142        let func = &self.func;
143
144        quote! {
145            #func
146            #wrapper_func
147            #finfo_func
148        }
149    }
150}