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::{Diff, diff_field};
24use crate::parse::normalize_body::NormalizedBody;
25use crate::plan::edges::DepEdge;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct ViewColumn {
48 pub name: Identifier,
50 pub column_type: ColumnType,
56 pub comment: Option<String>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub struct View {
68 pub qname: QualifiedName,
70 pub columns: Vec<ViewColumn>,
72 pub body_canonical: NormalizedBody,
74 pub body_dependencies: Vec<DepEdge>,
76 pub security_barrier: Option<bool>,
78 pub security_invoker: Option<bool>,
80 pub check_option: Option<CheckOption>,
84 pub comment: Option<String>,
86 #[serde(skip, default)]
91 pub raw_body: String,
92 pub owner: Option<Identifier>,
95 pub grants: Vec<crate::ir::grant::Grant>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub struct MaterializedView {
106 pub qname: QualifiedName,
108 pub columns: Vec<ViewColumn>,
110 pub body_canonical: NormalizedBody,
112 pub body_dependencies: Vec<DepEdge>,
114 pub comment: Option<String>,
116 #[serde(skip, default)]
121 pub raw_body: String,
122 pub owner: Option<Identifier>,
125 pub grants: Vec<crate::ir::grant::Grant>,
127 pub storage: crate::ir::reloptions::MaterializedViewStorageOptions,
129}
130
131impl Diff for View {
132 fn diff(&self, other: &Self) -> Vec<Difference> {
133 let mut out = Vec::new();
134 out.extend(diff_field("qname", &self.qname, &other.qname));
135 out.extend(diff_field(
136 "body_canonical",
137 &self.body_canonical.canonical_text(),
138 &other.body_canonical.canonical_text(),
139 ));
140 out.extend(diff_field(
141 "security_barrier",
142 &format!("{:?}", self.security_barrier),
143 &format!("{:?}", other.security_barrier),
144 ));
145 out.extend(diff_field(
146 "security_invoker",
147 &format!("{:?}", self.security_invoker),
148 &format!("{:?}", other.security_invoker),
149 ));
150 out.extend(diff_field(
151 "comment",
152 &format!("{:?}", self.comment),
153 &format!("{:?}", other.comment),
154 ));
155 out.extend(diff_field(
156 "owner",
157 &format!("{:?}", self.owner),
158 &format!("{:?}", other.owner),
159 ));
160 out.extend(diff_field(
161 "grants",
162 &format!("{:?}", self.grants),
163 &format!("{:?}", other.grants),
164 ));
165
166 let lhs: BTreeMap<_, _> = self.columns.iter().map(|c| (c.name.as_str(), c)).collect();
168 let rhs: BTreeMap<_, _> = other.columns.iter().map(|c| (c.name.as_str(), c)).collect();
169 for (name, l) in &lhs {
170 match rhs.get(name) {
171 None => out.push(Difference::new(
172 format!("columns.{name}"),
173 "present",
174 "removed",
175 )),
176 Some(r) => {
177 if l.column_type != r.column_type {
178 out.push(Difference::new(
179 format!("columns.{name}.column_type"),
180 l.column_type.render_sql(),
181 r.column_type.render_sql(),
182 ));
183 }
184 if l.comment != r.comment {
185 out.push(Difference::new(
186 format!("columns.{name}.comment"),
187 format!("{:?}", l.comment),
188 format!("{:?}", r.comment),
189 ));
190 }
191 }
192 }
193 }
194 for name in rhs.keys() {
195 if !lhs.contains_key(name) {
196 out.push(Difference::new(
197 format!("columns.{name}"),
198 "missing",
199 "added",
200 ));
201 }
202 }
203 let lhs_order: Vec<&str> = self.columns.iter().map(|c| c.name.as_str()).collect();
204 let rhs_order: Vec<&str> = other.columns.iter().map(|c| c.name.as_str()).collect();
205 if lhs_order != rhs_order {
206 out.push(Difference::new(
207 "columns.<order>",
208 lhs_order.join(","),
209 rhs_order.join(","),
210 ));
211 }
212
213 out.extend(diff_field(
215 "body_dependencies",
216 &format!("{:?}", self.body_dependencies),
217 &format!("{:?}", other.body_dependencies),
218 ));
219
220 out
221 }
222}
223
224impl Diff for MaterializedView {
225 fn diff(&self, other: &Self) -> Vec<Difference> {
226 let mut out = Vec::new();
227 out.extend(diff_field("qname", &self.qname, &other.qname));
228 out.extend(diff_field(
229 "body_canonical",
230 &self.body_canonical.canonical_text(),
231 &other.body_canonical.canonical_text(),
232 ));
233 out.extend(diff_field(
234 "comment",
235 &format!("{:?}", self.comment),
236 &format!("{:?}", other.comment),
237 ));
238 out.extend(diff_field(
239 "owner",
240 &format!("{:?}", self.owner),
241 &format!("{:?}", other.owner),
242 ));
243 out.extend(diff_field(
244 "grants",
245 &format!("{:?}", self.grants),
246 &format!("{:?}", other.grants),
247 ));
248 out.extend(diff_field(
249 "storage",
250 &format!("{:?}", self.storage),
251 &format!("{:?}", other.storage),
252 ));
253
254 let lhs: BTreeMap<_, _> = self.columns.iter().map(|c| (c.name.as_str(), c)).collect();
256 let rhs: BTreeMap<_, _> = other.columns.iter().map(|c| (c.name.as_str(), c)).collect();
257 for (name, l) in &lhs {
258 match rhs.get(name) {
259 None => out.push(Difference::new(
260 format!("columns.{name}"),
261 "present",
262 "removed",
263 )),
264 Some(r) => {
265 if l.column_type != r.column_type {
266 out.push(Difference::new(
267 format!("columns.{name}.column_type"),
268 l.column_type.render_sql(),
269 r.column_type.render_sql(),
270 ));
271 }
272 if l.comment != r.comment {
273 out.push(Difference::new(
274 format!("columns.{name}.comment"),
275 format!("{:?}", l.comment),
276 format!("{:?}", r.comment),
277 ));
278 }
279 }
280 }
281 }
282 for name in rhs.keys() {
283 if !lhs.contains_key(name) {
284 out.push(Difference::new(
285 format!("columns.{name}"),
286 "missing",
287 "added",
288 ));
289 }
290 }
291 let lhs_order: Vec<&str> = self.columns.iter().map(|c| c.name.as_str()).collect();
292 let rhs_order: Vec<&str> = other.columns.iter().map(|c| c.name.as_str()).collect();
293 if lhs_order != rhs_order {
294 out.push(Difference::new(
295 "columns.<order>",
296 lhs_order.join(","),
297 rhs_order.join(","),
298 ));
299 }
300
301 out.extend(diff_field(
303 "body_dependencies",
304 &format!("{:?}", self.body_dependencies),
305 &format!("{:?}", other.body_dependencies),
306 ));
307
308 out
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::ir::IrError;
316 use crate::ir::catalog::Catalog;
317 use crate::ir::column_type::ColumnType;
318 use crate::plan::edges::{DepSource, NodeId};
319
320 fn id(s: &str) -> Identifier {
321 Identifier::from_unquoted(s).unwrap()
322 }
323
324 fn qn(schema: &str, name: &str) -> QualifiedName {
325 QualifiedName::new(id(schema), id(name))
326 }
327
328 fn body(sql: &str) -> NormalizedBody {
329 NormalizedBody::from_sql(sql).unwrap()
330 }
331
332 fn simple_view(schema: &str, name: &str) -> View {
333 View {
334 qname: qn(schema, name),
335 columns: vec![ViewColumn {
336 name: id("id"),
337 column_type: ColumnType::BigInt,
338 comment: None,
339 }],
340 body_canonical: body("SELECT 1"),
341 body_dependencies: vec![],
342 security_barrier: None,
343 security_invoker: None,
344 check_option: None,
345 comment: None,
346 raw_body: String::new(),
347 owner: None,
348 grants: vec![],
349 }
350 }
351
352 fn simple_mv(schema: &str, name: &str) -> MaterializedView {
353 MaterializedView {
354 qname: qn(schema, name),
355 columns: vec![],
356 body_canonical: body("SELECT 1"),
357 body_dependencies: vec![],
358 comment: None,
359 raw_body: String::new(),
360 owner: None,
361 grants: vec![],
362 storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
363 }
364 }
365
366 #[test]
367 fn views_with_equal_fields_compare_equal() {
368 let v1 = simple_view("app", "active_users");
369 let v2 = View {
370 qname: qn("app", "active_users"),
371 columns: vec![ViewColumn {
372 name: id("id"),
373 column_type: ColumnType::BigInt,
374 comment: None,
375 }],
376 body_canonical: body("SELECT 1"),
377 body_dependencies: vec![],
378 security_barrier: None,
379 security_invoker: None,
380 check_option: None,
381 comment: None,
382 raw_body: String::new(),
383 owner: None,
384 grants: vec![],
385 };
386 assert_eq!(v1, v2);
387 }
388
389 #[test]
390 fn materialized_view_round_trips_through_serde() {
391 let mv = MaterializedView {
392 qname: qn("app", "summary"),
393 columns: vec![ViewColumn {
394 name: id("total"),
395 column_type: ColumnType::BigInt,
396 comment: Some("total count".to_string()),
397 }],
398 body_canonical: body("SELECT count(*) FROM users"),
399 body_dependencies: vec![DepEdge {
400 from: NodeId::Table(qn("app", "summary")),
401 to: NodeId::Table(qn("app", "users")),
402 source: DepSource::AstExtracted,
403 }],
404 comment: Some("materialized summary".to_string()),
405 raw_body: String::new(),
406 owner: None,
407 grants: vec![],
408 storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
409 };
410 let json = serde_json::to_string(&mv).expect("serialization must succeed");
411 let roundtripped: MaterializedView =
412 serde_json::from_str(&json).expect("deserialization must succeed");
413 assert_eq!(mv, roundtripped);
414 }
415
416 #[test]
417 fn catalog_with_views_canonicalizes() {
418 let mut c = Catalog::empty();
419 c.views.push(simple_view("app", "zzz_view"));
420 c.views.push(simple_view("app", "aaa_view"));
421 c.materialized_views.push(simple_mv("app", "zzz_mv"));
422 c.materialized_views.push(simple_mv("app", "aaa_mv"));
423
424 let result = c.canonicalize();
425 assert!(result.is_ok(), "canonicalize should succeed: {result:?}");
426 let canonical = result.unwrap();
427
428 assert_eq!(canonical.views[0].qname, qn("app", "aaa_view"));
429 assert_eq!(canonical.views[1].qname, qn("app", "zzz_view"));
430 assert_eq!(canonical.materialized_views[0].qname, qn("app", "aaa_mv"));
431 assert_eq!(canonical.materialized_views[1].qname, qn("app", "zzz_mv"));
432 }
433
434 #[test]
435 fn catalog_rejects_duplicate_view_qname() {
436 let mut c = Catalog::empty();
437 c.views.push(simple_view("app", "my_view"));
438 c.views.push(simple_view("app", "my_view"));
439
440 let result = c.canonicalize();
441 assert!(
442 matches!(result, Err(IrError::InvalidIdentifier(_))),
443 "expected duplicate-view error, got: {result:?}",
444 );
445 }
446
447 #[test]
448 fn catalog_rejects_duplicate_materialized_view_qname() {
449 let mut c = Catalog::empty();
450 c.materialized_views.push(simple_mv("app", "my_mv"));
451 c.materialized_views.push(simple_mv("app", "my_mv"));
452
453 let result = c.canonicalize();
454 assert!(
455 matches!(result, Err(IrError::InvalidIdentifier(_))),
456 "expected duplicate-mv error, got: {result:?}",
457 );
458 }
459
460 #[test]
461 fn view_owner_change_diffs() {
462 use crate::ir::eq::Diff;
463 let mut b = simple_view("app", "active_users");
464 b.owner = Some(id("new_owner"));
465 assert!(
466 simple_view("app", "active_users")
467 .diff(&b)
468 .iter()
469 .any(|x| x.path == "owner")
470 );
471 }
472
473 #[test]
474 fn view_grants_change_diffs() {
475 use crate::ir::eq::Diff;
476 let mut b = simple_view("app", "active_users");
477 b.grants.push(crate::ir::grant::Grant {
478 grantee: crate::ir::grant::GrantTarget::Public,
479 privilege: crate::ir::grant::Privilege::Select,
480 with_grant_option: false,
481 columns: None,
482 });
483 assert!(
484 simple_view("app", "active_users")
485 .diff(&b)
486 .iter()
487 .any(|x| x.path == "grants")
488 );
489 }
490
491 #[test]
492 fn materialized_view_owner_change_diffs() {
493 use crate::ir::eq::Diff;
494 let mut b = simple_mv("app", "my_mv");
495 b.owner = Some(id("new_owner"));
496 assert!(
497 simple_mv("app", "my_mv")
498 .diff(&b)
499 .iter()
500 .any(|x| x.path == "owner")
501 );
502 }
503
504 #[test]
505 fn materialized_view_grants_change_diffs() {
506 use crate::ir::eq::Diff;
507 let mut b = simple_mv("app", "my_mv");
508 b.grants.push(crate::ir::grant::Grant {
509 grantee: crate::ir::grant::GrantTarget::Public,
510 privilege: crate::ir::grant::Privilege::Select,
511 with_grant_option: false,
512 columns: None,
513 });
514 assert!(
515 simple_mv("app", "my_mv")
516 .diff(&b)
517 .iter()
518 .any(|x| x.path == "grants")
519 );
520 }
521
522 #[test]
523 fn materialized_view_storage_change_diffs() {
524 use crate::ir::eq::Diff;
525 let mut b = simple_mv("app", "my_mv");
526 b.storage = crate::ir::reloptions::MaterializedViewStorageOptions {
527 fillfactor: Some(80),
528 ..Default::default()
529 };
530 assert!(
531 simple_mv("app", "my_mv")
532 .diff(&b)
533 .iter()
534 .any(|x| x.path == "storage")
535 );
536 }
537
538 #[test]
539 fn check_option_local_does_not_equal_cascaded() {
540 assert_ne!(CheckOption::Local, CheckOption::Cascaded);
541 }
542
543 #[test]
544 fn check_option_implements_copy() {
545 let a = CheckOption::Local;
546 let b = a; let c = a; assert_eq!(b, CheckOption::Local);
549 assert_eq!(c, CheckOption::Local);
550 }
551}