Created
September 24, 2026 14:31
-
-
Save philerooski/bda969f7d9bcce656427deebc87e930a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Incrementally backfills COPY_FILES_TASK / COPY_CHANGES_TASK / COPY_SENT_MESSAGES_TASK's | |
| RDS_SNAPSHOTS_STAGE backlog one snapshot_date at a time (SNOW-580). | |
| These tasks time out trying to COPY INTO their entire backlog (150K+ files, | |
| 1TB+) in a single statement, even on COMPUTE_MEDIUM. This script drains the | |
| backlog day by day instead: each COPY INTO is scoped to one snapshot_date via | |
| the PATTERN clause, which keeps each run's file count/volume small enough to | |
| finish well inside the warehouse statement timeout. It's safe to re-run -- | |
| COPY INTO skips files it has already loaded -- so a partial or interrupted run | |
| can just be re-invoked to pick up where it left off. | |
| Shells out to the `snow` CLI (see AGENTS.md) rather than holding credentials | |
| directly. | |
| Usage: | |
| uv run --group snowflake python analytics/backfill_rds_snapshot_copy_tasks.py \\ | |
| --target files --warehouse compute_medium --max-dates 5 | |
| """ | |
| import argparse | |
| import json | |
| import subprocess | |
| import sys | |
| import time | |
| DATABASE = "SYNAPSE_DATA_WAREHOUSE" | |
| SCHEMA = "RDS_LANDING" | |
| STAGE = "RDS_SNAPSHOTS_STAGE" | |
| TARGETS = { | |
| "files": { | |
| "table": "files", | |
| "stage_subpath": "rds-snapshot/FILES/", | |
| "select": """ | |
| SELECT | |
| $1:ID::BIGINT AS id, | |
| $1:ETAG::VARCHAR AS etag, | |
| $1:PREVIEW_ID::BIGINT AS preview_id, | |
| $1:CREATED_ON::TIMESTAMP_NTZ(9) AS created_on, | |
| $1:CREATED_BY::BIGINT AS created_by, | |
| $1:UPDATED_ON::TIMESTAMP_NTZ(9) AS updated_on, | |
| $1:METADATA_TYPE::VARCHAR AS metadata_type, | |
| $1:CONTENT_TYPE::VARCHAR AS content_type, | |
| $1:CONTENT_SIZE::BIGINT AS content_size, | |
| $1:CONTENT_MD5::VARCHAR AS content_md5, | |
| $1:BUCKET_NAME::VARCHAR AS bucket_name, | |
| $1:NAME::VARCHAR AS name, | |
| $1:KEY::VARCHAR AS key, | |
| $1:STORAGE_LOCATION_ID::BIGINT AS storage_location_id, | |
| $1:ENDPOINT::VARCHAR AS endpoint, | |
| ($1:IS_PREVIEW::NUMBER != 0) AS is_preview, | |
| $1:STATUS::VARCHAR AS status, | |
| SPLIT_PART(METADATA$FILENAME, '/', 3)::BIGINT AS stack, | |
| SPLIT_PART(METADATA$FILENAME, '/', 4)::DATE AS snapshot_date, | |
| METADATA$FILENAME AS filename | |
| FROM @{stage}/rds-snapshot/FILES/ | |
| """, | |
| "file_format": None, | |
| }, | |
| "changes": { | |
| "table": "changes", | |
| "stage_subpath": "rds-snapshot/CHANGES/", | |
| "select": """ | |
| SELECT | |
| $1:CHANGE_NUM::BIGINT AS change_num, | |
| $1:TIME_STAMP::TIMESTAMP_NTZ(9) AS time_stamp, | |
| $1:OBJECT_ID::BIGINT AS object_id, | |
| $1:OBJECT_VERSION::BIGINT AS object_version, | |
| $1:USER_ID::BIGINT AS user_id, | |
| $1:OBJECT_TYPE::VARCHAR AS object_type, | |
| $1:CHANGE_TYPE::VARCHAR AS change_type, | |
| SPLIT_PART(METADATA$FILENAME, '/', 3)::BIGINT AS stack, | |
| SPLIT_PART(METADATA$FILENAME, '/', 4)::DATE AS snapshot_date, | |
| METADATA$FILENAME AS filename | |
| FROM @{stage}/rds-snapshot/CHANGES/ | |
| """, | |
| "file_format": None, | |
| }, | |
| "sent_messages": { | |
| "table": "sent_messages", | |
| "stage_subpath": "rds-snapshot/SENT_MESSAGES/", | |
| "select": """ | |
| SELECT | |
| $1:CHANGE_NUM::BIGINT AS change_num, | |
| $1:TIME_STAMP::TIMESTAMP_NTZ(9) AS time_stamp, | |
| $1:OBJECT_ID::BIGINT AS object_id, | |
| $1:OBJECT_VERSION::BIGINT AS object_version, | |
| $1:OBJECT_TYPE::VARCHAR AS object_type, | |
| SPLIT_PART(METADATA$FILENAME, '/', 3)::BIGINT AS stack, | |
| SPLIT_PART(METADATA$FILENAME, '/', 4)::DATE AS snapshot_date, | |
| METADATA$FILENAME AS filename | |
| FROM @{stage}/rds-snapshot/SENT_MESSAGES/ | |
| """, | |
| "file_format": None, | |
| }, | |
| } | |
| def run_snow_sql(sql, connection, role): | |
| cmd = ["snow", "sql", "-q", sql, "--format=JSON"] | |
| if connection: | |
| cmd += ["--connection", connection] | |
| if role: | |
| cmd += ["--role", role] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| raise RuntimeError(f"snow sql failed:\n{result.stderr or result.stdout}") | |
| return json.loads(result.stdout) | |
| def list_backlog_dates(stage_subpath, connection, role): | |
| list_sql = f"LIST @{DATABASE}.{SCHEMA}.{STAGE}/{stage_subpath};" | |
| dates_sql = f""" | |
| {list_sql} | |
| SELECT DISTINCT SPLIT_PART("name", '/', 7) AS snapshot_date | |
| FROM TABLE(RESULT_SCAN(LAST_QUERY_ID())) | |
| ORDER BY snapshot_date; | |
| """ | |
| result = run_snow_sql(dates_sql, connection, role) | |
| dates_result = result[-1] | |
| return [row["SNAPSHOT_DATE"] for row in dates_result] | |
| def copy_one_date(target_name, target, snapshot_date, warehouse, connection, role): | |
| select_sql = target["select"].format(stage=f"{DATABASE}.{SCHEMA}.{STAGE}") | |
| file_format_clause = f"FILE_FORMAT = ({target['file_format']})" if target["file_format"] else "" | |
| copy_sql = f""" | |
| USE WAREHOUSE {warehouse}; | |
| USE SCHEMA {DATABASE}.{SCHEMA}; | |
| COPY INTO {target['table']} FROM ( | |
| {select_sql} | |
| ) | |
| {file_format_clause} | |
| PATTERN = '.*\\/[0-9]+\\/{snapshot_date}\\/.*\\.gz\\.parquet'; | |
| """ | |
| result = run_snow_sql(copy_sql, connection, role) | |
| copy_result = result[-1] | |
| rows_loaded = sum(row.get("rows_loaded", 0) or 0 for row in copy_result) | |
| failed = [row for row in copy_result if row.get("status") not in ("LOADED", "LOAD_SKIPPED")] | |
| return rows_loaded, failed | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--target", choices=sorted(TARGETS), required=True) | |
| parser.add_argument("--warehouse", default="compute_medium") | |
| parser.add_argument("--max-dates", type=int, default=10, help="Safety cap per invocation; re-run to continue.") | |
| parser.add_argument("--start-date", help="Skip dates before this (inclusive), e.g. 2026-06-15.") | |
| parser.add_argument("--connection", help="Named `snow` CLI connection to use.") | |
| parser.add_argument("--role", help="Snowflake role to run as (needs INSERT on the target table).") | |
| parser.add_argument("--dry-run", action="store_true", help="List the dates that would be processed and exit.") | |
| args = parser.parse_args() | |
| target = TARGETS[args.target] | |
| dates = list_backlog_dates(target["stage_subpath"], args.connection, args.role) | |
| if args.start_date: | |
| dates = [d for d in dates if d >= args.start_date] | |
| dates = dates[: args.max_dates] | |
| if not dates: | |
| print("No backlog dates found for this target/start-date.") | |
| return | |
| if args.dry_run: | |
| print(f"Would process {len(dates)} date(s): {dates[0]} .. {dates[-1]}") | |
| return | |
| print(f"Processing {len(dates)} date(s) for '{args.target}' on {args.warehouse}: {dates[0]} .. {dates[-1]}") | |
| for snapshot_date in dates: | |
| start = time.monotonic() | |
| try: | |
| rows_loaded, failed = copy_one_date( | |
| args.target, target, snapshot_date, args.warehouse, args.connection, args.role | |
| ) | |
| except RuntimeError as e: | |
| print(f" {snapshot_date}: ERROR ({time.monotonic() - start:.0f}s) -- {e}", file=sys.stderr) | |
| continue | |
| elapsed = time.monotonic() - start | |
| status = "OK" if not failed else f"{len(failed)} FILE(S) FAILED" | |
| print(f" {snapshot_date}: {status}, {rows_loaded} row(s) loaded ({elapsed:.0f}s)") | |
| for row in failed: | |
| print(f" {row.get('file')}: {row.get('status')} -- {row.get('first_error')}", file=sys.stderr) | |
| print("Done. Re-run with --start-date past the last processed date (or a higher --max-dates) to continue.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment