1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::identifier::{Identifier, QualifiedName};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum CheckOption {
16 Local,
18 Cascaded,
20}
21use crate::ir::column_type::ColumnType;
22use crate::ir::difference::Difference;
23use crate::ir::eq::{Equiv, field_difference};
24use crate::parse::normalize_body::NormalizedBody;
25use crate::plan::edges::DepEdge;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct ViewColumn {
37 pub name: Identifier,
39 pub column_type: Option<ColumnType>,
41 pub comment: Option<String>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct View {
53 pub qname: QualifiedName,
55 pub columns: Vec<ViewColumn>,
57 pub body_canonical: NormalizedBody,
59 pub body_dependencies: Vec<DepEdge>,
61 pub security_barrier: Option<bool>,
63 pub security_invoker: Option<bool>,
65 pub check_option: Option<CheckOption>,
69 pub comment: Option<String>,
71 #[serde(skip, default)]
76 pub raw_body: String,
77 pub owner: Option<Identifier>,
80 pub grants: Vec<crate::ir::grant::Grant>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
90pub struct MaterializedView {
91 pub qname: QualifiedName,
93 pub columns: Vec<ViewColumn>,
95 pub body_canonical: NormalizedBody,
97 pub body_dependencies: Vec<DepEdge>,
99 pub comment: Option<String>,
101 #[serde(skip, default)]
106 pub raw_body: String,
107 pub owner: Option<Identifier>,
110 pub grants: Vec<crate::ir::grant::Grant>,
112 pub storage: crate::ir::reloptions::MaterializedViewStorageOptions,
114}
115
116fn render_column_type(ty: Option<&ColumnType>) -> String {
120 ty.map_or_else(|| "<unresolved>".to_string(), ColumnType::render_sql)
121}
122
123impl Equiv for View {
124 #[allow(clippy::too_many_lines)] fn differences(&self, other: &Self) -> Vec<Difference> {
126 let Self {
133 qname: _,
134 columns: _,
135 body_canonical: _,
136 body_dependencies: _,
137 security_barrier: _,
138 security_invoker: _,
139 check_option: _,
140 comment: _,
141 raw_body: _, owner: _,
143 grants: _,
144 } = self;
145 let mut out = Vec::new();
146 out.extend(field_difference("qname", &self.qname, &other.qname));
147 out.extend(field_difference(
148 "body_canonical",
149 &self.body_canonical.canonical_text(),
150 &other.body_canonical.canonical_text(),
151 ));
152 out.extend(field_difference(
153 "security_barrier",
154 &format!("{:?}", self.security_barrier),
155 &format!("{:?}", other.security_barrier),
156 ));
157 out.extend(field_difference(
158 "security_invoker",
159 &format!("{:?}", self.security_invoker),
160 &format!("{:?}", other.security_invoker),
161 ));
162 out.extend(field_difference(
163 "check_option",
164 &format!("{:?}", self.check_option),
165 &format!("{:?}", other.check_option),
166 ));
167 out.extend(field_difference(
168 "comment",
169 &format!("{:?}", self.comment),
170 &format!("{:?}", other.comment),
171 ));
172 out.extend(field_difference(
173 "owner",
174 &format!("{:?}", self.owner),
175 &format!("{:?}", other.owner),
176 ));
177 out.extend(field_difference(
178 "grants",
179 &format!("{:?}", self.grants),
180 &format!("{:?}", other.grants),
181 ));
182
183 let lhs: BTreeMap<_, _> = self.columns.iter().map(|c| (c.name.as_str(), c)).collect();
185 let rhs: BTreeMap<_, _> = other.columns.iter().map(|c| (c.name.as_str(), c)).collect();
186 for (name, l) in &lhs {
187 match rhs.get(name) {
188 None => out.push(Difference::new(
189 format!("columns.{name}"),
190 "present",
191 "removed",
192 )),
193 Some(r) => {
194 if l.column_type != r.column_type {
195 out.push(Difference::new(
196 format!("columns.{name}.column_type"),
197 render_column_type(l.column_type.as_ref()),
198 render_column_type(r.column_type.as_ref()),
199 ));
200 }
201 if l.comment != r.comment {
202 out.push(Difference::new(
203 format!("columns.{name}.comment"),
204 format!("{:?}", l.comment),
205 format!("{:?}", r.comment),
206 ));
207 }
208 }
209 }
210 }
211 for name in rhs.keys() {
212 if !lhs.contains_key(name) {
213 out.push(Difference::new(
214 format!("columns.{name}"),
215 "missing",
216 "added",
217 ));
218 }
219 }
220 let lhs_order: Vec<&str> = self.columns.iter().map(|c| c.name.as_str()).collect();
221 let rhs_order: Vec<&str> = other.columns.iter().map(|c| c.name.as_str()).collect();
222 if lhs_order != rhs_order {
223 out.push(Difference::new(
224 "columns.<order>",
225 lhs_order.join(","),
226 rhs_order.join(","),
227 ));
228 }
229
230 out.extend(field_difference(
232 "body_dependencies",
233 &format!("{:?}", self.body_dependencies),
234 &format!("{:?}", other.body_dependencies),
235 ));
236
237 out
238 }
239}
240
241impl Equiv for MaterializedView {
242 fn differences(&self, other: &Self) -> Vec<Difference> {
243 let Self {
249 qname: _,
250 columns: _,
251 body_canonical: _,
252 body_dependencies: _,
253 comment: _,
254 raw_body: _,
255 owner: _,
256 grants: _,
257 storage: _,
258 } = self;
259 let mut out = Vec::new();
260 out.extend(field_difference("qname", &self.qname, &other.qname));
261 out.extend(field_difference(
262 "body_canonical",
263 &self.body_canonical.canonical_text(),
264 &other.body_canonical.canonical_text(),
265 ));
266 out.extend(field_difference(
267 "comment",
268 &format!("{:?}", self.comment),
269 &format!("{:?}", other.comment),
270 ));
271 out.extend(field_difference(
272 "owner",
273 &format!("{:?}", self.owner),
274 &format!("{:?}", other.owner),
275 ));
276 out.extend(field_difference(
277 "grants",
278 &format!("{:?}", self.grants),
279 &format!("{:?}", other.grants),
280 ));
281 out.extend(field_difference(
282 "storage",
283 &format!("{:?}", self.storage),
284 &format!("{:?}", other.storage),
285 ));
286
287 let lhs: BTreeMap<_, _> = self.columns.iter().map(|c| (c.name.as_str(), c)).collect();
289 let rhs: BTreeMap<_, _> = other.columns.iter().map(|c| (c.name.as_str(), c)).collect();
290 for (name, l) in &lhs {
291 match rhs.get(name) {
292 None => out.push(Difference::new(
293 format!("columns.{name}"),
294 "present",
295 "removed",
296 )),
297 Some(r) => {
298 if l.column_type != r.column_type {
299 out.push(Difference::new(
300 format!("columns.{name}.column_type"),
301 render_column_type(l.column_type.as_ref()),
302 render_column_type(r.column_type.as_ref()),
303 ));
304 }
305 if l.comment != r.comment {
306 out.push(Difference::new(
307 format!("columns.{name}.comment"),
308 format!("{:?}", l.comment),
309 format!("{:?}", r.comment),
310 ));
311 }
312 }
313 }
314 }
315 for name in rhs.keys() {
316 if !lhs.contains_key(name) {
317 out.push(Difference::new(
318 format!("columns.{name}"),
319 "missing",
320 "added",
321 ));
322 }
323 }
324 let lhs_order: Vec<&str> = self.columns.iter().map(|c| c.name.as_str()).collect();
325 let rhs_order: Vec<&str> = other.columns.iter().map(|c| c.name.as_str()).collect();
326 if lhs_order != rhs_order {
327 out.push(Difference::new(
328 "columns.<order>",
329 lhs_order.join(","),
330 rhs_order.join(","),
331 ));
332 }
333
334 out.extend(field_difference(
336 "body_dependencies",
337 &format!("{:?}", self.body_dependencies),
338 &format!("{:?}", other.body_dependencies),
339 ));
340
341 out
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use crate::ir::IrError;
349 use crate::ir::catalog::Catalog;
350 use crate::ir::column_type::ColumnType;
351 use crate::plan::edges::{DepSource, NodeId};
352
353 fn id(s: &str) -> Identifier {
354 Identifier::from_unquoted(s).unwrap()
355 }
356
357 fn qn(schema: &str, name: &str) -> QualifiedName {
358 QualifiedName::new(id(schema), id(name))
359 }
360
361 fn body(sql: &str) -> NormalizedBody {
362 NormalizedBody::from_sql(sql).unwrap()
363 }
364
365 fn simple_view(schema: &str, name: &str) -> View {
366 View {
367 qname: qn(schema, name),
368 columns: vec![ViewColumn {
369 name: id("id"),
370 column_type: Some(ColumnType::BigInt),
371 comment: None,
372 }],
373 body_canonical: body("SELECT 1"),
374 body_dependencies: vec![],
375 security_barrier: None,
376 security_invoker: None,
377 check_option: None,
378 comment: None,
379 raw_body: String::new(),
380 owner: None,
381 grants: vec![],
382 }
383 }
384
385 fn simple_mv(schema: &str, name: &str) -> MaterializedView {
386 MaterializedView {
387 qname: qn(schema, name),
388 columns: vec![],
389 body_canonical: body("SELECT 1"),
390 body_dependencies: vec![],
391 comment: None,
392 raw_body: String::new(),
393 owner: None,
394 grants: vec![],
395 storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
396 }
397 }
398
399 #[test]
400 fn views_with_equal_fields_compare_equal() {
401 let v1 = simple_view("app", "active_users");
402 let v2 = View {
403 qname: qn("app", "active_users"),
404 columns: vec![ViewColumn {
405 name: id("id"),
406 column_type: Some(ColumnType::BigInt),
407 comment: None,
408 }],
409 body_canonical: body("SELECT 1"),
410 body_dependencies: vec![],
411 security_barrier: None,
412 security_invoker: None,
413 check_option: None,
414 comment: None,
415 raw_body: String::new(),
416 owner: None,
417 grants: vec![],
418 };
419 assert_eq!(v1, v2);
420 }
421
422 #[test]
423 fn materialized_view_round_trips_through_serde() {
424 let mv = MaterializedView {
425 qname: qn("app", "summary"),
426 columns: vec![ViewColumn {
427 name: id("total"),
428 column_type: Some(ColumnType::BigInt),
429 comment: Some("total count".to_string()),
430 }],
431 body_canonical: body("SELECT count(*) FROM users"),
432 body_dependencies: vec![DepEdge {
433 from: NodeId::Table(qn("app", "summary")),
434 to: NodeId::Table(qn("app", "users")),
435 source: DepSource::AstExtracted,
436 }],
437 comment: Some("materialized summary".to_string()),
438 raw_body: String::new(),
439 owner: None,
440 grants: vec![],
441 storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
442 };
443 let json = serde_json::to_string(&mv).expect("serialization must succeed");
444 let roundtripped: MaterializedView =
445 serde_json::from_str(&json).expect("deserialization must succeed");
446 assert_eq!(mv, roundtripped);
447 }
448
449 #[test]
450 fn catalog_with_views_canonicalizes() {
451 let mut c = Catalog::empty();
452 c.views.push(simple_view("app", "zzz_view"));
453 c.views.push(simple_view("app", "aaa_view"));
454 c.materialized_views.push(simple_mv("app", "zzz_mv"));
455 c.materialized_views.push(simple_mv("app", "aaa_mv"));
456
457 let result = c.canonicalize();
458 assert!(result.is_ok(), "canonicalize should succeed: {result:?}");
459 let canonical = result.unwrap();
460
461 assert_eq!(canonical.views[0].qname, qn("app", "aaa_view"));
462 assert_eq!(canonical.views[1].qname, qn("app", "zzz_view"));
463 assert_eq!(canonical.materialized_views[0].qname, qn("app", "aaa_mv"));
464 assert_eq!(canonical.materialized_views[1].qname, qn("app", "zzz_mv"));
465 }
466
467 #[test]
468 fn unresolved_view_column_rejected_by_canon() {
469 let mut c = Catalog::empty();
470 let mut v = simple_view("app", "v");
471 v.columns[0].column_type = None;
473 c.views.push(v);
474
475 let result = c.canonicalize();
476 assert!(
477 matches!(result, Err(IrError::UnresolvedViewColumn { .. })),
478 "expected UnresolvedViewColumn error, got: {result:?}",
479 );
480 }
481
482 #[test]
483 fn catalog_rejects_duplicate_view_qname() {
484 let mut c = Catalog::empty();
485 c.views.push(simple_view("app", "my_view"));
486 c.views.push(simple_view("app", "my_view"));
487
488 let result = c.canonicalize();
489 assert!(
490 matches!(result, Err(IrError::DuplicateObject { kind: "view", .. })),
491 "expected duplicate-view error, got: {result:?}",
492 );
493 }
494
495 #[test]
496 fn catalog_rejects_duplicate_materialized_view_qname() {
497 let mut c = Catalog::empty();
498 c.materialized_views.push(simple_mv("app", "my_mv"));
499 c.materialized_views.push(simple_mv("app", "my_mv"));
500
501 let result = c.canonicalize();
502 assert!(
503 matches!(
504 result,
505 Err(IrError::DuplicateObject {
506 kind: "materialized view",
507 ..
508 })
509 ),
510 "expected duplicate-mv error, got: {result:?}",
511 );
512 }
513
514 #[test]
515 fn view_owner_change_diffs() {
516 use crate::ir::eq::Equiv;
517 let mut b = simple_view("app", "active_users");
518 b.owner = Some(id("new_owner"));
519 assert!(
520 simple_view("app", "active_users")
521 .differences(&b)
522 .iter()
523 .any(|x| x.path == "owner")
524 );
525 }
526
527 #[test]
528 fn view_grants_change_diffs() {
529 use crate::ir::eq::Equiv;
530 let mut b = simple_view("app", "active_users");
531 b.grants.push(crate::ir::grant::Grant {
532 grantee: crate::ir::grant::GrantTarget::Public,
533 privilege: crate::ir::grant::Privilege::Select,
534 with_grant_option: false,
535 columns: None,
536 });
537 assert!(
538 simple_view("app", "active_users")
539 .differences(&b)
540 .iter()
541 .any(|x| x.path == "grants")
542 );
543 }
544
545 #[test]
546 fn materialized_view_owner_change_diffs() {
547 use crate::ir::eq::Equiv;
548 let mut b = simple_mv("app", "my_mv");
549 b.owner = Some(id("new_owner"));
550 assert!(
551 simple_mv("app", "my_mv")
552 .differences(&b)
553 .iter()
554 .any(|x| x.path == "owner")
555 );
556 }
557
558 #[test]
559 fn materialized_view_grants_change_diffs() {
560 use crate::ir::eq::Equiv;
561 let mut b = simple_mv("app", "my_mv");
562 b.grants.push(crate::ir::grant::Grant {
563 grantee: crate::ir::grant::GrantTarget::Public,
564 privilege: crate::ir::grant::Privilege::Select,
565 with_grant_option: false,
566 columns: None,
567 });
568 assert!(
569 simple_mv("app", "my_mv")
570 .differences(&b)
571 .iter()
572 .any(|x| x.path == "grants")
573 );
574 }
575
576 #[test]
577 fn materialized_view_storage_change_diffs() {
578 use crate::ir::eq::Equiv;
579 let mut b = simple_mv("app", "my_mv");
580 b.storage = crate::ir::reloptions::MaterializedViewStorageOptions {
581 fillfactor: Some(80),
582 ..Default::default()
583 };
584 assert!(
585 simple_mv("app", "my_mv")
586 .differences(&b)
587 .iter()
588 .any(|x| x.path == "storage")
589 );
590 }
591
592 #[test]
593 fn view_check_option_change_diffs() {
594 use crate::ir::eq::Equiv;
595 let a = simple_view("app", "v");
596 let mut b = simple_view("app", "v");
597 b.check_option = Some(CheckOption::Cascaded);
598 let d = a.differences(&b);
599 assert!(
600 d.iter().any(|x| x.path == "check_option"),
601 "check_option change must be reported (was silently ignored before): {d:?}",
602 );
603 }
604
605 #[test]
606 fn check_option_local_does_not_equal_cascaded() {
607 assert_ne!(CheckOption::Local, CheckOption::Cascaded);
608 }
609
610 #[test]
611 fn check_option_implements_copy() {
612 let a = CheckOption::Local;
613 let b = a; let c = a; assert_eq!(b, CheckOption::Local);
616 assert_eq!(c, CheckOption::Local);
617 }
618}