Source code for bhf_smarthealth.load.sync_db

"""This module contains utility functions for connecting to, and updating an in-process DuckDB database instance"""

import duckdb
import logging
from typing import List, Dict
from os import PathLike


logger = logging.getLogger(__name__)


[docs] def load_target_as_table( conn: duckdb.DuckDBPyConnection, table_name: str, target_parquet_path: PathLike ) -> None: """ Connects to DuckDB using an in-memory database and loads the target Parquet file into a temporary, mutable table with the given name. Parameters ---------- conn : duckdb.DuckDBPyConnection An active DuckDB connection object. table_name : str The name of the in-memory table to create. target_parquet_path : str The path to the target Parquet file to load. """ conn.execute( f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM read_parquet('{target_parquet_path}');" ) logging.info(f"Loaded '{target_parquet_path}' into in-memory table '{table_name}'.")
[docs] def get_upsert_counts( conn: duckdb.DuckDBPyConnection, target_table_name: str, update_parquet_path: PathLike, join_conditions: str, key_column: str, ) -> Dict[str, int]: """ Calculates UPSERT counts (updated and inserted rows) using a single, efficient LEFT JOIN query on the update file and the registered target table. Parameters ---------- conn : duckdb.DuckDBPyConnection An active DuckDB connection object. target_table_name : str The name of the temporary DuckDB table containing the target data (t). update_parquet_path : str The file path of the update Parquet file (u). key_column : str Column name that is used to check matching rows. Returns ------- Dict[str, int] A dictionary containing 'rows_updated', 'rows_inserted', and 'total_incoming' on success, or None if an exception occurs during calculation. """ # Use the first key column from the target table # to check for a match (IS NOT NULL) or non-match (IS NULL). key_col_for_check = f"target.{key_column}" # Combine the match (update) and non-match (insert) counting # into a single LEFT JOIN query. combined_count_query = f""" SELECT COUNT({key_col_for_check}) AS rows_updated, COUNT(*) - COUNT({key_col_for_check}) AS rows_inserted, COUNT(*) AS total_incoming FROM read_parquet('{update_parquet_path}') AS update LEFT JOIN {target_table_name} AS target ON {join_conditions}; """ logging.info("Executing single LEFT JOIN query for UPSERT counts.") result = conn.execute(combined_count_query).fetchone() if result: counts = { "rows_updated": result[0], "rows_inserted": result[1], "total_incoming": result[2], } else: counts = {"rows_updated": 0, "rows_inserted": 0, "total_incoming": 0} logging.info( f"Found {counts['total_incoming']} incoming records in '{update_parquet_path}': " f"{counts['rows_updated']} will be UPDATED, {counts['rows_inserted']} will be INSERTED." ) return counts
[docs] def run_upsert_update( conn: duckdb.DuckDBPyConnection, table_name: str, update_parquet_path: PathLike, join_conditions: str, ) -> None: """ Performs an UPSERT (Update or Insert) operation on a Parquet file using an in-memory DuckDB instance via the efficient MERGE INTO statement. The function loads the TARGET file into an in-memory table, merges the data from the UPDATE file, and then overwrites the original TARGET file with the final, reconciled table state. Parameters ---------- conn : duckdb.DuckDBPyConnection An active DuckDB connection object. table_name: str The name of the temporary in-memory table to update (e.g., "intraday_activities"). update_parquet_path: str The file path to the Parquet file containing the new/update rows. join_conditions: str SQL snippet specifying the columns to match on Returns ------- None The function modifies the TARGET Parquet file in place (via overwrite) and returns no explicit value on successful execution. """ # 4. Execute the MERGE INTO statement for UPSERT logic merge_sql = f""" MERGE INTO {table_name} AS target USING read_parquet('{update_parquet_path}') AS update ON {join_conditions} WHEN MATCHED THEN -- Full row replacement: update all columns (*) UPDATE SET * WHEN NOT MATCHED THEN -- Insert new rows: insert all columns (*) INSERT *; """ logging.info( "Executing MERGE INTO for synchronization operation (Update or Insert)" ) conn.execute(merge_sql) logging.info("UPSERT operation complete.")
[docs] def save_table_to_parquet( conn: duckdb.DuckDBPyConnection, table_name: str, output_path: PathLike ): """Save a specified table to a parquet file Parameters ---------- conn : duckdb.DuckDBPyConnection An active DuckDB connection object. table_name : str The name of the temporary in-memory table to save output_path : PathLike Path to save the table to """ export_sql = f"COPY {table_name} TO '{output_path}' (FORMAT PARQUET, OVERWRITE 1);" conn.execute(export_sql) logging.info(f"Table: {table_name} saved to '{output_path}' successfully.")
[docs] def execute_upsert( table_name: str, target_parquet_path: PathLike, update_parquet_path: PathLike, temp_output_path: PathLike, key_columns: List[str], ): """Overarching function for executing the upsert workflow Parameters ---------- table_name : str The name of the temporary in-memory table to load the target parquet file into target_parquet_path : PathLike Path to the target parquet file update_parquet_path : PathLike Path to the update parquet file temp_output_path : PathLike Path to the temp output parquet file key_columns : List[str] Key columns to join the tables on """ join_conditions = " AND ".join( [f"target.{col} = update.{col}" for col in key_columns] ) config = {"enable_external_access": "True"} with duckdb.connect(database=":memory:", read_only=False, config=config) as conn: try: logging.info( f"Starting Parquet MERGE UPSERT process on target file: '{target_parquet_path} using key columns: {key_columns}'" ) load_target_as_table(conn, table_name, target_parquet_path) get_upsert_counts( conn, table_name, update_parquet_path, join_conditions, key_columns[0] ) run_upsert_update(conn, table_name, update_parquet_path, join_conditions) save_table_to_parquet(conn, table_name, temp_output_path) except duckdb.Error as e: e.add_note(f"DuckDB error occurred whilst executing upsert: {e}") raise except Exception as e: e.add_note( f"An unexpected error occurred whilst executing upsert merge: {e}" ) raise