A Python utility for flattening complex nested data structures generated by the Connect for Cancer Prevention study questionnaires.
The flattener converts Connect study data - stored as nested BigQuery tables - into flat, analyst-friendly tables. It exports source tables to Parquet in GCS, uses DuckDB to flatten nested structures, and loads the results back into BigQuery. It also generates a user profile history table directly in BigQuery.
The tool is deployed as a Cloud Run service exposing a Flask API. The pipeline is orchestrated as an Airflow DAG in the flattener-orchestrator repository.
The Airflow DAG calls the following endpoints in order:
GET /heartbeat-- verify the service is healthyPOST /refresh_firestore-- (optional) trigger a Firestore backupPOST /table_to_parquet-- export a BigQuery table to Parquet in GCSPOST /convert_parquet-- decode BLOB columns to STRUCT (boxes table only)POST /flatten_parquet-- flatten nested Parquet into a wide Parquet file via DuckDBPOST /parquet_to_table-- load the flattened Parquet back into BigQueryPOST /generate_profile_history-- generate the user profile history table in BigQuery
Steps 3-6 run in parallel across all selected tables. Step 7 runs once after all tables are loaded.
The tool reads the schema of a Parquet file and recursively traverses it to identify all leaf fields. For each field, it:
- Builds the correct hierarchical path (e.g.,
"parent"."child"."grandchild") - Creates an alias using underscores (e.g.,
parent_child_grandchild) - Casts the value to STRING in the generated SELECT expression
Some fields use a Firestore value wrapper pattern: a STRUCT with string, integer, and provided sub-fields. These are collapsed into a single STRING column using a CASE expression that checks the provided field.
For fields of type VARCHAR[], the tool:
- Queries the data for all distinct integer-castable values in the array
- For each value, creates a binary indicator column (
1if present,NULLif not) - Names these columns using the pattern
original_field_D_value
For example, an array field containing values ["104430631", "353358909"] would expand to:
original_field_D_104430631 = 1original_field_D_353358909 = 1
The boxes table stores specimen bag data in BLOB columns that are first converted to STRUCTs, then flattened into a UNION ALL of unnested tube rows with parent-level fields projected alongside.
The d_569151507 field on the participants table is an array of profile change deltas. The /generate_profile_history endpoint reads directly from BigQuery, unnests this array, and applies carry-forward logic using LAST_VALUE(IGNORE NULLS) window functions to produce a complete historical snapshot table.
- Nested fields:
parent_child_grandchild - Expanded array values:
parent_array_field_D_value - Firestore wrappers: the
string/integer/providedsuffixes are stripped
The service is deployed via Cloud Build (cloudbuild.yaml) as a Cloud Run service with a GCS volume mount for DuckDB temporary files.