1use 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#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Function {
16 pub qname: QualifiedName,
18 pub args: Vec<FunctionArg>,
20 pub arg_types_normalized: NormalizedArgTypes,
22 pub return_type: ReturnType,
24 pub language: FunctionLanguage,
26 pub body: NormalizedBody,
28 #[serde(default)]
33 pub body_dependencies: Vec<DepEdge>,
34 pub volatility: Volatility,
36 pub strict: bool,
38 pub security: SecurityMode,
40 pub parallel: ParallelSafety,
42 pub leakproof: bool,
44 pub cost: Option<f32>,
46 pub rows: Option<f32>,
48 pub comment: Option<String>,
50 pub owner: Option<crate::identifier::Identifier>,
53 pub grants: Vec<crate::ir::grant::Grant>,
55}
56
57impl 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 #[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 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#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
244pub struct FunctionArg {
245 pub name: Option<Identifier>,
247 pub mode: ArgMode,
249 pub ty: ColumnType,
251 pub default: Option<NormalizedExpr>,
253}
254
255#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum ArgMode {
259 In,
261 Out,
263 InOut,
265 Variadic,
267}
268
269#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
271#[serde(tag = "kind", rename_all = "snake_case")]
272pub enum ReturnType {
273 Scalar {
275 ty: ColumnType,
277 },
278 SetOf {
280 ty: ColumnType,
282 },
283 Table {
285 columns: Vec<TableColumn>,
287 },
288 Trigger,
290 EventTrigger,
292 Void,
294}
295
296#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
298pub struct TableColumn {
299 pub name: Identifier,
301 pub ty: ColumnType,
303}
304
305#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum FunctionLanguage {
309 Sql,
311 PlPgSql,
313}
314
315#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "snake_case")]
318pub enum Volatility {
319 Immutable,
321 Stable,
323 Volatile,
325}
326
327#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum SecurityMode {
331 Invoker,
333 Definer,
335}
336
337#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum ParallelSafety {
341 Unsafe,
343 Restricted,
345 Safe,
347}
348
349#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
354pub struct NormalizedArgTypes {
355 pub types: Vec<ColumnType>,
357 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 self.canonical_hash.cmp(&other.canonical_hash)
371 }
372}
373
374impl NormalizedArgTypes {
375 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}