Skip to content

Commit 887efa6

Browse files
committed
fix: id_registrar replacing the accumulated mapped ids when GCS storage adapter
1 parent b6e27f3 commit 887efa6

6 files changed

Lines changed: 92 additions & 92 deletions

File tree

-69.7 KB
Binary file not shown.
-72.4 KB
Binary file not shown.
54.5 KB
Loading

data_pipeline/contract/contract_executor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def apply_contract(
145145
mapping_dict=ID_COLUMNS_TO_MAP,
146146
runtime_dir=temp_path,
147147
destination=storage_mapping,
148+
run_id=run_context.run_id,
148149
)
149150

150151
output_path = run_context.contracted_path / f"{filename}.parquet"
Lines changed: 64 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# =============================================================================
2-
# UUIDs to Integers Mappings Implementation
2+
# UUIDs to Integers Mappings
33
# =============================================================================
44

55
import polars as pl
66
from pathlib import Path
7-
from data_pipeline.shared.storage_adapter import promote_new_mapping_files
7+
from data_pipeline.shared.storage_adapter import (
8+
promote_new_mapping_files,
9+
check_gcs_path_exists,
10+
)
811

912
ID_COLUMNS_TO_MAP = {
1013
"df_orders": ["order_id", "customer_id"],
@@ -15,29 +18,46 @@
1518
}
1619

1720

18-
def map_uuid_to_int(df: pl.DataFrame, mapping_file_path: Path, id_column: str) -> None:
21+
def map_uuid_to_int(
22+
df: pl.DataFrame,
23+
id_column: str,
24+
storage_destination: str | Path,
25+
local_output_path: Path,
26+
) -> None:
1927
"""
20-
Enforces idempotency and persistence of UUID-to-Integer mappings for a specific column.
21-
22-
Contract (Executor):
23-
- Discovery: Extracts unique UUIDs from the target column to minimize memory footprint.
24-
- Differential Update: Scans existing registry (if present) to identify UUIDs via set exclusion.
25-
- Registry Extension: Generates sequential UInt32 identifiers starting from max(existing_id) + 1.
26-
- Atomic Write: Sinks updated registry to a temporary Parquet file before performing an atomic replace to ensure data integrity.
27-
28-
Invariants:
29-
- Guarantees 1:1 mapping between UUID strings and UInt32 integers.
30-
- Ensures newly assigned IDs are strictly greater than any existing ID in the registry.
31-
- Never modifies the input DataFrame in-place or returns it.
28+
Enforces idempotency and persistence of UUID-to-Integer mappings using a Delta
29+
pattern.
30+
31+
Contract:
32+
- Discovery: Scans ALL existing mapping deltas from storage (glob) to find max_id
33+
and existing UUIDs.
34+
- Differential Update: Identifies only the new UUIDs not present in history.
35+
- Registry Extension: Generates new IDs starting from max(existing) + 1.
36+
- Delta Write: Saves ONLY the new mappings to a local run-specific file for later
37+
promotion.
3238
"""
3339

3440
int_col_name = f"{id_column}_int"
35-
3641
delta_ids = df.select(id_column).unique().get_column(id_column)
3742

38-
if mapping_file_path.exists():
39-
mapping_lf = pl.scan_parquet(mapping_file_path)
43+
# Resolve Storage Path (Local or GCS)
44+
dest_str = str(storage_destination).replace("\\", "/")
45+
is_gcs = dest_str.startswith("gs://")
46+
47+
column_storage_dir = f"{dest_str}/{id_column}"
48+
storage_glob = f"{column_storage_dir}/*.parquet"
49+
50+
# Check existence and Scan history
51+
exists = (
52+
check_gcs_path_exists(column_storage_dir)
53+
if is_gcs
54+
else Path(column_storage_dir).exists()
55+
)
4056

57+
if exists:
58+
mapping_lf = pl.scan_parquet(storage_glob)
59+
60+
# Get max ID across all deltas
4161
max_id = mapping_lf.select(pl.col(int_col_name).max()).collect().item()
4262
max_id = max_id if max_id is not None else 0
4363

@@ -47,39 +67,22 @@ def map_uuid_to_int(df: pl.DataFrame, mapping_file_path: Path, id_column: str) -
4767
.collect()
4868
.get_column(id_column)
4969
)
50-
51-
# Compare the delta ids to existing record while streaming
5270
new_uuids = delta_ids.filter(~delta_ids.is_in(existing_ids.to_list()))
53-
df_new_uuids = pl.DataFrame({id_column: new_uuids})
54-
5571
else:
56-
mapping_lf = pl.LazyFrame(
57-
schema={id_column: pl.String, int_col_name: pl.UInt32}
58-
)
5972
max_id = 0
60-
df_new_uuids = pl.DataFrame({id_column: delta_ids})
73+
new_uuids = delta_ids
6174

62-
if df_new_uuids.height > 0:
75+
# If new UUIDs found, write a new detla file
76+
if new_uuids.len() > 0:
6377
start_val = max_id + 1
64-
65-
# Attach new mapped Uint32 ID
66-
new_mapping_df = df_new_uuids.with_columns(
67-
pl.int_range(
68-
start_val, df_new_uuids.height + start_val, dtype=pl.UInt32
69-
).alias(int_col_name)
78+
new_mapping_df = pl.DataFrame({id_column: new_uuids}).with_columns(
79+
pl.int_range(start_val, new_uuids.len() + start_val, dtype=pl.UInt32).alias(
80+
int_col_name
81+
)
7082
)
7183

72-
# Temporarily hold while syncing updates
73-
temp_map_path = mapping_file_path.with_suffix(".tmp.parquet")
74-
75-
if mapping_file_path.exists():
76-
updated_registy_lf = pl.concat([mapping_lf, new_mapping_df.lazy()])
77-
updated_registy_lf.sink_parquet(temp_map_path)
78-
79-
else:
80-
new_mapping_df.write_parquet(temp_map_path)
81-
82-
temp_map_path.replace(mapping_file_path)
84+
local_output_path.parent.mkdir(parents=True, exist_ok=True)
85+
new_mapping_df.write_parquet(local_output_path)
8386

8487

8588
def id_mapping(
@@ -88,65 +91,42 @@ def id_mapping(
8891
mapping_dict: dict,
8992
runtime_dir: Path,
9093
destination: Path | str,
94+
run_id: str,
9195
) -> pl.LazyFrame:
9296
"""
93-
Orchestrates the two-phase transformation of UUID strings to persistent Integer keys.
97+
Orchestrates the transformation of UUID strings to persistent Integer keys using
98+
Delta Architecture.
9499
95100
Execution Workflow:
96-
1. Update (Eager): Iterates through all specified ID columns, updating their respective physical registries on disk.
97-
2. Promote: Synchronizes newly created mapping files from the runtime environment to central storage.
98-
3. Transform (Lazy): Constructs a chain of left-joins against the persistent registries to attach integer keys.
99-
100-
Contract:
101-
- Integrity: Ensures every unique UUID in the input DataFrame has a corresponding entry in the mapping registry before the join.
102-
- Efficiency: Utilizes LazyFrame chaining and Parquet scanning to defer data loading until terminal execution.
103-
- Grain: Maintains the original grain of the input DataFrame; appends {column}_int columns.
104-
105-
Outputs:
106-
- A pl.LazyFrame ready for streaming execution, containing original data enriched with integer surrogates.
101+
1. Update (Delta): Creates new mapping files only for previously unseen UUIDs.
102+
2. Promote: Uploads the tiny new delta files to the central storage directory.
103+
3. Transform (Lazy): Joins the input against the full set of deltas (glob scan).
107104
"""
108105

109106
cols_to_map = mapping_dict.get(table_name, [])
110-
111107
if not cols_to_map:
112108
return df.lazy()
113109

114110
for id_column in cols_to_map:
115-
mapping_filename = f"{id_column}_mapping.parquet"
116-
temp_path = runtime_dir / mapping_filename
111+
local_delta_path = runtime_dir / id_column / f"map_{run_id}.parquet"
117112

118-
# When destination is a GCS URI
119-
if str(destination).startswith("gs://"):
120-
target_path = temp_path
121-
else:
122-
storage_path = destination / mapping_filename # type: ignore
123-
target_path = storage_path if storage_path.exists() else temp_path
124-
125-
if not target_path.parent.exists():
126-
target_path.parent.mkdir(parents=True, exist_ok=True)
127-
128-
map_uuid_to_int(df, target_path, id_column)
113+
map_uuid_to_int(
114+
df=df,
115+
id_column=id_column,
116+
storage_destination=destination,
117+
local_output_path=local_delta_path,
118+
)
129119

130-
# Promote new mapping to storage
131120
promote_new_mapping_files(runtime_dir=runtime_dir, destination=destination)
132121

122+
# Apply mapped ids to dataframe
123+
dest_str = str(destination).replace("\\", "/")
133124
lf_mapped = df.lazy()
134125

135126
for id_column in cols_to_map:
136-
mapping_filename = f"{id_column}_mapping.parquet"
137-
138-
# Switch between local and gcp IO
139-
if str(destination).startswith("gs://"):
140-
destination_str = str(destination)
141-
if not destination_str.endswith("/"):
142-
destination_str += "/"
143-
144-
storage_path = f"{destination_str}{mapping_filename}"
145-
else:
146-
storage_path = str(destination / mapping_filename) # type: ignore
127+
storage_glob = f"{dest_str}/{id_column}/*.parquet"
147128

148-
# Enriched existing id registry with new mapping
149-
registry_lf = pl.scan_parquet(storage_path)
129+
registry_lf = pl.scan_parquet(storage_glob)
150130
lf_mapped = lf_mapped.join(registry_lf, on=id_column, how="left")
151131

152132
return lf_mapped

data_pipeline/shared/storage_adapter.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ def _split_gcs_path(path: str):
2323
return bucket, prefix
2424

2525

26+
def check_gcs_path_exists(gcs_uri: str) -> bool:
27+
"""
28+
Helper to check if a GCS prefix has any blobs (effectively checking if 'directory' exists).
29+
"""
30+
client = storage.Client()
31+
bucket_name, prefix = _split_gcs_path(gcs_uri)
32+
33+
bucket = client.bucket(bucket_name)
34+
blobs = list(bucket.list_blobs(prefix=prefix, max_results=1))
35+
return len(blobs) > 0
36+
37+
2638
def download_raw_snapshot(run_context: RunContext) -> None:
2739
"""
2840
Synchronizes the raw data snapshot from Cloud Storage to the local workspace.
@@ -212,8 +224,8 @@ def promote_new_mapping_files(runtime_dir: Path, destination: Path | str) -> Non
212224
Synchronizes new UUID mapping files from the local temporary directory to central storage.
213225
214226
Contract:
215-
- Identifies all '*_mapping.parquet' files in the local 'runtime_dir'.
216-
- Promotes them to the persistent 'destination' (local directory or GCS bucket).
227+
- Recursively identifies all '*.parquet' files in the local 'runtime_dir' subdirectories.
228+
- Promotes them to the persistent 'destination' under matching subdirectories.
217229
"""
218230

219231
if not runtime_dir.exists():
@@ -223,18 +235,25 @@ def promote_new_mapping_files(runtime_dir: Path, destination: Path | str) -> Non
223235

224236
# Local filesystem case
225237
if not destination_str.startswith("gs://"):
226-
Path(destination).mkdir(parents=True, exist_ok=True)
227-
for file in runtime_dir.glob("*_mapping.parquet"):
228-
shutil.copy2(file, Path(destination))
238+
dest_base = Path(destination)
239+
240+
for file in runtime_dir.rglob("*.parquet"):
241+
if file.is_file():
242+
# Reconstruct relative path in destination
243+
# (e.g., destination/order_id/run_id.parquet)
244+
relative_path = file.relative_to(runtime_dir)
245+
target_path = dest_base / relative_path
246+
target_path.parent.mkdir(parents=True, exist_ok=True)
247+
shutil.copy2(file, target_path)
229248
return
230249

231250
# GCS case
232251
client = storage.Client()
233252
bucket_name, prefix = _split_gcs_path(destination_str)
234253
bucket = client.bucket(bucket_name)
235254

236-
for file in runtime_dir.glob("*_mapping.parquet"):
255+
for file in runtime_dir.rglob("*.parquet"):
237256
if file.is_file():
238-
# Create a blob with the target filename in the bucket
239-
blob = bucket.blob(f"{prefix}/{file.name}")
257+
relative_path = file.relative_to(runtime_dir).as_posix()
258+
blob = bucket.blob(f"{prefix}/{relative_path}")
240259
blob.upload_from_filename(str(file))

0 commit comments

Comments
 (0)