From 701fbc017ef382fc7ac5b2b6ae60c765f86b21b9 Mon Sep 17 00:00:00 2001 From: aliziel <21992503+aliziel@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:17:51 -0700 Subject: [PATCH 1/4] feat: make rds_deletion_protection configurable for certain db maintenance procedures --- features_api_database/infrastructure/config.py | 4 ++++ features_api_database/infrastructure/construct.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/features_api_database/infrastructure/config.py b/features_api_database/infrastructure/config.py index 765eb6b..10a91a1 100644 --- a/features_api_database/infrastructure/config.py +++ b/features_api_database/infrastructure/config.py @@ -99,6 +99,10 @@ class FeaturesDBSettings(BaseSettings): False, description="Boolean if the RDS should be encrypted", ) + rds_deletion_protection: Optional[bool] = Field( + True, + description="Config to temporarily disable deletion protection for certain maintenance operations. Be sure to reenable after work is complete.", + ) max_allocated_storage: Optional[int] = Field( 500, description="Upper limit to which RDS can scale the storage in GiB(Gibibyte)", diff --git a/features_api_database/infrastructure/construct.py b/features_api_database/infrastructure/construct.py index 0b92c18..a76db7d 100644 --- a/features_api_database/infrastructure/construct.py +++ b/features_api_database/infrastructure/construct.py @@ -180,7 +180,7 @@ def __init__( "engine": engine, "instance_type": rds_instance_type, "vpc_subnets": self.vpc_subnets, - "deletion_protection": True, + "deletion_protection": features_db_settings.rds_deletion_protection, "removal_policy": RemovalPolicy.RETAIN, "publicly_accessible": features_db_settings.publicly_accessible, "parameter_group": parameter_group, From a60a1841532a22a35c11908837d1b7159a28c47d Mon Sep 17 00:00:00 2001 From: aliziel <21992503+aliziel@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:20:43 -0700 Subject: [PATCH 2/4] fix: replace identifiers (double quotes) on non-identifier values (https://www.psycopg.org/psycopg3/docs/api/sql.html#psycopg.sql.SQL.format) --- features_api_database/runtime/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features_api_database/runtime/handler.py b/features_api_database/runtime/handler.py index a3e6aa1..c4a61aa 100644 --- a/features_api_database/runtime/handler.py +++ b/features_api_database/runtime/handler.py @@ -128,7 +128,7 @@ def create_user(cursor, username: str, password: str) -> None: " END IF; " "END " "$$; " - ).format(username=sql.Identifier(username), password=sql.Identifier(password), user=sql.Identifier(username)) + ).format(username=sql.Identifier(username), password=password, user=username) ) print(f"DEBUG: SQL executed successfully") From 311be8978f43a56a2cbe82d745b3777293a6ec65 Mon Sep 17 00:00:00 2001 From: aliziel <21992503+aliziel@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:24:38 -0700 Subject: [PATCH 3/4] fix: condense mismatching user exists queries, use one in control flow outside of db for create/alter if exists for easier issue identification + maintenance --- features_api_database/runtime/handler.py | 35 ++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/features_api_database/runtime/handler.py b/features_api_database/runtime/handler.py index c4a61aa..a36dcec 100644 --- a/features_api_database/runtime/handler.py +++ b/features_api_database/runtime/handler.py @@ -107,29 +107,24 @@ def create_user(cursor, username: str, password: str) -> None: try: # Check if user exists before - cursor.execute("SELECT rolname FROM pg_roles WHERE rolname = %s", (username,)) - exists_before = cursor.fetchone() is not None + cursor.execute("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s)", (username,)) + exists_before = cursor.fetchone()[0] print(f"DEBUG: User '{username}' exists before: {exists_before}") # Create/update user - cursor.execute( - sql.SQL( - "DO $$ " - "BEGIN " - " IF NOT EXISTS ( " - " SELECT 1 FROM pg_roles " - " WHERE rolname = {user}) " - " THEN " - " CREATE USER {username} " - " WITH PASSWORD {password}; " - " ELSE " - " ALTER USER {username} " - " WITH PASSWORD {password}; " - " END IF; " - "END " - "$$; " - ).format(username=sql.Identifier(username), password=password, user=username) - ) + if exists_before: + cursor.execute( + sql.SQL( + "ALTER USER {username} WITH PASSWORD {password};" + ).format(username=sql.Identifier(username), password=password) + ) + else: + cursor.execute( + sql.SQL( + "CREATE USER {username} WITH PASSWORD {password};" + ).format(username=sql.Identifier(username), password=password) + ) + print(f"DEBUG: SQL executed successfully") # Check if user exists after From 00adcc4771265015a389163030aab15157a5c193 Mon Sep 17 00:00:00 2001 From: aliziel <21992503+aliziel@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:59:30 -0700 Subject: [PATCH 4/4] fix: allow rollback on failed create --- features_api_database/infrastructure/construct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features_api_database/infrastructure/construct.py b/features_api_database/infrastructure/construct.py index a76db7d..258b7b7 100644 --- a/features_api_database/infrastructure/construct.py +++ b/features_api_database/infrastructure/construct.py @@ -181,7 +181,7 @@ def __init__( "instance_type": rds_instance_type, "vpc_subnets": self.vpc_subnets, "deletion_protection": features_db_settings.rds_deletion_protection, - "removal_policy": RemovalPolicy.RETAIN, + "removal_policy": RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, "publicly_accessible": features_db_settings.publicly_accessible, "parameter_group": parameter_group, }