Skip to main content

pgevolve_core/ir/
function.rs

1//! User-defined functions (SQL or PL/pgSQL).
2
3use serde::{Deserialize, Serialize};
4
5use crate::identifier::{Identifier, QualifiedName};
6use crate::ir::column_type::ColumnType;
7use crate::ir::default_expr::NormalizedExpr;
8use crate::ir::difference::Difference;
9use crate::ir::eq::Diff;
10use crate::parse::normalize_body::NormalizedBody;
11use crate::plan::edges::DepEdge;
12
13/// A user-defined function (SQL or PL/pgSQL).
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Function {
16    /// Schema-qualified function name.
17    pub qname: QualifiedName,
18    /// Declared argument list (all modes).
19    pub args: Vec<FunctionArg>,
20    /// Normalized identity hash over IN/INOUT/VARIADIC args only.
21    pub arg_types_normalized: NormalizedArgTypes,
22    /// Return type.
23    pub return_type: ReturnType,
24    /// Implementation language.
25    pub language: FunctionLanguage,
26    /// Canonicalized function body.
27    pub body: NormalizedBody,
28    /// Dependency edges extracted from the function body AST.
29    ///
30    /// Filled by the T4 PL/pgSQL body parser and the T6 AST resolution pass.
31    /// Empty until those passes run.
32    #[serde(default)]
33    pub body_dependencies: Vec<DepEdge>,
34    /// Volatility category.
35    pub volatility: Volatility,
36    /// Whether the function returns NULL immediately for any NULL input.
37    pub strict: bool,
38    /// Security context (INVOKER or DEFINER).
39    pub security: SecurityMode,
40    /// Parallel safety classification.
41    pub parallel: ParallelSafety,
42    /// Whether the function is marked LEAKPROOF.
43    pub leakproof: bool,
44    /// Estimated per-call cost (units: sequential page fetches).
45    pub cost: Option<f32>,
46    /// Estimated rows returned per call (for set-returning functions).
47    pub rows: Option<f32>,
48    /// Optional `COMMENT ON FUNCTION` text.
49    pub comment: Option<String>,
50    /// Object owner. `None` = unmanaged (the differ ignores ownership).
51    /// `Some(role)` = managed: diff emits `ALTER FUNCTION ... OWNER TO role`.
52    pub owner: Option<crate::identifier::Identifier>,
53    /// Grants on this object. Empty = no grants. Canonicalized.
54    pub grants: Vec<crate::ir::grant::Grant>,
55}
56
57// f32 fields prevent deriving Eq, PartialEq, and Hash;
58// implement manually using bit patterns so that equality and hashing are
59// consistent (same bit pattern ↔ equal ↔ same hash).
60impl PartialEq for Function {
61    fn eq(&self, other: &Self) -> bool {
62        self.qname == other.qname
63            && self.args == other.args
64            && self.arg_types_normalized == other.arg_types_normalized
65            && self.return_type == other.return_type
66            && self.language == other.language
67            && self.body == other.body
68            && self.body_dependencies == other.body_dependencies
69            && self.volatility == other.volatility
70            && self.strict == other.strict
71            && self.security == other.security
72            && self.parallel == other.parallel
73            && self.leakproof == other.leakproof
74            && self.cost.map(f32::to_bits) == other.cost.map(f32::to_bits)
75            && self.rows.map(f32::to_bits) == other.rows.map(f32::to_bits)
76            && self.comment == other.comment
77            && self.owner == other.owner
78            && self.grants == other.grants
79    }
80}
81
82impl Eq for Function {}
83
84impl std::hash::Hash for Function {
85    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
86        self.qname.hash(state);
87        self.args.hash(state);
88        self.arg_types_normalized.hash(state);
89        self.return_type.hash(state);
90        self.language.hash(state);
91        self.body.hash(state);
92        self.body_dependencies.hash(state);
93        self.volatility.hash(state);
94        self.strict.hash(state);
95        self.security.hash(state);
96        self.parallel.hash(state);
97        self.leakproof.hash(state);
98        self.cost.map(f32::to_bits).hash(state);
99        self.rows.map(f32::to_bits).hash(state);
100        self.comment.hash(state);
101        self.owner.hash(state);
102        self.grants.hash(state);
103    }
104}
105
106impl Diff for Function {
107    // The structural differ at the change level lives in `crate::diff::routines`
108    // (T8) and produces granular FunctionChange variants. This `Diff` impl is
109    // the debug/equivalence-rule hook used by `Catalog::diff` for reporting:
110    // it emits one entry per actually-differing field so conformance-suite
111    // failure messages identify exactly what diverged.
112    #[allow(clippy::too_many_lines)]
113    fn diff(&self, other: &Self) -> Vec<Difference> {
114        if self == other {
115            return Vec::new();
116        }
117        let key = format!(
118            "{}({})",
119            self.qname,
120            self.args
121                .iter()
122                .map(|a| a.ty.render_sql())
123                .collect::<Vec<_>>()
124                .join(","),
125        );
126        let mut out = Vec::new();
127        let push = |out: &mut Vec<Difference>, field: &str, from: String, to: String| {
128            if from != to {
129                out.push(Difference::new(format!("{key}.{field}"), from, to));
130            }
131        };
132        push(
133            &mut out,
134            "body_hash",
135            hex::encode(self.body.canonical_hash()),
136            hex::encode(other.body.canonical_hash()),
137        );
138        push(
139            &mut out,
140            "body_canonical_text",
141            self.body.canonical_text().to_string(),
142            other.body.canonical_text().to_string(),
143        );
144        push(
145            &mut out,
146            "body_dependencies",
147            format!("{:?}", self.body_dependencies),
148            format!("{:?}", other.body_dependencies),
149        );
150        push(
151            &mut out,
152            "return_type",
153            format!("{:?}", self.return_type),
154            format!("{:?}", other.return_type),
155        );
156        push(
157            &mut out,
158            "language",
159            format!("{:?}", self.language),
160            format!("{:?}", other.language),
161        );
162        push(
163            &mut out,
164            "volatility",
165            format!("{:?}", self.volatility),
166            format!("{:?}", other.volatility),
167        );
168        push(
169            &mut out,
170            "strict",
171            self.strict.to_string(),
172            other.strict.to_string(),
173        );
174        push(
175            &mut out,
176            "security",
177            format!("{:?}", self.security),
178            format!("{:?}", other.security),
179        );
180        push(
181            &mut out,
182            "parallel",
183            format!("{:?}", self.parallel),
184            format!("{:?}", other.parallel),
185        );
186        push(
187            &mut out,
188            "leakproof",
189            self.leakproof.to_string(),
190            other.leakproof.to_string(),
191        );
192        push(
193            &mut out,
194            "cost",
195            format!("{:?}", self.cost),
196            format!("{:?}", other.cost),
197        );
198        push(
199            &mut out,
200            "rows",
201            format!("{:?}", self.rows),
202            format!("{:?}", other.rows),
203        );
204        push(
205            &mut out,
206            "comment",
207            format!("{:?}", self.comment),
208            format!("{:?}", other.comment),
209        );
210        push(
211            &mut out,
212            "args",
213            format!("{:?}", self.args),
214            format!("{:?}", other.args),
215        );
216        push(
217            &mut out,
218            "owner",
219            format!("{:?}", self.owner),
220            format!("{:?}", other.owner),
221        );
222        push(
223            &mut out,
224            "grants",
225            format!("{:?}", self.grants),
226            format!("{:?}", other.grants),
227        );
228        // If we still report no differences but the structs aren't equal,
229        // fall back to a generic "<unknown field>" entry so the assertion
230        // still surfaces something useful.
231        if out.is_empty() {
232            out.push(Difference::new(
233                key,
234                "<unknown field divergence>".to_string(),
235                "<unknown field divergence>".to_string(),
236            ));
237        }
238        out
239    }
240}
241
242/// A single argument declaration in a function or procedure.
243#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
244pub struct FunctionArg {
245    /// Optional argument name.
246    pub name: Option<Identifier>,
247    /// Argument mode (IN, OUT, INOUT, VARIADIC).
248    pub mode: ArgMode,
249    /// Data type of the argument.
250    pub ty: ColumnType,
251    /// Optional default expression for this argument.
252    pub default: Option<NormalizedExpr>,
253}
254
255/// Argument passing mode.
256#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum ArgMode {
259    /// Standard input argument.
260    In,
261    /// Output argument.
262    Out,
263    /// Input/output argument.
264    InOut,
265    /// Variadic input argument (must be last).
266    Variadic,
267}
268
269/// Return type of a function.
270#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
271#[serde(tag = "kind", rename_all = "snake_case")]
272pub enum ReturnType {
273    /// Returns a single scalar value.
274    Scalar {
275        /// The scalar type.
276        ty: ColumnType,
277    },
278    /// Returns a set of scalar values (SETOF).
279    SetOf {
280        /// The element type.
281        ty: ColumnType,
282    },
283    /// Returns a virtual table (RETURNS TABLE).
284    Table {
285        /// Column definitions.
286        columns: Vec<TableColumn>,
287    },
288    /// Trigger function (returns trigger).
289    Trigger,
290    /// Event trigger function.
291    EventTrigger,
292    /// Returns nothing (void).
293    Void,
294}
295
296/// A column definition in a RETURNS TABLE clause.
297#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
298pub struct TableColumn {
299    /// Column name.
300    pub name: Identifier,
301    /// Column data type.
302    pub ty: ColumnType,
303}
304
305/// Implementation language of a function or procedure.
306#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum FunctionLanguage {
309    /// Plain SQL body.
310    Sql,
311    /// PL/pgSQL procedural body.
312    PlPgSql,
313}
314
315/// Volatility category of a function.
316#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "snake_case")]
318pub enum Volatility {
319    /// Result never changes for the same input (allows aggressive caching).
320    Immutable,
321    /// Result is stable within a single transaction.
322    Stable,
323    /// Result may change at any time (default).
324    Volatile,
325}
326
327/// Security execution context.
328#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum SecurityMode {
331    /// Executes with the privileges of the calling role (default).
332    Invoker,
333    /// Executes with the privileges of the defining role.
334    Definer,
335}
336
337/// Parallel safety classification for a function.
338#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum ParallelSafety {
341    /// Cannot be executed in parallel mode.
342    Unsafe,
343    /// Can be executed in parallel but must run in the parallel leader.
344    Restricted,
345    /// Can be executed safely in parallel workers.
346    Safe,
347}
348
349/// Normalized argument types — function identity disambiguator.
350///
351/// Built over the IN/INOUT/VARIADIC args only (matches PG's `proargtypes`).
352/// The `canonical_hash` is BLAKE3 of the comma-joined canonical type strings.
353#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
354pub struct NormalizedArgTypes {
355    /// IN/INOUT/VARIADIC types in declaration order.
356    pub types: Vec<ColumnType>,
357    /// BLAKE3 hash of the comma-joined canonical type strings.
358    pub canonical_hash: [u8; 32],
359}
360
361impl PartialOrd for NormalizedArgTypes {
362    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
363        Some(self.cmp(other))
364    }
365}
366
367impl Ord for NormalizedArgTypes {
368    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
369        // Order by the canonical hash — unique and stable across the arg-type space.
370        self.canonical_hash.cmp(&other.canonical_hash)
371    }
372}
373
374impl NormalizedArgTypes {
375    /// Construct from a list of args, filtering to IN/INOUT/VARIADIC and
376    /// computing the BLAKE3 hash of the canonical type-string list.
377    pub fn from_args(args: &[FunctionArg]) -> Self {
378        let types: Vec<ColumnType> = args
379            .iter()
380            .filter(|a| matches!(a.mode, ArgMode::In | ArgMode::InOut | ArgMode::Variadic))
381            .map(|a| a.ty.clone())
382            .collect();
383        let canonical_string = types
384            .iter()
385            .map(ColumnType::render_sql)
386            .collect::<Vec<_>>()
387            .join(",");
388        let canonical_hash = blake3::hash(canonical_string.as_bytes()).into();
389        Self {
390            types,
391            canonical_hash,
392        }
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::ir::catalog::Catalog;
400    use crate::ir::schema::Schema;
401
402    fn ident(s: &str) -> Identifier {
403        Identifier::from_unquoted(s).unwrap()
404    }
405    fn qname(schema: &str, name: &str) -> QualifiedName {
406        QualifiedName::new(ident(schema), ident(name))
407    }
408
409    fn sample_function() -> Function {
410        let args = vec![FunctionArg {
411            name: Some(ident("x")),
412            mode: ArgMode::In,
413            ty: ColumnType::Integer,
414            default: None,
415        }];
416        let arg_types_normalized = NormalizedArgTypes::from_args(&args);
417        Function {
418            qname: qname("app", "double"),
419            args,
420            arg_types_normalized,
421            return_type: ReturnType::Scalar {
422                ty: ColumnType::Integer,
423            },
424            language: FunctionLanguage::Sql,
425            body: NormalizedBody::from_sql("SELECT $1 * 2").unwrap(),
426            body_dependencies: vec![],
427            volatility: Volatility::Immutable,
428            strict: true,
429            security: SecurityMode::Invoker,
430            parallel: ParallelSafety::Safe,
431            leakproof: false,
432            cost: Some(1.0),
433            rows: None,
434            comment: None,
435            owner: None,
436            grants: Vec::new(),
437        }
438    }
439
440    #[test]
441    fn function_serde_round_trip() {
442        let f = sample_function();
443        let json = serde_json::to_string(&f).unwrap();
444        let back: Function = serde_json::from_str(&json).unwrap();
445        assert_eq!(f, back);
446    }
447
448    #[test]
449    fn function_overloads_have_distinct_arg_hashes() {
450        let int_args = vec![FunctionArg {
451            name: None,
452            mode: ArgMode::In,
453            ty: ColumnType::Integer,
454            default: None,
455        }];
456        let text_args = vec![FunctionArg {
457            name: None,
458            mode: ArgMode::In,
459            ty: ColumnType::Text,
460            default: None,
461        }];
462        let int_norm = NormalizedArgTypes::from_args(&int_args);
463        let text_norm = NormalizedArgTypes::from_args(&text_args);
464        assert_ne!(int_norm.canonical_hash, text_norm.canonical_hash);
465    }
466
467    #[test]
468    fn out_args_excluded_from_normalized_types() {
469        let args = vec![
470            FunctionArg {
471                name: None,
472                mode: ArgMode::In,
473                ty: ColumnType::Integer,
474                default: None,
475            },
476            FunctionArg {
477                name: None,
478                mode: ArgMode::Out,
479                ty: ColumnType::Text,
480                default: None,
481            },
482        ];
483        let norm = NormalizedArgTypes::from_args(&args);
484        assert_eq!(
485            norm.types.len(),
486            1,
487            "OUT args must not appear in identity hash"
488        );
489        assert!(matches!(norm.types[0], ColumnType::Integer));
490    }
491
492    #[test]
493    fn catalog_holds_functions_and_canonicalizes() {
494        let mut c = Catalog::empty();
495        c.schemas.push(Schema::new(ident("app")));
496        c.functions.push(sample_function());
497        c = c.canonicalize().expect("must canonicalize");
498        assert_eq!(c.functions.len(), 1);
499        assert_eq!(c.functions[0].qname.to_string(), "app.double");
500    }
501
502    #[test]
503    fn catalog_rejects_duplicate_function_identity() {
504        use crate::ir::IrError;
505
506        let mut c = Catalog::empty();
507        c.schemas.push(Schema::new(ident("app")));
508        c.functions.push(sample_function());
509        c.functions.push(sample_function());
510        let r = c.canonicalize();
511        assert!(
512            matches!(r, Err(IrError::InvalidIdentifier(_))),
513            "expected InvalidIdentifier, got {r:?}",
514        );
515        let msg = r.unwrap_err().to_string();
516        assert!(
517            msg.contains("app.double"),
518            "should name the function: {msg}"
519        );
520    }
521
522    #[test]
523    fn catalog_allows_distinct_function_overloads() {
524        let mut c = Catalog::empty();
525        c.schemas.push(Schema::new(ident("app")));
526        let f1 = sample_function();
527        let mut f2 = sample_function();
528        f2.args[0].ty = ColumnType::Text;
529        f2.arg_types_normalized = NormalizedArgTypes::from_args(&f2.args);
530        f2.return_type = ReturnType::Scalar {
531            ty: ColumnType::Text,
532        };
533        c.functions.push(f1);
534        c.functions.push(f2);
535        let c = c.canonicalize().expect("overloads should be allowed");
536        assert_eq!(c.functions.len(), 2);
537    }
538
539    #[test]
540    fn owner_change_diffs() {
541        use crate::ir::eq::Diff;
542        let mut b = sample_function();
543        b.owner = Some(ident("new_owner"));
544        assert!(
545            sample_function()
546                .diff(&b)
547                .iter()
548                .any(|x| x.path.ends_with("owner"))
549        );
550    }
551
552    #[test]
553    fn grants_change_diffs() {
554        use crate::ir::eq::Diff;
555        let mut b = sample_function();
556        b.grants.push(crate::ir::grant::Grant {
557            grantee: crate::ir::grant::GrantTarget::Public,
558            privilege: crate::ir::grant::Privilege::Execute,
559            with_grant_option: false,
560            columns: None,
561        });
562        assert!(
563            sample_function()
564                .diff(&b)
565                .iter()
566                .any(|x| x.path.ends_with("grants"))
567        );
568    }
569}