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::Equiv;
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 Equiv for Function {
107    // The structural differ at the change level lives in `crate::diff::routines`
108    // (T8) and produces granular FunctionChange variants. This `Equiv` impl is
109    // the debug/equivalence-rule hook used by `Catalog::differences` 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 differences(&self, other: &Self) -> Vec<Difference> {
114        // Field-completeness guard: the compiler errors if a field is added to
115        // `Function` without being handled below. `qname` and
116        // `arg_types_normalized` are covered by the `self == other` early-out
117        // plus the `<unknown field divergence>` fallback rather than an explicit
118        // `push`. Bindings are unused (values read via `self`/`other`).
119        let Self {
120            qname: _,
121            args: _,
122            arg_types_normalized: _,
123            return_type: _,
124            language: _,
125            body: _,
126            body_dependencies: _,
127            volatility: _,
128            strict: _,
129            security: _,
130            parallel: _,
131            leakproof: _,
132            cost: _,
133            rows: _,
134            comment: _,
135            owner: _,
136            grants: _,
137        } = self;
138        if self == other {
139            return Vec::new();
140        }
141        let key = format!(
142            "{}({})",
143            self.qname,
144            self.args
145                .iter()
146                .map(|a| a.ty.render_sql())
147                .collect::<Vec<_>>()
148                .join(","),
149        );
150        let mut out = Vec::new();
151        let push = |out: &mut Vec<Difference>, field: &str, from: String, to: String| {
152            if from != to {
153                out.push(Difference::new(format!("{key}.{field}"), from, to));
154            }
155        };
156        push(
157            &mut out,
158            "body_hash",
159            hex::encode(self.body.canonical_hash()),
160            hex::encode(other.body.canonical_hash()),
161        );
162        push(
163            &mut out,
164            "body_canonical_text",
165            self.body.canonical_text().to_string(),
166            other.body.canonical_text().to_string(),
167        );
168        push(
169            &mut out,
170            "body_dependencies",
171            format!("{:?}", self.body_dependencies),
172            format!("{:?}", other.body_dependencies),
173        );
174        push(
175            &mut out,
176            "return_type",
177            format!("{:?}", self.return_type),
178            format!("{:?}", other.return_type),
179        );
180        push(
181            &mut out,
182            "language",
183            format!("{:?}", self.language),
184            format!("{:?}", other.language),
185        );
186        push(
187            &mut out,
188            "volatility",
189            format!("{:?}", self.volatility),
190            format!("{:?}", other.volatility),
191        );
192        push(
193            &mut out,
194            "strict",
195            self.strict.to_string(),
196            other.strict.to_string(),
197        );
198        push(
199            &mut out,
200            "security",
201            format!("{:?}", self.security),
202            format!("{:?}", other.security),
203        );
204        push(
205            &mut out,
206            "parallel",
207            format!("{:?}", self.parallel),
208            format!("{:?}", other.parallel),
209        );
210        push(
211            &mut out,
212            "leakproof",
213            self.leakproof.to_string(),
214            other.leakproof.to_string(),
215        );
216        push(
217            &mut out,
218            "cost",
219            format!("{:?}", self.cost),
220            format!("{:?}", other.cost),
221        );
222        push(
223            &mut out,
224            "rows",
225            format!("{:?}", self.rows),
226            format!("{:?}", other.rows),
227        );
228        push(
229            &mut out,
230            "comment",
231            format!("{:?}", self.comment),
232            format!("{:?}", other.comment),
233        );
234        push(
235            &mut out,
236            "args",
237            format!("{:?}", self.args),
238            format!("{:?}", other.args),
239        );
240        push(
241            &mut out,
242            "owner",
243            format!("{:?}", self.owner),
244            format!("{:?}", other.owner),
245        );
246        push(
247            &mut out,
248            "grants",
249            format!("{:?}", self.grants),
250            format!("{:?}", other.grants),
251        );
252        // If we still report no differences but the structs aren't equal,
253        // fall back to a generic "<unknown field>" entry so the assertion
254        // still surfaces something useful.
255        if out.is_empty() {
256            out.push(Difference::new(
257                key,
258                "<unknown field divergence>".to_string(),
259                "<unknown field divergence>".to_string(),
260            ));
261        }
262        out
263    }
264}
265
266/// A single argument declaration in a function or procedure.
267#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
268pub struct FunctionArg {
269    /// Optional argument name.
270    pub name: Option<Identifier>,
271    /// Argument mode (IN, OUT, INOUT, VARIADIC).
272    pub mode: ArgMode,
273    /// Data type of the argument.
274    pub ty: ColumnType,
275    /// Optional default expression for this argument.
276    pub default: Option<NormalizedExpr>,
277}
278
279/// Argument passing mode.
280#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ArgMode {
283    /// Standard input argument.
284    In,
285    /// Output argument.
286    Out,
287    /// Input/output argument.
288    InOut,
289    /// Variadic input argument (must be last).
290    Variadic,
291}
292
293/// Return type of a function.
294#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
295#[serde(tag = "kind", rename_all = "snake_case")]
296pub enum ReturnType {
297    /// Returns a single scalar value.
298    Scalar {
299        /// The scalar type.
300        ty: ColumnType,
301    },
302    /// Returns a set of scalar values (SETOF).
303    SetOf {
304        /// The element type.
305        ty: ColumnType,
306    },
307    /// Returns a virtual table (RETURNS TABLE).
308    Table {
309        /// Column definitions.
310        columns: Vec<TableColumn>,
311    },
312    /// Trigger function (returns trigger).
313    Trigger,
314    /// Event trigger function.
315    EventTrigger,
316    /// Returns nothing (void).
317    Void,
318}
319
320/// A column definition in a RETURNS TABLE clause.
321#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
322pub struct TableColumn {
323    /// Column name.
324    pub name: Identifier,
325    /// Column data type.
326    pub ty: ColumnType,
327}
328
329/// Implementation language of a function or procedure.
330#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
331#[serde(rename_all = "snake_case")]
332pub enum FunctionLanguage {
333    /// Plain SQL body.
334    Sql,
335    /// PL/pgSQL procedural body.
336    PlPgSql,
337}
338
339/// Volatility category of a function.
340#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
341#[serde(rename_all = "snake_case")]
342pub enum Volatility {
343    /// Result never changes for the same input (allows aggressive caching).
344    Immutable,
345    /// Result is stable within a single transaction.
346    Stable,
347    /// Result may change at any time (default).
348    Volatile,
349}
350
351/// Security execution context.
352#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum SecurityMode {
355    /// Executes with the privileges of the calling role (default).
356    Invoker,
357    /// Executes with the privileges of the defining role.
358    Definer,
359}
360
361/// Parallel safety classification for a function.
362#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364pub enum ParallelSafety {
365    /// Cannot be executed in parallel mode.
366    Unsafe,
367    /// Can be executed in parallel but must run in the parallel leader.
368    Restricted,
369    /// Can be executed safely in parallel workers.
370    Safe,
371}
372
373/// Normalized argument types — function identity disambiguator.
374///
375/// Built over the IN/INOUT/VARIADIC args only (matches PG's `proargtypes`).
376/// The `canonical_hash` is BLAKE3 of the comma-joined canonical type strings.
377#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
378pub struct NormalizedArgTypes {
379    /// IN/INOUT/VARIADIC types in declaration order.
380    pub types: Vec<ColumnType>,
381    /// BLAKE3 hash of the comma-joined canonical type strings.
382    pub canonical_hash: [u8; 32],
383}
384
385impl PartialOrd for NormalizedArgTypes {
386    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
387        Some(self.cmp(other))
388    }
389}
390
391impl Ord for NormalizedArgTypes {
392    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
393        // Order by the canonical hash — unique and stable across the arg-type space.
394        self.canonical_hash.cmp(&other.canonical_hash)
395    }
396}
397
398impl NormalizedArgTypes {
399    /// Construct from a list of args, filtering to IN/INOUT/VARIADIC and
400    /// computing the BLAKE3 hash of the canonical type-string list.
401    pub fn from_args(args: &[FunctionArg]) -> Self {
402        let types: Vec<ColumnType> = args
403            .iter()
404            .filter(|a| matches!(a.mode, ArgMode::In | ArgMode::InOut | ArgMode::Variadic))
405            .map(|a| a.ty.clone())
406            .collect();
407        let canonical_string = types
408            .iter()
409            .map(ColumnType::render_sql)
410            .collect::<Vec<_>>()
411            .join(",");
412        let canonical_hash = blake3::hash(canonical_string.as_bytes()).into();
413        Self {
414            types,
415            canonical_hash,
416        }
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::ir::catalog::Catalog;
424    use crate::ir::schema::Schema;
425
426    fn ident(s: &str) -> Identifier {
427        Identifier::from_unquoted(s).unwrap()
428    }
429    fn qname(schema: &str, name: &str) -> QualifiedName {
430        QualifiedName::new(ident(schema), ident(name))
431    }
432
433    fn sample_function() -> Function {
434        let args = vec![FunctionArg {
435            name: Some(ident("x")),
436            mode: ArgMode::In,
437            ty: ColumnType::Integer,
438            default: None,
439        }];
440        let arg_types_normalized = NormalizedArgTypes::from_args(&args);
441        Function {
442            qname: qname("app", "double"),
443            args,
444            arg_types_normalized,
445            return_type: ReturnType::Scalar {
446                ty: ColumnType::Integer,
447            },
448            language: FunctionLanguage::Sql,
449            body: NormalizedBody::from_sql("SELECT $1 * 2").unwrap(),
450            body_dependencies: vec![],
451            volatility: Volatility::Immutable,
452            strict: true,
453            security: SecurityMode::Invoker,
454            parallel: ParallelSafety::Safe,
455            leakproof: false,
456            cost: Some(1.0),
457            rows: None,
458            comment: None,
459            owner: None,
460            grants: Vec::new(),
461        }
462    }
463
464    #[test]
465    fn function_serde_round_trip() {
466        let f = sample_function();
467        let json = serde_json::to_string(&f).unwrap();
468        let back: Function = serde_json::from_str(&json).unwrap();
469        assert_eq!(f, back);
470    }
471
472    #[test]
473    fn function_overloads_have_distinct_arg_hashes() {
474        let int_args = vec![FunctionArg {
475            name: None,
476            mode: ArgMode::In,
477            ty: ColumnType::Integer,
478            default: None,
479        }];
480        let text_args = vec![FunctionArg {
481            name: None,
482            mode: ArgMode::In,
483            ty: ColumnType::Text,
484            default: None,
485        }];
486        let int_norm = NormalizedArgTypes::from_args(&int_args);
487        let text_norm = NormalizedArgTypes::from_args(&text_args);
488        assert_ne!(int_norm.canonical_hash, text_norm.canonical_hash);
489    }
490
491    #[test]
492    fn out_args_excluded_from_normalized_types() {
493        let args = vec![
494            FunctionArg {
495                name: None,
496                mode: ArgMode::In,
497                ty: ColumnType::Integer,
498                default: None,
499            },
500            FunctionArg {
501                name: None,
502                mode: ArgMode::Out,
503                ty: ColumnType::Text,
504                default: None,
505            },
506        ];
507        let norm = NormalizedArgTypes::from_args(&args);
508        assert_eq!(
509            norm.types.len(),
510            1,
511            "OUT args must not appear in identity hash"
512        );
513        assert!(matches!(norm.types[0], ColumnType::Integer));
514    }
515
516    #[test]
517    fn catalog_holds_functions_and_canonicalizes() {
518        let mut c = Catalog::empty();
519        c.schemas.push(Schema::new(ident("app")));
520        c.functions.push(sample_function());
521        c = c.canonicalize().expect("must canonicalize");
522        assert_eq!(c.functions.len(), 1);
523        assert_eq!(c.functions[0].qname.to_string(), "app.double");
524    }
525
526    #[test]
527    fn catalog_rejects_duplicate_function_identity() {
528        use crate::ir::IrError;
529
530        let mut c = Catalog::empty();
531        c.schemas.push(Schema::new(ident("app")));
532        c.functions.push(sample_function());
533        c.functions.push(sample_function());
534        let r = c.canonicalize();
535        assert!(
536            matches!(
537                r,
538                Err(IrError::DuplicateObject {
539                    kind: "function",
540                    ..
541                })
542            ),
543            "expected DuplicateObject, got {r:?}",
544        );
545        let msg = r.unwrap_err().to_string();
546        assert!(
547            msg.contains("app.double"),
548            "should name the function: {msg}"
549        );
550    }
551
552    #[test]
553    fn catalog_allows_distinct_function_overloads() {
554        let mut c = Catalog::empty();
555        c.schemas.push(Schema::new(ident("app")));
556        let f1 = sample_function();
557        let mut f2 = sample_function();
558        f2.args[0].ty = ColumnType::Text;
559        f2.arg_types_normalized = NormalizedArgTypes::from_args(&f2.args);
560        f2.return_type = ReturnType::Scalar {
561            ty: ColumnType::Text,
562        };
563        c.functions.push(f1);
564        c.functions.push(f2);
565        let c = c.canonicalize().expect("overloads should be allowed");
566        assert_eq!(c.functions.len(), 2);
567    }
568
569    #[test]
570    fn owner_change_diffs() {
571        use crate::ir::eq::Equiv;
572        let mut b = sample_function();
573        b.owner = Some(ident("new_owner"));
574        assert!(
575            sample_function()
576                .differences(&b)
577                .iter()
578                .any(|x| x.path.ends_with("owner"))
579        );
580    }
581
582    #[test]
583    fn grants_change_diffs() {
584        use crate::ir::eq::Equiv;
585        let mut b = sample_function();
586        b.grants.push(crate::ir::grant::Grant {
587            grantee: crate::ir::grant::GrantTarget::Public,
588            privilege: crate::ir::grant::Privilege::Execute,
589            with_grant_option: false,
590            columns: None,
591        });
592        assert!(
593            sample_function()
594                .differences(&b)
595                .iter()
596                .any(|x| x.path.ends_with("grants"))
597        );
598    }
599}