database_replicator/migration/
dump.rs1use crate::filters::ReplicationFilter;
5use anyhow::{Context, Result};
6use std::collections::BTreeSet;
7use std::fs;
8use std::process::{Command, Stdio};
9use std::time::Duration;
10
11pub async fn dump_globals(source_url: &str, output_path: &str) -> Result<()> {
13 tracing::info!("Dumping global objects to {}", output_path);
14
15 let parts = crate::utils::parse_postgres_url(source_url)
17 .with_context(|| format!("Failed to parse source URL: {}", source_url))?;
18 let pgpass = crate::utils::PgPassFile::new(&parts)
19 .context("Failed to create .pgpass file for authentication")?;
20
21 let env_vars = parts.to_pg_env_vars();
22 let output_path_owned = output_path.to_string();
23
24 crate::utils::retry_subprocess_with_backoff(
26 || {
27 let mut cmd = Command::new("pg_dumpall");
28 cmd.arg("--globals-only")
29 .arg("--no-role-passwords") .arg("--verbose") .arg("--host")
32 .arg(&parts.host)
33 .arg("--port")
34 .arg(parts.port.to_string())
35 .arg("--database")
36 .arg(&parts.database)
37 .arg(format!("--file={}", output_path_owned))
38 .env("PGPASSFILE", pgpass.path())
39 .stdout(Stdio::inherit())
40 .stderr(Stdio::inherit());
41
42 if let Some(user) = &parts.user {
44 cmd.arg("--username").arg(user);
45 }
46
47 for (env_var, value) in &env_vars {
49 cmd.env(env_var, value);
50 }
51
52 for (env_var, value) in crate::utils::get_keepalive_env_vars() {
54 cmd.env(env_var, value);
55 }
56
57 cmd.status().context(
58 "Failed to execute pg_dumpall. Is PostgreSQL client installed?\n\
59 Install with:\n\
60 - Ubuntu/Debian: sudo apt-get install postgresql-client\n\
61 - macOS: brew install postgresql\n\
62 - RHEL/CentOS: sudo yum install postgresql",
63 )
64 },
65 3, Duration::from_secs(1), "pg_dumpall (dump globals)",
68 )
69 .await
70 .context(
71 "pg_dumpall failed to dump global objects.\n\
72 \n\
73 Common causes:\n\
74 - Connection authentication failed\n\
75 - User lacks sufficient privileges (need SUPERUSER or pg_read_all_settings role)\n\
76 - Network connectivity issues\n\
77 - Invalid connection string\n\
78 - Connection timeout or network issues",
79 )?;
80
81 tracing::info!("✓ Global objects dumped successfully");
82 Ok(())
83}
84
85pub fn sanitize_globals_dump(path: &str) -> Result<()> {
96 let content = fs::read_to_string(path)
97 .with_context(|| format!("Failed to read globals dump at {}", path))?;
98
99 if let Some(updated) = rewrite_create_role_statements(&content) {
100 fs::write(path, updated)
101 .with_context(|| format!("Failed to update globals dump at {}", path))?;
102 }
103
104 Ok(())
105}
106
107pub fn remove_superuser_from_globals(path: &str) -> Result<()> {
113 let content = fs::read_to_string(path)
114 .with_context(|| format!("Failed to read globals dump at {}", path))?;
115
116 let mut updated = String::with_capacity(content.len());
117 let mut modified = false;
118 for line in content.lines() {
119 if line.contains("ALTER ROLE") && line.contains("SUPERUSER") {
120 updated.push_str("-- ");
121 updated.push_str(line);
122 updated.push('\n');
123 modified = true;
124 } else {
125 updated.push_str(line);
126 updated.push('\n');
127 }
128 }
129
130 if modified {
131 fs::write(path, updated)
132 .with_context(|| format!("Failed to write sanitized globals dump to {}", path))?;
133 }
134
135 Ok(())
136}
137
138pub fn remove_restricted_guc_settings(path: &str) -> Result<()> {
144 let content = fs::read_to_string(path)
145 .with_context(|| format!("Failed to read globals dump at {}", path))?;
146
147 let mut updated = String::with_capacity(content.len());
148 let mut modified = false;
149
150 for line in content.lines() {
151 let lower_line = line.to_ascii_lowercase();
152 if lower_line.contains("alter role") && lower_line.contains("set") {
153 updated.push_str("-- ");
154 updated.push_str(line);
155 updated.push('\n');
156 modified = true;
157 } else {
158 updated.push_str(line);
159 updated.push('\n');
160 }
161 }
162
163 if modified {
164 fs::write(path, updated)
165 .with_context(|| format!("Failed to write sanitized globals dump to {}", path))?;
166 }
167
168 Ok(())
169}
170
171pub fn remove_restricted_role_grants(path: &str) -> Result<()> {
177 const RESTRICTED_ROLES: &[&str] = &[
179 "pg_checkpoint",
180 "pg_read_all_data",
181 "pg_write_all_data",
182 "pg_read_all_settings",
183 "pg_read_all_stats",
184 "pg_stat_scan_tables",
185 "pg_monitor",
186 "pg_signal_backend",
187 "pg_read_server_files",
188 "pg_write_server_files",
189 "pg_execute_server_program",
190 "pg_create_subscription",
191 "pg_maintain",
192 "pg_use_reserved_connections",
193 ];
194
195 let content = fs::read_to_string(path)
196 .with_context(|| format!("Failed to read globals dump at {}", path))?;
197
198 let mut updated = String::with_capacity(content.len());
199 let mut modified = false;
200
201 for line in content.lines() {
202 let lower_trimmed = line.trim().to_ascii_lowercase();
203 if lower_trimmed.starts_with("grant ") {
204 let is_restricted = RESTRICTED_ROLES.iter().any(|role| {
205 lower_trimmed
208 .split_whitespace()
209 .nth(1)
210 .map(|r| r.trim_matches('"') == *role)
211 .unwrap_or(false)
212 });
213
214 if is_restricted {
215 updated.push_str("-- ");
216 updated.push_str(line);
217 updated.push('\n');
218 modified = true;
219 continue;
220 }
221 }
222
223 updated.push_str(line);
224 updated.push('\n');
225 }
226
227 if modified {
228 fs::write(path, updated)
229 .with_context(|| format!("Failed to write sanitized globals dump to {}", path))?;
230 }
231
232 Ok(())
233}
234
235fn rewrite_create_role_statements(sql: &str) -> Option<String> {
236 if sql.is_empty() {
237 return None;
238 }
239
240 let mut output = String::with_capacity(sql.len() + 1024);
241 let mut modified = false;
242 let mut cursor = 0;
243
244 while cursor < sql.len() {
245 if let Some(rel_pos) = sql[cursor..].find('\n') {
246 let end = cursor + rel_pos + 1;
247 let chunk = &sql[cursor..end];
248 if let Some(transformed) = wrap_create_role_line(chunk) {
249 output.push_str(&transformed);
250 modified = true;
251 } else {
252 output.push_str(chunk);
253 }
254 cursor = end;
255 } else {
256 let chunk = &sql[cursor..];
257 if let Some(transformed) = wrap_create_role_line(chunk) {
258 output.push_str(&transformed);
259 modified = true;
260 } else {
261 output.push_str(chunk);
262 }
263 break;
264 }
265 }
266
267 if modified {
268 Some(output)
269 } else {
270 None
271 }
272}
273
274fn wrap_create_role_line(chunk: &str) -> Option<String> {
275 let trimmed = chunk.trim_start();
276 if !trimmed.starts_with("CREATE ROLE ") {
277 return None;
278 }
279
280 let statement = trimmed.trim_end();
281 let statement_body = statement.trim_end_matches(';').trim_end();
282 let leading_ws_len = chunk.len() - trimmed.len();
283 let leading_ws = &chunk[..leading_ws_len];
284 let newline = if chunk.ends_with("\r\n") {
285 "\r\n"
286 } else if chunk.ends_with('\n') {
287 "\n"
288 } else {
289 ""
290 };
291
292 let role_token = extract_role_token(statement_body)?;
293
294 let notice_name = escape_single_quotes(&unquote_role_name(&role_token));
295
296 let mut block = String::with_capacity(chunk.len() + 128);
297 block.push_str(leading_ws);
298 block.push_str("DO $$\n");
299 block.push_str(leading_ws);
300 block.push_str("BEGIN\n");
301 block.push_str(leading_ws);
302 block.push_str(" ");
303 block.push_str(statement_body);
304 block.push_str(";\n");
305 block.push_str(leading_ws);
306 block.push_str("EXCEPTION\n");
307 block.push_str(leading_ws);
308 block.push_str(" WHEN duplicate_object THEN\n");
309 block.push_str(leading_ws);
310 block.push_str(" RAISE NOTICE 'Role ");
311 block.push_str(¬ice_name);
312 block.push_str(" already exists on target, skipping CREATE ROLE';\n");
313 block.push_str(leading_ws);
314 block.push_str("END $$;");
315
316 if !newline.is_empty() {
317 block.push_str(newline);
318 }
319
320 Some(block)
321}
322
323fn extract_role_token(statement: &str) -> Option<String> {
324 let remainder = statement.strip_prefix("CREATE ROLE")?.trim_start();
325
326 if remainder.starts_with('"') {
327 let mut idx = 1;
328 let bytes = remainder.as_bytes();
329 while idx < bytes.len() {
330 if bytes[idx] == b'"' {
331 if idx + 1 < bytes.len() && bytes[idx + 1] == b'"' {
332 idx += 2;
333 continue;
334 } else {
335 idx += 1;
336 break;
337 }
338 }
339 idx += 1;
340 }
341 if idx <= remainder.len() {
342 return Some(remainder[..idx].to_string());
343 }
344 None
345 } else {
346 let mut end = remainder.len();
347 for (i, ch) in remainder.char_indices() {
348 if ch.is_whitespace() || ch == ';' {
349 end = i;
350 break;
351 }
352 }
353 if end == 0 {
354 None
355 } else {
356 Some(remainder[..end].to_string())
357 }
358 }
359}
360
361fn unquote_role_name(token: &str) -> String {
362 if token.starts_with('"') && token.ends_with('"') && token.len() >= 2 {
363 let inner = &token[1..token.len() - 1];
364 inner.replace("\"\"", "\"")
365 } else {
366 token.to_string()
367 }
368}
369
370fn escape_single_quotes(value: &str) -> String {
371 value.replace('\'', "''")
372}
373
374pub async fn dump_schema(
376 source_url: &str,
377 database: &str,
378 output_path: &str,
379 filter: &ReplicationFilter,
380) -> Result<()> {
381 tracing::info!(
382 "Dumping schema for database '{}' to {}",
383 database,
384 output_path
385 );
386
387 let parts = crate::utils::parse_postgres_url(source_url)
389 .with_context(|| format!("Failed to parse source URL: {}", source_url))?;
390 let pgpass = crate::utils::PgPassFile::new(&parts)
391 .context("Failed to create .pgpass file for authentication")?;
392
393 let env_vars = parts.to_pg_env_vars();
394 let output_path_owned = output_path.to_string();
395
396 let exclude_tables = get_schema_excluded_tables_for_db(filter, database);
398 let include_tables = get_included_tables_for_db(filter, database);
399
400 crate::utils::retry_subprocess_with_backoff(
402 || {
403 let mut cmd = Command::new("pg_dump");
404 cmd.arg("--schema-only")
405 .arg("--no-owner") .arg("--no-privileges") .arg("--verbose"); if let Some(ref exclude) = exclude_tables {
412 if !exclude.is_empty() {
413 for table in exclude {
414 cmd.arg("--exclude-table").arg(table);
415 }
416 }
417 }
418
419 if let Some(ref include) = include_tables {
421 if !include.is_empty() {
422 for table in include {
423 cmd.arg("--table").arg(table);
424 }
425 }
426 }
427
428 cmd.arg("--host")
429 .arg(&parts.host)
430 .arg("--port")
431 .arg(parts.port.to_string())
432 .arg("--dbname")
433 .arg(&parts.database)
434 .arg(format!("--file={}", output_path_owned))
435 .env("PGPASSFILE", pgpass.path())
436 .stdout(Stdio::inherit())
437 .stderr(Stdio::inherit());
438
439 if let Some(user) = &parts.user {
441 cmd.arg("--username").arg(user);
442 }
443
444 for (env_var, value) in &env_vars {
446 cmd.env(env_var, value);
447 }
448
449 for (env_var, value) in crate::utils::get_keepalive_env_vars() {
451 cmd.env(env_var, value);
452 }
453
454 cmd.status().context(
455 "Failed to execute pg_dump. Is PostgreSQL client installed?\n\
456 Install with:\n\
457 - Ubuntu/Debian: sudo apt-get install postgresql-client\n\
458 - macOS: brew install postgresql\n\
459 - RHEL/CentOS: sudo yum install postgresql",
460 )
461 },
462 3, Duration::from_secs(1), "pg_dump (dump schema)",
465 )
466 .await
467 .with_context(|| {
468 format!(
469 "pg_dump failed to dump schema for database '{}'.\n\
470 \n\
471 Common causes:\n\
472 - Database does not exist\n\
473 - Connection authentication failed\n\
474 - User lacks privileges to read database schema\n\
475 - Network connectivity issues\n\
476 - Connection timeout or network issues",
477 database
478 )
479 })?;
480
481 tracing::info!("✓ Schema dumped successfully");
482 Ok(())
483}
484
485pub async fn dump_data(
495 source_url: &str,
496 database: &str,
497 output_path: &str,
498 filter: &ReplicationFilter,
499) -> Result<()> {
500 let num_cpus = std::thread::available_parallelism()
502 .map(|n| n.get().min(8))
503 .unwrap_or(4);
504
505 tracing::info!(
506 "Dumping data for database '{}' to {} (parallel={}, compression=9, format=directory)",
507 database,
508 output_path,
509 num_cpus
510 );
511
512 let parts = crate::utils::parse_postgres_url(source_url)
514 .with_context(|| format!("Failed to parse source URL: {}", source_url))?;
515 let pgpass = crate::utils::PgPassFile::new(&parts)
516 .context("Failed to create .pgpass file for authentication")?;
517
518 let env_vars = parts.to_pg_env_vars();
519 let output_path_owned = output_path.to_string();
520
521 let exclude_tables = get_data_excluded_tables_for_db(filter, database);
523 let include_tables = get_included_tables_for_db(filter, database);
524
525 crate::utils::retry_subprocess_with_backoff(
527 || {
528 let mut cmd = Command::new("pg_dump");
529 cmd.arg("--data-only")
530 .arg("--no-owner")
531 .arg("--format=directory") .arg("--blobs") .arg("--compress=9") .arg(format!("--jobs={}", num_cpus)) .arg("--verbose"); if let Some(ref exclude) = exclude_tables {
540 if !exclude.is_empty() {
541 for table in exclude {
542 cmd.arg("--exclude-table-data").arg(table);
543 }
544 }
545 }
546
547 if let Some(ref include) = include_tables {
549 if !include.is_empty() {
550 for table in include {
551 cmd.arg("--table").arg(table);
552 }
553 }
554 }
555
556 cmd.arg("--host")
557 .arg(&parts.host)
558 .arg("--port")
559 .arg(parts.port.to_string())
560 .arg("--dbname")
561 .arg(&parts.database)
562 .arg(format!("--file={}", output_path_owned))
563 .env("PGPASSFILE", pgpass.path())
564 .stdout(Stdio::inherit())
565 .stderr(Stdio::inherit());
566
567 if let Some(user) = &parts.user {
569 cmd.arg("--username").arg(user);
570 }
571
572 for (env_var, value) in &env_vars {
574 cmd.env(env_var, value);
575 }
576
577 for (env_var, value) in crate::utils::get_keepalive_env_vars() {
579 cmd.env(env_var, value);
580 }
581
582 cmd.status().context(
583 "Failed to execute pg_dump. Is PostgreSQL client installed?\n\
584 Install with:\n\
585 - Ubuntu/Debian: sudo apt-get install postgresql-client\n\
586 - macOS: brew install postgresql\n\
587 - RHEL/CentOS: sudo yum install postgresql",
588 )
589 },
590 3, Duration::from_secs(1), "pg_dump (dump data)",
593 )
594 .await
595 .with_context(|| {
596 format!(
597 "pg_dump failed to dump data for database '{}'.\n\
598 \n\
599 Common causes:\n\
600 - Database does not exist\n\
601 - Connection authentication failed\n\
602 - User lacks privileges to read table data\n\
603 - Network connectivity issues\n\
604 - Insufficient disk space for dump directory\n\
605 - Output directory already exists (pg_dump requires non-existent path)\n\
606 - Connection timeout or network issues",
607 database
608 )
609 })?;
610
611 tracing::info!(
612 "✓ Data dumped successfully using {} parallel jobs",
613 num_cpus
614 );
615 Ok(())
616}
617
618fn get_schema_excluded_tables_for_db(
623 filter: &ReplicationFilter,
624 db_name: &str,
625) -> Option<Vec<String>> {
626 let mut tables = BTreeSet::new();
627
628 if let Some(explicit) = filter.exclude_tables() {
631 for full_name in explicit {
632 let parts: Vec<&str> = full_name.split('.').collect();
633 if parts.len() == 2 && parts[0] == db_name {
634 tables.insert(format!("\"public\".\"{}\"", parts[1]));
636 }
637 }
638 }
639
640 if tables.is_empty() {
641 None
642 } else {
643 Some(tables.into_iter().collect())
644 }
645}
646
647fn get_data_excluded_tables_for_db(
652 filter: &ReplicationFilter,
653 db_name: &str,
654) -> Option<Vec<String>> {
655 let mut tables = BTreeSet::new();
656
657 if let Some(explicit) = filter.exclude_tables() {
660 for full_name in explicit {
661 let parts: Vec<&str> = full_name.split('.').collect();
662 if parts.len() == 2 && parts[0] == db_name {
663 tables.insert(format!("\"public\".\"{}\"", parts[1]));
665 }
666 }
667 }
668
669 for table in filter.schema_only_tables(db_name) {
671 tables.insert(table);
672 }
673
674 for (table, _) in filter.predicate_tables(db_name) {
675 tables.insert(table);
676 }
677
678 if tables.is_empty() {
679 None
680 } else {
681 Some(tables.into_iter().collect())
682 }
683}
684
685fn get_included_tables_for_db(filter: &ReplicationFilter, db_name: &str) -> Option<Vec<String>> {
688 filter.include_tables().map(|tables| {
689 tables
690 .iter()
691 .filter_map(|full_name| {
692 let parts: Vec<&str> = full_name.split('.').collect();
693 if parts.len() == 2 && parts[0] == db_name {
694 Some(format!("\"public\".\"{}\"", parts[1]))
696 } else {
697 None
698 }
699 })
700 .collect()
701 })
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use tempfile::tempdir;
708
709 #[tokio::test]
710 #[ignore]
711 async fn test_dump_globals() {
712 let url = std::env::var("TEST_SOURCE_URL").unwrap();
713 let dir = tempdir().unwrap();
714 let output = dir.path().join("globals.sql");
715
716 let result = dump_globals(&url, output.to_str().unwrap()).await;
717
718 assert!(result.is_ok());
719 assert!(output.exists());
720
721 let content = std::fs::read_to_string(&output).unwrap();
723 assert!(content.contains("CREATE ROLE") || !content.is_empty());
724 }
725
726 #[tokio::test]
727 #[ignore]
728 async fn test_dump_schema() {
729 let url = std::env::var("TEST_SOURCE_URL").unwrap();
730 let dir = tempdir().unwrap();
731 let output = dir.path().join("schema.sql");
732
733 let db = url.split('/').next_back().unwrap_or("postgres");
735
736 let filter = crate::filters::ReplicationFilter::empty();
737 let result = dump_schema(&url, db, output.to_str().unwrap(), &filter).await;
738
739 assert!(result.is_ok());
740 assert!(output.exists());
741 }
742
743 #[test]
744 fn test_get_schema_excluded_tables_for_db() {
745 let filter = crate::filters::ReplicationFilter::new(
746 None,
747 None,
748 None,
749 Some(vec![
750 "db1.table1".to_string(),
751 "db1.table2".to_string(),
752 "db2.table3".to_string(),
753 ]),
754 )
755 .unwrap();
756
757 let tables = get_schema_excluded_tables_for_db(&filter, "db1").unwrap();
759 assert_eq!(
761 tables,
762 vec!["\"public\".\"table1\"", "\"public\".\"table2\""]
763 );
764
765 let tables = get_schema_excluded_tables_for_db(&filter, "db2").unwrap();
766 assert_eq!(tables, vec!["\"public\".\"table3\""]);
767
768 let tables = get_schema_excluded_tables_for_db(&filter, "db3");
769 assert!(tables.is_none() || tables.unwrap().is_empty());
770 }
771
772 #[test]
773 fn test_get_data_excluded_tables_for_db() {
774 let filter = crate::filters::ReplicationFilter::new(
775 None,
776 None,
777 None,
778 Some(vec![
779 "db1.table1".to_string(),
780 "db1.table2".to_string(),
781 "db2.table3".to_string(),
782 ]),
783 )
784 .unwrap();
785
786 let tables = get_data_excluded_tables_for_db(&filter, "db1").unwrap();
788 assert_eq!(
790 tables,
791 vec!["\"public\".\"table1\"", "\"public\".\"table2\""]
792 );
793
794 let tables = get_data_excluded_tables_for_db(&filter, "db2").unwrap();
795 assert_eq!(tables, vec!["\"public\".\"table3\""]);
796
797 let tables = get_data_excluded_tables_for_db(&filter, "db3");
798 assert!(tables.is_none() || tables.unwrap().is_empty());
799 }
800
801 #[test]
802 fn test_get_included_tables_for_db() {
803 let filter = crate::filters::ReplicationFilter::new(
804 None,
805 None,
806 Some(vec![
807 "db1.users".to_string(),
808 "db1.orders".to_string(),
809 "db2.products".to_string(),
810 ]),
811 None,
812 )
813 .unwrap();
814
815 let tables = get_included_tables_for_db(&filter, "db1").unwrap();
816 assert_eq!(
818 tables,
819 vec!["\"public\".\"users\"", "\"public\".\"orders\""]
820 );
821
822 let tables = get_included_tables_for_db(&filter, "db2").unwrap();
823 assert_eq!(tables, vec!["\"public\".\"products\""]);
824
825 let tables = get_included_tables_for_db(&filter, "db3");
826 assert!(tables.is_none() || tables.unwrap().is_empty());
827 }
828
829 #[test]
830 fn test_get_schema_excluded_tables_for_db_with_empty_filter() {
831 let filter = crate::filters::ReplicationFilter::empty();
832 let tables = get_schema_excluded_tables_for_db(&filter, "db1");
833 assert!(tables.is_none());
834 }
835
836 #[test]
837 fn test_get_data_excluded_tables_for_db_with_empty_filter() {
838 let filter = crate::filters::ReplicationFilter::empty();
839 let tables = get_data_excluded_tables_for_db(&filter, "db1");
840 assert!(tables.is_none());
841 }
842
843 #[test]
844 fn test_get_included_tables_for_db_with_empty_filter() {
845 let filter = crate::filters::ReplicationFilter::empty();
846 let tables = get_included_tables_for_db(&filter, "db1");
847 assert!(tables.is_none());
848 }
849
850 #[test]
851 fn test_rewrite_create_role_statements_wraps_unquoted_role() {
852 let sql = "CREATE ROLE replicator WITH LOGIN;\nALTER ROLE replicator WITH LOGIN;\n";
853 let rewritten = rewrite_create_role_statements(sql).expect("rewrite expected");
854
855 assert!(rewritten.contains("DO $$"));
856 assert!(rewritten.contains("Role replicator already exists"));
857 assert!(rewritten.contains("CREATE ROLE replicator WITH LOGIN;"));
858 assert!(rewritten.contains("ALTER ROLE replicator WITH LOGIN;"));
859 }
860
861 #[test]
862 fn test_rewrite_create_role_statements_wraps_quoted_role() {
863 let sql = " CREATE ROLE \"Andre Admin\";\n";
864 let rewritten = rewrite_create_role_statements(sql).expect("rewrite expected");
865
866 assert!(rewritten.contains("DO $$"));
867 assert!(rewritten.contains("Andre Admin already exists"));
868 assert!(rewritten.contains("CREATE ROLE \"Andre Admin\""));
869 assert!(rewritten.starts_with(" DO $$"));
870 }
871
872 #[test]
873 fn test_rewrite_create_role_statements_noop_when_absent() {
874 let sql = "ALTER ROLE existing WITH LOGIN;\n";
875 assert!(rewrite_create_role_statements(sql).is_none());
876 }
877
878 #[test]
879 fn test_remove_restricted_role_grants() {
880 let dir = tempdir().unwrap();
881 let globals_file = dir.path().join("globals.sql");
882
883 let content = r#"CREATE ROLE myuser;
885ALTER ROLE myuser WITH LOGIN;
886GRANT pg_checkpoint TO myuser;
887GRANT "pg_read_all_stats" TO myuser;
888GRANT pg_monitor TO myuser;
889GRANT myrole TO myuser;
890GRANT SELECT ON TABLE users TO myuser;
891"#;
892 std::fs::write(&globals_file, content).unwrap();
893
894 remove_restricted_role_grants(globals_file.to_str().unwrap()).unwrap();
896
897 let result = std::fs::read_to_string(&globals_file).unwrap();
899
900 assert!(result.contains("-- GRANT pg_checkpoint TO myuser;"));
902 assert!(result.contains("-- GRANT \"pg_read_all_stats\" TO myuser;"));
903 assert!(result.contains("-- GRANT pg_monitor TO myuser;"));
904
905 assert!(result.contains("\nGRANT myrole TO myuser;\n"));
907 assert!(result.contains("\nGRANT SELECT ON TABLE users TO myuser;\n"));
908
909 assert!(result.contains("CREATE ROLE myuser;"));
911 assert!(result.contains("ALTER ROLE myuser WITH LOGIN;"));
912 }
913}