Skip to content

Commit 7079301

Browse files
committed
Enhance DumpCleaner and import_to_supabase functions with target schema support, improve logging, and refine error handling
1 parent edf0a9d commit 7079301

File tree

4 files changed

+117
-69
lines changed

4 files changed

+117
-69
lines changed

cloudsql_to_supabase/clean.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,43 @@
77
logger = logging.getLogger('cloudsql_to_supabase.clean')
88

99
class DumpCleaner:
10-
def __init__(self, input_file: Path = None, output_file: Path = None) -> None:
10+
def __init__(self, input_file: Path = None, output_file: Path = None, target_schema: str = None) -> None:
1111
self.input_file = input_file or Path(config.OUTPUT_DUMP)
1212
self.output_file = output_file or Path(config.CLEANED_DUMP)
13+
self.target_schema = target_schema or config.SUPABASE_SCHEMA
14+
1315
self.skip_patterns = [
1416
r'^(CREATE|ALTER) ROLE', # Skip role creation/alteration
1517
r'^COMMENT ON EXTENSION', # Skip extension comments
1618
]
19+
20+
# Regular ownership replacement
21+
owner_replacement = 'OWNER TO public;'
22+
# If using non-public schema and we have permissions, keep the target schema as owner
23+
if self.target_schema != "public":
24+
owner_replacement = f'OWNER TO {self.target_schema};'
25+
1726
self.replacement_rules = [
18-
(r'OWNER TO .*?;', 'OWNER TO public;'), # Change ownership to public
27+
(r'OWNER TO .*?;', owner_replacement),
1928
(r'CREATE SCHEMA .*?;', '-- Schema creation removed'), # Comment out schema creation
2029
]
2130

31+
# If we're targeting a non-public schema, add schema modifications
32+
if self.target_schema != "public":
33+
# Add rules to update schema references in the dump
34+
self.replacement_rules.extend([
35+
# Update SET search_path statements
36+
(r'SET search_path = public', f'SET search_path = {self.target_schema}'),
37+
# Update schema references in table names
38+
(r'CREATE TABLE public\.', f'CREATE TABLE {self.target_schema}.'),
39+
# Update schema references in sequence names
40+
(r'CREATE SEQUENCE public\.', f'CREATE SEQUENCE {self.target_schema}.'),
41+
# Update schema references in views
42+
(r'CREATE VIEW public\.', f'CREATE VIEW {self.target_schema}.'),
43+
# Update schema references in functions
44+
(r'CREATE FUNCTION public\.', f'CREATE FUNCTION {self.target_schema}.'),
45+
])
46+
2247
def clean_dump_file(self) -> Path:
2348
"""
2449
Clean the SQL dump file for Supabase import by removing/modifying
@@ -57,9 +82,14 @@ def clean_dump_file(self) -> Path:
5782
logger.info(f"Cleaned dump saved as {self.output_file}")
5883
return self.output_file
5984

60-
def clean_dump_file(input_file: Optional[Path] = None, output_file: Optional[Path] = None) -> Path:
85+
def clean_dump_file(input_file: Optional[Path] = None, output_file: Optional[Path] = None, target_schema: Optional[str] = None) -> Path:
6186
"""
6287
Convenience function to clean a SQL dump file
88+
89+
Args:
90+
input_file: Path to the input SQL dump file
91+
output_file: Path to the output cleaned SQL file
92+
target_schema: Schema to use in Supabase. If None, uses the one from config
6393
"""
64-
cleaner = DumpCleaner(input_file, output_file)
65-
return cleaner.clean_dump_file()
94+
cleaner = DumpCleaner(input_file, output_file, target_schema)
95+
return cleaner.clean_dump_file()

cloudsql_to_supabase/config.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,54 @@
1-
from dotenv import load_dotenv
21
import os
32
from pathlib import Path
4-
import logging
5-
6-
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
7-
3+
from dotenv import load_dotenv
4+
import logging
85

6+
# Configure logging
7+
logging.basicConfig(
8+
level=logging.INFO,
9+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
10+
)
911
logger = logging.getLogger('cloudsql_to_supabase')
1012

13+
# Load environment variables
1114
load_dotenv()
1215

13-
16+
# Database configurations
1417
CLOUDSQL_USER = os.getenv("CLOUDSQL_USER")
1518
CLOUDSQL_HOST = os.getenv("CLOUDSQL_HOST")
1619
CLOUDSQL_DB = os.getenv("CLOUDSQL_DB")
1720
CLOUDSQL_PORT = int(os.getenv("CLOUDSQL_PORT", 5432))
1821
CLOUDSQL_SSL_MODE = os.getenv("CLOUDSQL_SSL_MODE", "prefer")
22+
CLOUDSQL_SCHEMA = os.getenv("CLOUDSQL_SCHEMA", "public")
1923

2024
SUPABASE_USER = os.getenv("SUPABASE_USER", "postgres")
2125
SUPABASE_HOST = os.getenv("SUPABASE_HOST")
2226
SUPABASE_DB = os.getenv("SUPABASE_DB", "postgres")
2327
SUPABASE_PASSWORD = os.getenv("SUPABASE_PASSWORD")
2428
SUPABASE_PORT = int(os.getenv("SUPABASE_PORT", 5432))
2529
SUPABASE_SSL_MODE = os.getenv("SUPABASE_SSL_MODE", "require")
30+
SUPABASE_SCHEMA = os.getenv("SUPABASE_SCHEMA", "public")
2631

27-
OUTPUT_DIR = Path(os.getenv('OUTPUT_DIR', '.'))
32+
# Output file paths
33+
OUTPUT_DIR = Path(os.getenv("OUTPUT_DIR", "."))
2834
OUTPUT_DUMP = OUTPUT_DIR / os.getenv("OUTPUT_DUMP", "backup.sql")
2935
CLEANED_DUMP = OUTPUT_DIR / os.getenv("CLEANED_DUMP", "cleaned_backup.sql")
3036

31-
37+
# Validation function
3238
def validate_config():
33-
"""
34-
Validate the configuration values.
35-
"""
39+
"""Validate that the required environment variables are set."""
3640
required_vars = {
37-
"CLOUDSQL_USER": CLOUDSQL_USER,
41+
"CLOUDSQL_USER": CLOUDSQL_USER,
3842
"CLOUDSQL_HOST": CLOUDSQL_HOST,
3943
"CLOUDSQL_DB": CLOUDSQL_DB,
4044
"SUPABASE_HOST": SUPABASE_HOST,
4145
"SUPABASE_PASSWORD": SUPABASE_PASSWORD,
4246
}
4347

44-
4548
missing = [var for var, value in required_vars.items() if not value]
4649

4750
if missing:
4851
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
4952

50-
OUTPUT_DIR.mkdir(exist_ok=True)
53+
# Create output directory if it doesn't exist
54+
OUTPUT_DIR.mkdir(exist_ok=True)

cloudsql_to_supabase/import_.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,65 @@
44
from typing import Optional
55
from . import config, utils
66

7-
8-
97
logger = logging.getLogger('cloudsql_to_supabase.import')
108

11-
12-
13-
def import_to_supabase(input_file: Optional[Path] = None, password: Optional[str] = None) -> None:
9+
def import_to_supabase(input_file: Optional[Path] = None, password: Optional[str] = None, schema: Optional[str] = None) -> None:
1410
"""
15-
import cleaned sql dump into supabase
16-
17-
Args:
18-
input_file: Path to the sql dump file to import. if None, uses the default.
19-
password: supabase database password. if none uses from config
20-
11+
Import a cleaned SQL dump file into Supabase
12+
13+
Args:
14+
input_file: Path to the SQL dump file to import. If None, uses the default.
15+
password: Supabase database password. If None, uses from config.
16+
schema: Target schema to import into. If None, uses the one from config.
2117
"""
2218
config.validate_config()
2319
dump_file = input_file or Path(config.CLEANED_DUMP)
20+
target_schema = schema or config.SUPABASE_SCHEMA
21+
2422
if not dump_file.exists():
25-
raise FileNotFoundError(f"Dump not found: {dump_file}")
23+
raise FileNotFoundError(f"Dump file not found: {dump_file}")
2624

27-
logger.info(f'importing data into supabase database: {config.SUPABASE_DB}')
25+
logger.info(f"Importing into Supabase database: {config.SUPABASE_DB}, schema: {target_schema}")
2826

27+
# Set up environment with password
2928
env = os.environ.copy()
3029
env['PGPASSWORD'] = password or config.SUPABASE_PASSWORD
3130

31+
# Create schema if it doesn't exist (and it's not 'public')
32+
if target_schema != "public":
33+
create_schema_cmd = (
34+
f"psql -h {config.SUPABASE_HOST} "
35+
f"-p {config.SUPABASE_PORT} "
36+
f"-U {config.SUPABASE_USER} "
37+
f"-d {config.SUPABASE_DB} "
38+
f"--sslmode={config.SUPABASE_SSL_MODE} "
39+
f"-c \"CREATE SCHEMA IF NOT EXISTS {target_schema};\""
40+
)
41+
try:
42+
logger.info(f"Creating schema if it doesn't exist: {target_schema}")
43+
utils.run_command(create_schema_cmd, env)
44+
except Exception as e:
45+
logger.warning(f"Failed to create schema, it may already exist: {e}")
3246

47+
# Build psql command
3348
cmd = (
3449
f"psql -h {config.SUPABASE_HOST} "
3550
f"-p {config.SUPABASE_PORT} "
3651
f"-U {config.SUPABASE_USER} "
3752
f"-d {config.SUPABASE_DB} "
3853
f"--set ON_ERROR_STOP=on " # Stop on first error
3954
f"--single-transaction " # Run as a single transaction
40-
f"-f {dump_file}"
4155
)
4256

57+
# Set search_path if using non-public schema
58+
if target_schema != "public":
59+
cmd += f"--set search_path={target_schema} "
60+
61+
cmd += f"--sslmode={config.SUPABASE_SSL_MODE} -f {dump_file}"
4362

4463
try:
4564
utils.run_command(cmd, env)
46-
logger.info("import completed successfully")
65+
logger.info("Import completed successfully")
4766
except Exception as e:
48-
logger.error(f'import failed: {e}')
67+
logger.error(f"Import failed: {e}")
4968
raise

cloudsql_to_supabase/utils.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
from typing import Dict, Optional, List, Union
44
import logging
55

6+
logger = logging.getLogger('cloudsql_to_supabase.utils')
67

7-
8-
logger = logging.getLogger('cloud_to_supabase.utils')
9-
10-
11-
def run_command(cmd: str, env: Optional[Dict] = None, show_output: bool = True)-> subprocess.CompletedProcess:
8+
def run_command(cmd: str, env: Optional[Dict] = None, show_output: bool = True) -> subprocess.CompletedProcess:
129
"""
13-
Run a shell command safely with proper logging and error handling.
10+
Run a shell command safely with proper logging and error handling.
1411
1512
Args:
1613
cmd: Command to run
@@ -20,36 +17,34 @@ def run_command(cmd: str, env: Optional[Dict] = None, show_output: bool = True)-
2017
Returns:
2118
CompletedProcess instance
2219
"""
23-
20+
# Log the command (with password redacted if present)
2421
safe_cmd = cmd
2522
if "PGPASSWORD" in str(env):
26-
safe_cmd = cmd.replace(env.get('PGPASSWORD', ''), '************')
27-
logger.info(f'Running command: {safe_cmd}')
23+
safe_cmd = cmd.replace(env.get('PGPASSWORD', ''), '********')
24+
25+
logger.info(f"Running command: {safe_cmd}")
26+
27+
try:
28+
# Use shlex.split for safer command execution
29+
args = shlex.split(cmd)
30+
result = subprocess.run(
31+
args,
32+
env=env,
33+
stdout=subprocess.PIPE,
34+
stderr=subprocess.PIPE,
35+
text=True,
36+
check=False # We'll handle errors ourselves
37+
)
2838

39+
if result.returncode != 0:
40+
logger.error(f"Command failed with exit code {result.returncode}")
41+
logger.error(result.stderr)
42+
raise RuntimeError(f"Command failed: {result.stderr}")
2943

30-
try:
31-
args = shlex.split(cmd)
32-
result = subprocess.run(
33-
args,
34-
env=env,
35-
stdout=subprocess.PIPE,
36-
stderr=subprocess.PIPE,
37-
text=True,
38-
check=False
39-
)
40-
41-
if result.returncode != 0:
42-
logger.error(f'command failed with exit code {result.returncode}')
43-
logger.error(result.stderr)
44-
raise RuntimeError(f'command failed: {result.stderr}')
44+
if show_output and result.stdout:
45+
logger.info(result.stdout)
4546

46-
47-
if show_output and result.stdout:
48-
logger.info(result.stdout)
49-
50-
51-
return result
52-
53-
except Exception as e:
54-
logger.exception(f'Error executing command: {e}')
55-
raise
47+
return result
48+
except Exception as e:
49+
logger.exception(f"Error executing command: {e}")
50+
raise

0 commit comments

Comments
 (0)