1use crate::ast::identifiers::ObjectId;
4use crate::db::cache::{DbCache, ForeignKeyCache, IndexCache};
5use crate::model::relation::{Persistence, RelationKind, RelationState};
6use anyhow::{Context, Result};
7use postgres::{Client, NoTls};
8use std::fs;
9use std::path::Path;
10
11pub fn sync_cache(out_path: &Path, schemas: Option<&[String]>) -> Result<()> {
12 let db_url = std::env::var("DATABASE_URL")
14 .context("DATABASE_URL environment variable is required to sync database stats. Do not pass credentials via CLI flags or config files.")?;
15
16 if out_path.exists() {
18 fs::remove_file(out_path).context("Failed to remove old cache file before sync")?;
19 }
20
21 let host = db_url
23 .split('@')
24 .nth(1)
25 .and_then(|h| h.split('/').next())
26 .unwrap_or("localhost");
27 if !host.starts_with("localhost")
28 && !host.starts_with("127.")
29 && !host.starts_with("/")
30 && host != "::1"
31 {
32 eprintln!(
33 "[WARN] Connecting to PostgreSQL at {} without TLS encryption.\n\
34 The database password will be sent in cleartext over the network.\n\
35 Use an SSH tunnel or a local connection for sensitive databases,\n\
36 or add native-tls support (see https://github.com/dsecurity49/safe-migrate).",
37 host
38 );
39 }
40
41 let mut client = Client::connect(&db_url, NoTls).context("Failed to connect to PostgreSQL")?;
42
43 let cache = populate_cache(&mut client, schemas)?;
44
45 let tmp_path = out_path.with_extension("tmp");
47 let file = std::fs::File::create(&tmp_path).context("Failed to create temporary cache file")?;
48 let writer = std::io::BufWriter::new(file);
49 let mut encoder =
50 zstd::stream::Encoder::new(writer, 3).context("Failed to init zstd compression")?;
51
52 let versioned = crate::db::cache::DbCacheVersioned::V5(cache);
53 let bincode_config = bincode::config::standard().with_variable_int_encoding();
54
55 bincode::serde::encode_into_std_write(&versioned, &mut encoder, bincode_config)
56 .context("Failed binary bincode 2.0 schema compilation and write")?;
57
58 encoder
59 .finish()
60 .context("Failed to flush final zstd stream to disk")?;
61
62 fs::rename(&tmp_path, out_path).context("Failed to atomically rename cache file")?;
63
64 Ok(())
65}
66
67pub fn populate_cache(client: &mut Client, schemas: Option<&[String]>) -> Result<DbCache> {
68 let mut cache = DbCache::new();
69
70 let schema_filter = if let Some(s) = schemas {
71 format!("AND n.nspname = ANY(ARRAY['{}'])", s.join("','"))
72 } else {
73 "".to_string()
74 };
75
76 let schema_filter_with_fk = if let Some(s) = schemas {
77 let arr = format!("ARRAY['{}']", s.join("','"));
78 format!(
79 "AND (
80 n.nspname = ANY({arr})
81 OR c.oid IN (
82 SELECT conrelid FROM pg_constraint cst
83 JOIN pg_class c2 ON c2.oid = cst.confrelid
84 JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
85 WHERE n2.nspname = ANY({arr})
86 )
87 OR c.oid IN (
88 SELECT confrelid FROM pg_constraint cst
89 JOIN pg_class c2 ON c2.oid = cst.conrelid
90 JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
91 WHERE n2.nspname = ANY({arr})
92 )
93 )"
94 )
95 } else {
96 "".to_string()
97 };
98
99 let schema_filter_n1_or_n2 = if let Some(s) = schemas {
100 let arr = format!("ARRAY['{}']", s.join("','"));
101 format!("AND (n1.nspname = ANY({arr}) OR n2.nspname = ANY({arr}))")
102 } else {
103 "".to_string()
104 };
105
106 let schema_filter_nt = if let Some(s) = schemas {
107 let arr = format!("ARRAY['{}']", s.join("','"));
108 format!(
109 "AND (
110 n_t.nspname = ANY({arr})
111 OR t.oid IN (
112 SELECT conrelid FROM pg_constraint cst
113 JOIN pg_class c2 ON c2.oid = cst.confrelid
114 JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
115 WHERE n2.nspname = ANY({arr})
116 )
117 OR t.oid IN (
118 SELECT confrelid FROM pg_constraint cst
119 JOIN pg_class c2 ON c2.oid = cst.conrelid
120 JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
121 WHERE n2.nspname = ANY({arr})
122 )
123 )"
124 )
125 } else {
126 "".to_string()
127 };
128
129 let version_row = client.query_one("SHOW server_version_num;", &[])?;
131 let version_str: String = version_row.get(0);
132 cache.pg_version_num = version_str.parse::<u32>().ok();
133
134 let search_path_row = client.query_one("SELECT current_schemas(false);", &[])?;
137 cache.search_path = search_path_row.get(0);
138
139 let table_query = format!(
141 "
142 SELECT
143 n.nspname AS schema_name,
144 c.relname AS relation_name,
145 c.relkind AS relation_kind,
146 c.relpersistence AS persistence,
147 CASE WHEN c.reltuples < 0 THEN -1 ELSE c.reltuples::bigint END AS estimated_rows,
148 c.relpages::bigint AS relpages,
149 to_char(s.last_analyze, 'YYYY-MM-DD HH24:MI:SS') AS last_analyze,
150 to_char(s.last_autoanalyze, 'YYYY-MM-DD HH24:MI:SS') AS last_autoanalyze,
151 p.partstrat::text AS partition_strategy
152 FROM pg_class c
153 JOIN pg_namespace n ON n.oid = c.relnamespace
154 LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid
155 LEFT JOIN pg_partitioned_table p ON p.partrelid = c.oid
156 WHERE c.relkind IN ('r', 'p', 'v', 'm')
157 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
158 {schema_filter_with_fk};
159 "
160 );
161
162 for row in client.query(&table_query, &[])? {
163 let schema_name: String = row.get("schema_name");
164 let relation_name: String = row.get("relation_name");
165 let relkind: i8 = row.get("relation_kind");
166 let persistence_char: i8 = row.get("persistence");
167 let raw_rows: i64 = row.get("estimated_rows");
168 let relpages: i64 = row.get("relpages");
169
170 let last_analyze: Option<String> = row.get("last_analyze");
171 let last_autoanalyze: Option<String> = row.get("last_autoanalyze");
172
173 let object_id = ObjectId::new(&schema_name, &relation_name);
174
175 let kind = match relkind as u8 {
176 b'v' => RelationKind::View,
177 b'm' => RelationKind::MaterializedView,
178 _ => RelationKind::Table,
179 };
180
181 let persistence = match persistence_char as u8 {
182 b't' => Persistence::Temporary,
183 b'u' => Persistence::Unlogged,
184 _ => Persistence::Permanent,
185 };
186
187 let estimated_rows = if raw_rows < 0 {
188 None
189 } else {
190 Some(raw_rows as u64)
191 };
192
193 let mut state = RelationState::new(
194 object_id.clone(),
195 ObjectId::new("public", "postgres"),
196 0,
197 estimated_rows,
198 kind,
199 persistence,
200 0,
201 );
202 state.relpages = Some(relpages as u64);
203 state.last_analyze = last_analyze;
204 state.last_autoanalyze = last_autoanalyze;
205
206 let partition_strategy: Option<String> = row.get("partition_strategy");
207 if let Some(ref strat) = partition_strategy {
208 state.partition_type = Some(match strat.as_str() {
209 "r" => "RANGE".to_string(),
210 "l" => "LIST".to_string(),
211 "h" => "HASH".to_string(),
212 _ => strat.to_uppercase(),
213 });
214 }
215
216 if let Some(s) = schemas
217 && !s.contains(&schema_name)
218 {
219 state.mark_fk_dependency();
220 }
221
222 cache.insert_baseline(object_id, state);
223 }
224
225 let col_query = format!("
227 SELECT
228 n.nspname AS schema_name,
229 c.relname AS relation_name,
230 a.attname AS column_name,
231 pg_catalog.format_type(a.atttypid, a.atttypmod) AS type_name,
232 a.attnotnull AS not_null,
233 s.avg_width AS avg_width,
234 pg_get_expr(ad.adbin, ad.adrelid) AS default_expr_text,
235 a.atttypmod AS type_modifier
236 FROM pg_attribute a
237 JOIN pg_class c ON a.attrelid = c.oid
238 JOIN pg_namespace n ON n.oid = c.relnamespace
239 LEFT JOIN pg_stats s ON s.schemaname = n.nspname AND s.tablename = c.relname AND s.attname = a.attname
240 LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
241 WHERE a.attnum > 0 AND NOT a.attisdropped
242 AND c.relkind IN ('r', 'p', 'v', 'm')
243 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
244 {schema_filter_with_fk}
245 ORDER BY n.nspname, c.relname;
246 ");
247
248 let mut current_object_id: Option<ObjectId> = None;
249 let mut current_rel: Option<*mut crate::model::relation::RelationState> = None;
250
251 for row in client.query(&col_query, &[])? {
252 let schema_name: String = row.get("schema_name");
253 let relation_name: String = row.get("relation_name");
254 let column_name: String = row.get("column_name");
255 let type_name: String = row.get("type_name");
256 let not_null: bool = row.get("not_null");
257 let avg_width: Option<i32> = row.get("avg_width");
258 let default_expr_text: Option<String> = row.get("default_expr_text");
259 let type_modifier: Option<i32> = row.get("type_modifier");
260
261 let is_same_rel = if let Some(ref cur) = current_object_id {
263 cur.schema == schema_name && cur.name == relation_name
264 } else {
265 false
266 };
267
268 if !is_same_rel {
269 let new_oid = ObjectId::new(&schema_name, &relation_name);
270 if let Some(rel) = cache.relations.get_mut(&new_oid) {
271 current_rel = Some(rel as *mut _);
272 } else {
273 current_rel = None;
274 }
275 current_object_id = Some(new_oid);
276 }
277
278 if let Some(rel_ptr) = current_rel {
279 let rel = unsafe { &mut *rel_ptr };
282 rel.columns.push(crate::model::column::Column {
283 name: column_name,
284 data_type: Some(type_name),
285 is_nullable: !not_null,
286 default: None,
287 avg_width,
288 default_expr_text,
289 type_modifier,
290 });
291 }
292 }
293
294 let tp_query = format!("
296 SELECT
297 n.nspname AS schema_name,
298 c.relname AS relation_name,
299 COALESCE(array_agg(DISTINCT t.tgname) FILTER (WHERE t.tgname IS NOT NULL AND t.tgisinternal = false), '{{}}') as triggers,
300 COALESCE(array_agg(DISTINCT p.polname) FILTER (WHERE p.polname IS NOT NULL), '{{}}') as policies
301 FROM pg_class c
302 JOIN pg_namespace n ON n.oid = c.relnamespace
303 LEFT JOIN pg_trigger t ON t.tgrelid = c.oid
304 LEFT JOIN pg_policy p ON p.polrelid = c.oid
305 WHERE c.relkind IN ('r', 'p', 'v', 'm') AND n.nspname NOT IN ('pg_catalog', 'information_schema')
306 {schema_filter_with_fk}
307 GROUP BY n.nspname, c.relname;
308 ");
309
310 for row in client.query(&tp_query, &[])? {
311 let schema_name: String = row.get("schema_name");
312 let relation_name: String = row.get("relation_name");
313 let triggers: Vec<String> = row.get("triggers");
314 let policies: Vec<String> = row.get("policies");
315
316 let object_id = ObjectId::new(&schema_name, &relation_name);
317
318 if let Some(rel) = cache.relations.get_mut(&object_id) {
319 rel.triggers.extend(triggers);
320 rel.policies.extend(policies);
321 }
322 }
323
324 let acl_query = format!(
326 "
327 SELECT
328 n.nspname AS schema_name,
329 c.relname AS relation_name,
330 CASE
331 WHEN acl.grantee = 0 THEN 'public'
332 ELSE pg_catalog.pg_get_userbyid(acl.grantee)
333 END AS grantee,
334 acl.privilege_type
335 FROM pg_class c
336 JOIN pg_namespace n ON n.oid = c.relnamespace
337 CROSS JOIN LATERAL pg_catalog.aclexplode(c.relacl) acl
338 WHERE c.relkind IN ('r', 'p', 'v', 'm')
339 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
340 AND acl.grantee <> c.relowner
341 {schema_filter_with_fk};
342 "
343 );
344
345 for row in client.query(&acl_query, &[])? {
346 let schema_name: String = row.get("schema_name");
347 let relation_name: String = row.get("relation_name");
348 let grantee: String = row.get("grantee");
349 let privilege_type: String = row.get("privilege_type");
350 let privilege = match privilege_type.as_str() {
351 "SELECT" => crate::model::relation::Privilege::Select,
352 "INSERT" => crate::model::relation::Privilege::Insert,
353 "UPDATE" => crate::model::relation::Privilege::Update,
354 "DELETE" => crate::model::relation::Privilege::Delete,
355 "TRUNCATE" => crate::model::relation::Privilege::Truncate,
356 "REFERENCES" => crate::model::relation::Privilege::References,
357 "TRIGGER" => crate::model::relation::Privilege::Trigger,
358 _ => continue,
359 };
360 if let Some(relation) = cache
361 .relations
362 .get_mut(&ObjectId::new(&schema_name, &relation_name))
363 {
364 relation.privileges.grant(
365 ObjectId::new("", grantee),
366 [privilege].into_iter().collect(),
367 );
368 }
369 }
370
371 let trig_query = format!(
373 "
374 SELECT
375 n.nspname AS table_schema,
376 c.relname AS table_name,
377 t.tgname AS trigger_name,
378 t.tgenabled::text AS enabled_mode,
379 fn.nspname AS function_schema,
380 f.proname || '()' AS function_name
381 FROM pg_trigger t
382 JOIN pg_class c ON c.oid = t.tgrelid
383 JOIN pg_namespace n ON n.oid = c.relnamespace
384 JOIN pg_proc f ON f.oid = t.tgfoid
385 JOIN pg_namespace fn ON fn.oid = f.pronamespace
386 WHERE t.tgisinternal = false
387 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
388 {schema_filter_with_fk};
389 "
390 );
391
392 for row in client.query(&trig_query, &[])? {
393 let table_schema: String = row.get("table_schema");
394 let table_name: String = row.get("table_name");
395 let trigger_name: String = row.get("trigger_name");
396 let enabled_mode: String = row.get("enabled_mode");
397 let function_schema: String = row.get("function_schema");
398 let function_name: String = row.get("function_name");
399
400 cache.triggers.push(crate::db::cache::TriggerCache {
401 trigger_id: ObjectId::new(&table_schema, &trigger_name),
402 table_id: ObjectId::new(&table_schema, &table_name),
403 function_id: ObjectId::new(&function_schema, &function_name),
404 enabled_mode: crate::model::trigger::TriggerEnableMode::from_pg_code(&enabled_mode)
405 .ok_or_else(|| {
406 anyhow::anyhow!("unknown pg_trigger.tgenabled value {enabled_mode}")
407 })?,
408 });
409 }
410
411 let constraint_query = format!(
413 "
414 SELECT
415 n.nspname AS table_schema,
416 c.relname AS table_name,
417 con.conname AS constraint_name,
418 con.contype::text AS constraint_type,
419 con.convalidated AS validated
420 FROM pg_constraint con
421 JOIN pg_class c ON c.oid = con.conrelid
422 JOIN pg_namespace n ON n.oid = c.relnamespace
423 WHERE con.contype IN ('c', 'f', 'p', 'u', 'x')
424 AND n.nspname NOT IN ('pg_catalog', 'information_schema')
425 {schema_filter};
426 "
427 );
428
429 for row in client.query(&constraint_query, &[])? {
430 let table_schema: String = row.get("table_schema");
431 let table_name: String = row.get("table_name");
432 let constraint_name: String = row.get("constraint_name");
433 let constraint_type: String = row.get("constraint_type");
434 let validated: bool = row.get("validated");
435 let kind = match constraint_type.as_str() {
436 "c" => crate::model::constraint::ConstraintKind::Check,
437 "f" => crate::model::constraint::ConstraintKind::ForeignKey,
438 "p" => crate::model::constraint::ConstraintKind::PrimaryKey,
439 "u" => crate::model::constraint::ConstraintKind::Unique,
440 "x" => crate::model::constraint::ConstraintKind::Exclusion,
441 _ => continue,
442 };
443 cache
444 .constraints
445 .push(crate::model::constraint::ConstraintState {
446 table_id: ObjectId::new(&table_schema, &table_name),
447 name: constraint_name,
448 kind,
449 validated,
450 });
451 }
452
453 let fk_query = format!(
455 "
456 SELECT
457 c.conname AS constraint_name,
458 n1.nspname AS from_schema, t1.relname AS from_table,
459 n2.nspname AS to_schema, t2.relname AS to_table
460 FROM pg_constraint c
461 JOIN pg_class t1 ON t1.oid = c.conrelid
462 JOIN pg_namespace n1 ON n1.oid = t1.relnamespace
463 JOIN pg_class t2 ON t2.oid = c.confrelid
464 JOIN pg_namespace n2 ON n2.oid = t2.relnamespace
465 WHERE c.contype = 'f'
466 {schema_filter_n1_or_n2};
467 "
468 );
469
470 for row in client.query(&fk_query, &[])? {
471 let constraint_name: String = row.get("constraint_name");
472 let from_schema: String = row.get("from_schema");
473 let from_table: String = row.get("from_table");
474 let to_schema: String = row.get("to_schema");
475 let to_table: String = row.get("to_table");
476
477 if let Some(s) = schemas
478 && (!s.contains(&from_schema) || !s.contains(&to_schema))
479 {
480 let out_of_scope_schema = if !s.contains(&from_schema) {
482 &from_schema
483 } else {
484 &to_schema
485 };
486 let out_of_scope_table = if !s.contains(&from_schema) {
487 &from_table
488 } else {
489 &to_table
490 };
491 eprintln!(
492 "[WARN] Foreign key '{}' crosses schema boundary. Table '{}.{}' was pulled into cache as a dependency to evaluate cross-team locks.",
493 constraint_name, out_of_scope_schema, out_of_scope_table
494 );
495 }
496
497 cache.foreign_keys.push(ForeignKeyCache {
498 constraint_name,
499 from_table: ObjectId::new(&from_schema, &from_table),
500 to_table: ObjectId::new(&to_schema, &to_table),
501 });
502 }
503
504 let idx_query = format!(
506 "
507 SELECT
508 n_i.nspname AS index_schema, i.relname AS index_name,
509 n_t.nspname AS table_schema, t.relname AS table_name
510 FROM pg_index x
511 JOIN pg_class i ON i.oid = x.indexrelid
512 JOIN pg_namespace n_i ON n_i.oid = i.relnamespace
513 JOIN pg_class t ON t.oid = x.indrelid
514 JOIN pg_namespace n_t ON n_t.oid = t.relnamespace
515 WHERE x.indisvalid = true
516 {schema_filter_nt};
517 "
518 );
519
520 for row in client.query(&idx_query, &[])? {
521 let index_schema: String = row.get("index_schema");
522 let index_name: String = row.get("index_name");
523 let table_schema: String = row.get("table_schema");
524 let table_name: String = row.get("table_name");
525
526 cache.indexes.push(IndexCache {
527 index_id: ObjectId::new(&index_schema, &index_name),
528 table_id: ObjectId::new(&table_schema, &table_name),
529 });
530 }
531
532 let func_query = format!(
534 "
535 SELECT
536 n.nspname AS schema_name,
537 p.proname AS func_name,
538 COALESCE(
539 (SELECT string_agg(pg_catalog.format_type(t, NULL), ',' ORDER BY n)
540 FROM unnest(p.proargtypes::int[]) WITH ORDINALITY AS u(t, n)),
541 ''
542 ) AS arg_types,
543 pg_catalog.pg_get_function_result(p.oid) AS return_type,
544 p.provolatile::text AS volatility,
545 l.lanname AS language,
546 p.prosecdef AS security_definer
547 FROM pg_proc p
548 JOIN pg_namespace n ON n.oid = p.pronamespace
549 JOIN pg_language l ON l.oid = p.prolang
550 WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
551 AND p.prokind = 'f'
552 {schema_filter};
553 "
554 );
555
556 for row in client.query(&func_query, &[])? {
557 let schema_name: String = row.get("schema_name");
558 let func_name: String = row.get("func_name");
559 let arg_types_str: String = row.get("arg_types");
560 let return_type: Option<String> = row.get("return_type");
561 let volatility_char: String = row.get("volatility");
562 let language: String = row.get("language");
563 let security_definer: bool = row.get("security_definer");
564
565 let volatility = match volatility_char.as_str() {
566 "v" => crate::model::function::Volatility::Volatile,
567 "s" => crate::model::function::Volatility::Stable,
568 "i" => crate::model::function::Volatility::Immutable,
569 _ => crate::model::function::Volatility::Volatile,
570 };
571
572 let security = if security_definer {
573 crate::model::function::SecurityMode::Definer
574 } else {
575 crate::model::function::SecurityMode::Invoker
576 };
577
578 let arg_types_str = arg_types_str
580 .split(',')
581 .map(|s| s.trim().to_lowercase())
582 .collect::<Vec<_>>()
583 .join(",");
584
585 let id = ObjectId::new(&schema_name, format!("{}({})", func_name, arg_types_str));
586
587 let arg_types = if arg_types_str.is_empty() {
588 Vec::new()
589 } else {
590 arg_types_str.split(',').map(|s| s.to_string()).collect()
591 };
592
593 cache.functions.insert(
594 id.clone(),
595 crate::model::function::FunctionState {
596 id,
597 arg_types,
598 return_type: return_type.unwrap_or_default(),
599 volatility,
600 language,
601 security,
602 },
603 );
604 }
605
606 let type_query = format!(
608 "
609 SELECT
610 n.nspname AS schema_name,
611 t.typname AS type_name,
612 t.typtype::text AS type_kind,
613 CASE WHEN t.typtype = 'd'
614 THEN pg_catalog.format_type(t.typbasetype, t.typtypmod)
615 ELSE NULL
616 END AS domain_base_type,
617 COALESCE(
618 array_agg(e.enumlabel ORDER BY e.enumsortorder)
619 FILTER (WHERE e.enumlabel IS NOT NULL),
620 ARRAY[]::text[]
621 ) AS enum_labels
622 FROM pg_type t
623 JOIN pg_namespace n ON n.oid = t.typnamespace
624 LEFT JOIN pg_enum e ON e.enumtypid = t.oid
625 WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
626 AND t.typtype IN ('e', 'd')
627 {schema_filter}
628 GROUP BY n.nspname, t.typname, t.typtype, t.typbasetype, t.typtypmod;
629 "
630 );
631
632 for row in client.query(&type_query, &[])? {
633 let schema_name: String = row.get("schema_name");
634 let type_name: String = row.get("type_name");
635 let type_kind: String = row.get("type_kind");
636 let domain_base_type: Option<String> = row.get("domain_base_type");
637 let enum_labels: Vec<String> = row.get("enum_labels");
638 let kind = match type_kind.as_str() {
639 "e" => crate::model::types::TypeKind::Enum {
640 variants: enum_labels,
641 },
642 "d" => crate::model::types::TypeKind::Domain {
643 base_type: domain_base_type.unwrap_or_default(),
644 },
645 _ => continue,
646 };
647 let id = ObjectId::new(&schema_name, &type_name);
648 cache.types.insert(
649 id.clone(),
650 crate::model::types::TypeState {
651 id,
652 generation: 0,
653 kind,
654 },
655 );
656 }
657
658 let dependency_schemas = schemas.map(|items| items.to_vec());
660 let depend_query = r#"
661 SELECT
662 d.classid, d.objid, d.objsubid,
663 d.refclassid, d.refobjid, d.refobjsubid,
664 d.deptype::text,
665 COALESCE(n1.nspname, n1p.nspname, n1t.nspname) AS obj_schema,
666 COALESCE(c1.relname, p1.proname, t1.typname) AS obj_name,
667 COALESCE(n2.nspname, n2p.nspname, n2t.nspname) AS ref_schema,
668 COALESCE(c2.relname, p2.proname, t2.typname) AS ref_name
669 FROM pg_depend d
670 LEFT JOIN pg_class c1 ON c1.oid = d.objid AND d.classid = 'pg_class'::regclass
671 LEFT JOIN pg_namespace n1 ON n1.oid = c1.relnamespace
672 LEFT JOIN pg_proc p1 ON p1.oid = d.objid AND d.classid = 'pg_proc'::regclass
673 LEFT JOIN pg_namespace n1p ON n1p.oid = p1.pronamespace
674 LEFT JOIN pg_type t1 ON t1.oid = d.objid AND d.classid = 'pg_type'::regclass
675 LEFT JOIN pg_namespace n1t ON n1t.oid = t1.typnamespace
676 LEFT JOIN pg_class c2 ON c2.oid = d.refobjid AND d.refclassid = 'pg_class'::regclass
677 LEFT JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
678 LEFT JOIN pg_proc p2 ON p2.oid = d.refobjid AND d.refclassid = 'pg_proc'::regclass
679 LEFT JOIN pg_namespace n2p ON n2p.oid = p2.pronamespace
680 LEFT JOIN pg_type t2 ON t2.oid = d.refobjid AND d.refclassid = 'pg_type'::regclass
681 LEFT JOIN pg_namespace n2t ON n2t.oid = t2.typnamespace
682 WHERE d.deptype IN ('n', 'a', 'i')
683 AND COALESCE(n1.nspname, n1p.nspname, n1t.nspname) IS NOT NULL
684 AND COALESCE(n1.nspname, n1p.nspname, n1t.nspname)
685 NOT IN ('pg_catalog', 'information_schema')
686 AND (
687 $1::text[] IS NULL
688 OR COALESCE(n1.nspname, n1p.nspname, n1t.nspname) = ANY($1)
689 )
690 "#;
691
692 for row in client.query(depend_query, &[&dependency_schemas])? {
693 let classid: u32 = row.get(0);
694 let objid: u32 = row.get(1);
695 let objsubid: i32 = row.get(2);
696 let refclassid: u32 = row.get(3);
697 let refobjid: u32 = row.get(4);
698 let refobjsubid: i32 = row.get(5);
699 let deptype: String = row.get(6);
700 let obj_schema: Option<String> = row.get(7);
701 let obj_name: Option<String> = row.get(8);
702 let ref_schema: Option<String> = row.get(9);
703 let ref_name: Option<String> = row.get(10);
704
705 cache.dependencies.push(crate::db::cache::DependencyCache {
706 classid,
707 objid,
708 objsubid,
709 refclassid,
710 refobjid,
711 refobjsubid,
712 deptype,
713 obj_schema,
714 obj_name,
715 ref_schema,
716 ref_name,
717 });
718 }
719
720 let view_depend_query = r#"
723 SELECT DISTINCT
724 'pg_class'::regclass::oid AS classid,
725 vc.oid AS objid,
726 0 AS objsubid,
727 'pg_class'::regclass::oid AS refclassid,
728 tc.oid AS refobjid,
729 0 AS refobjsubid,
730 vn.nspname AS obj_schema,
731 vc.relname AS obj_name,
732 tn.nspname AS ref_schema,
733 tc.relname AS ref_name
734 FROM pg_rewrite rw
735 JOIN pg_class vc ON vc.oid = rw.ev_class
736 JOIN pg_namespace vn ON vn.oid = vc.relnamespace
737 JOIN pg_depend d ON d.objid = rw.oid
738 JOIN pg_class tc ON tc.oid = d.refobjid
739 JOIN pg_namespace tn ON tn.oid = tc.relnamespace
740 WHERE vc.relkind IN ('v', 'm')
741 AND d.deptype = 'n'
742 AND (
743 $1::text[] IS NULL
744 OR (vn.nspname = ANY($1) AND tn.nspname = ANY($1))
745 )
746 "#;
747
748 for row in client.query(view_depend_query, &[&dependency_schemas])? {
749 cache.dependencies.push(crate::db::cache::DependencyCache {
750 classid: row.get(0),
751 objid: row.get(1),
752 objsubid: row.get(2),
753 refclassid: row.get(3),
754 refobjid: row.get(4),
755 refobjsubid: row.get(5),
756 deptype: "view".to_string(),
757 obj_schema: Some(row.get(6)),
758 obj_name: Some(row.get(7)),
759 ref_schema: Some(row.get(8)),
760 ref_name: Some(row.get(9)),
761 });
762 }
763
764 Ok(cache)
765}