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::Equiv;
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 Equiv for Function {
107 #[allow(clippy::too_many_lines)]
113 fn differences(&self, other: &Self) -> Vec<Difference> {
114 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 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#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
268pub struct FunctionArg {
269 pub name: Option<Identifier>,
271 pub mode: ArgMode,
273 pub ty: ColumnType,
275 pub default: Option<NormalizedExpr>,
277}
278
279#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ArgMode {
283 In,
285 Out,
287 InOut,
289 Variadic,
291}
292
293#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
295#[serde(tag = "kind", rename_all = "snake_case")]
296pub enum ReturnType {
297 Scalar {
299 ty: ColumnType,
301 },
302 SetOf {
304 ty: ColumnType,
306 },
307 Table {
309 columns: Vec<TableColumn>,
311 },
312 Trigger,
314 EventTrigger,
316 Void,
318}
319
320#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
322pub struct TableColumn {
323 pub name: Identifier,
325 pub ty: ColumnType,
327}
328
329#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
331#[serde(rename_all = "snake_case")]
332pub enum FunctionLanguage {
333 Sql,
335 PlPgSql,
337}
338
339#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
341#[serde(rename_all = "snake_case")]
342pub enum Volatility {
343 Immutable,
345 Stable,
347 Volatile,
349}
350
351#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum SecurityMode {
355 Invoker,
357 Definer,
359}
360
361#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364pub enum ParallelSafety {
365 Unsafe,
367 Restricted,
369 Safe,
371}
372
373#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
378pub struct NormalizedArgTypes {
379 pub types: Vec<ColumnType>,
381 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 self.canonical_hash.cmp(&other.canonical_hash)
395 }
396}
397
398impl NormalizedArgTypes {
399 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}