Source code for operators.postgres.utils.backup_table

"""
Backup Table Operator

This module provides an operator for backing up a Postgres table to a temporary
table within the same schema. Useful for preserving data before destructive
operations like schema syncs.

Usage:
    from operators.postgres.utils.backup_table import BackupTableOperator

    backup_users = BackupTableOperator(
        task_id="backup_users",
        postgres_conn_id="boss_grapi_stage",
        tenant="arkansas",
        source_table="User",
        backup_suffix="_backup",
    )
"""

from __future__ import annotations

from typing import Sequence

from airflow.models import BaseOperator
from airflow.providers.postgres.hooks.postgres import PostgresHook
from airflow.utils.context import Context


################################################################################
## Constants
################################################################################

DEFAULT_BACKUP_SUFFIX = "_backup"
STAGE_CONN_PATTERN = "stage"

SQL_DROP_BACKUP = """
DROP TABLE IF EXISTS {tenant}."{table}{suffix}";
"""

SQL_CREATE_BACKUP = """
CREATE TABLE {tenant}."{table}{suffix}" AS
SELECT * FROM {tenant}."{table}";
"""

SQL_CREATE_BACKUP_CAST = """
CREATE TABLE {tenant}."{table}{suffix}" AS
SELECT {columns} FROM {tenant}."{table}";
"""

SQL_GET_ENUM_COLUMNS = """
SELECT c.column_name
FROM information_schema.columns AS c
JOIN pg_type AS t ON t.typname = c.udt_name
WHERE c.table_schema = '{tenant}'
    AND c.table_name = '{table}'
    AND t.typtype = 'e';
"""


################################################################################
## Operator
################################################################################


[docs] class BackupTableOperator(BaseOperator): """ Backup a Postgres table to a temporary table in the same schema. Creates a copy of the source table with a suffix appended to the name. Drops any existing backup table with the same name first. :param postgres_conn_id: Airflow connection ID for the Postgres database. :param tenant: The tenant schema name. :param source_table: The table name to backup. :param backup_suffix: Suffix to append to backup table name (default: "_backup"). :param cast_enums_to_text: If True, cast ENUM columns to TEXT in the backup to avoid dependencies on ENUM types (default: False). """ ui_color = "#BADA55" template_fields: Sequence[str] = ("postgres_conn_id", "tenant", "source_table") def __init__( self, *, postgres_conn_id: str, tenant: str, source_table: str, backup_suffix: str = DEFAULT_BACKUP_SUFFIX, cast_enums_to_text: bool = False, **kwargs, ): super().__init__(**kwargs) self.postgres_conn_id = postgres_conn_id self.tenant = tenant self.source_table = source_table self.backup_suffix = backup_suffix self.cast_enums_to_text = cast_enums_to_text ############################################################################ ## Execute ############################################################################
[docs] def execute(self, context: Context) -> str: if STAGE_CONN_PATTERN not in self.postgres_conn_id.lower(): raise ValueError( f"Safety check failed: '{self.postgres_conn_id}' does not appear to be a stage connection. " "This operator should only modify stage databases." ) hook = PostgresHook(postgres_conn_id=self.postgres_conn_id) backup_table = f"{self.source_table}{self.backup_suffix}" drop_sql = SQL_DROP_BACKUP.format( tenant=self.tenant, table=self.source_table, suffix=self.backup_suffix, ) if self.cast_enums_to_text: enum_sql = SQL_GET_ENUM_COLUMNS.format( tenant=self.tenant, table=self.source_table, ) enum_cols = { row[0] for row in hook.get_records(enum_sql) } self.log.info("Enum columns to cast as TEXT: %s", enum_cols) all_cols_sql = ( f"SELECT column_name FROM information_schema.columns " f"WHERE table_schema = '{self.tenant}' " f"AND table_name = '{self.source_table}' " f"ORDER BY ordinal_position" ) all_cols = [row[0] for row in hook.get_records(all_cols_sql)] columns = ", ".join( f'"{c}"::TEXT AS "{c}"' if c in enum_cols else f'"{c}"' for c in all_cols ) create_sql = SQL_CREATE_BACKUP_CAST.format( tenant=self.tenant, table=self.source_table, suffix=self.backup_suffix, columns=columns, ) else: create_sql = SQL_CREATE_BACKUP.format( tenant=self.tenant, table=self.source_table, suffix=self.backup_suffix, ) self.log.info( "Backing up %s.%s to %s.%s", self.tenant, self.source_table, self.tenant, backup_table, ) hook.run(drop_sql) hook.run(create_sql) self.log.info("Backup complete: %s.%s", self.tenant, backup_table) return backup_table