SilverStreamingApplication 1.7__tar.gz → 1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/PKG-INFO +1 -1
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Operations.py +2 -2
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/SilverMerge.py +72 -27
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/WorkflowBuilder.py +13 -3
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/setup.py +1 -1
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/PKG-INFO +1 -1
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Maintenance.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/PartitionAnalyzer.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/PerfAnalyzer.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/__init__.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/logging.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/sparksession.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/utils.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/utilsDatabricksAPI.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/OperationsTest.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/__init__.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/main_test.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/utils_test.py +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/setup.cfg +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/SOURCES.txt +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/dependency_links.txt +0 -0
- {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/top_level.txt +0 -0
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Operations.py
RENAMED
|
@@ -574,5 +574,5 @@ def move_dsp_to_bronze(srcDatabaseName, srcTableName, src_dsp_path, src_table_pa
|
|
|
574
574
|
else:
|
|
575
575
|
print(f"No files were copied from {dsp_path} to {bronze_path}, so skipping refresh of {srcDatabaseName}.{srcTableName}")
|
|
576
576
|
|
|
577
|
-
|
|
578
|
-
|
|
577
|
+
# spark.sql(f"REFRESH TABLE {srcDatabaseName}.{srcTableName}")
|
|
578
|
+
# print(f"Table {srcDatabaseName}.{srcTableName} is Refreshed")
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/SilverMerge.py
RENAMED
|
@@ -407,28 +407,37 @@ class SilverMerge:
|
|
|
407
407
|
lower_cols = [c.lower() for c in microBatchOutputDF.columns]
|
|
408
408
|
microBatchOutputDF = microBatchOutputDF.toDF(*lower_cols)
|
|
409
409
|
|
|
410
|
-
# Compatible ordering across all modes
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
410
|
+
# Compatible ordering across all modes.
|
|
411
|
+
# Do not infer mode from column presence because unionByName(allowMissingColumns=True)
|
|
412
|
+
# can materialize placeholder columns with null values.
|
|
413
|
+
# Example: HVR-only batch → after union, __operation_type column exists but is all NULL.
|
|
414
|
+
# Checking column presence alone would incorrectly route HVR data to mixed-mode ordering.
|
|
414
415
|
batch_cols = set(microBatchOutputDF.columns)
|
|
415
|
-
has_hvr_key = "hvr_integ_key" in batch_cols
|
|
416
|
-
has_dsp_op = "__operation_type" in batch_cols
|
|
416
|
+
has_hvr_key = "hvr_integ_key" in batch_cols # True for HVR and mixed tables
|
|
417
|
+
has_dsp_op = "__operation_type" in batch_cols # True for DSP and mixed tables (may be all-null after union)
|
|
418
|
+
has_src_commit_time = "src_commit_time" in batch_cols # Common timestamp column for both sources
|
|
417
419
|
|
|
418
420
|
if has_hvr_key and not has_dsp_op:
|
|
419
|
-
# HVR-only:
|
|
420
|
-
windowSpec = Window.partitionBy(self.keyColsList).orderBy(col("hvr_integ_key").
|
|
421
|
-
elif has_hvr_key and has_dsp_op:
|
|
422
|
-
# Mixed mode: prioritize DSP records, then newest timestamp
|
|
423
|
-
is_dsp_record = col("hvr_integ_key").isNull()
|
|
424
|
-
windowSpec = Window.partitionBy(self.keyColsList).orderBy(
|
|
425
|
-
is_dsp_record.desc(),
|
|
426
|
-
col("src_commit_time").desc(), #__timestamp is casted to src_commit_time in silverCleansing()
|
|
427
|
-
col("hvr_integ_key").desc()
|
|
428
|
-
)
|
|
421
|
+
# HVR-only table (no DSP column at all): deduplicate by integ_key descending — highest key = latest change.
|
|
422
|
+
windowSpec = Window.partitionBy(self.keyColsList).orderBy(col("hvr_integ_key").desc_nulls_last())
|
|
429
423
|
else:
|
|
430
|
-
#
|
|
431
|
-
|
|
424
|
+
# Mixed or DSP path: build ordering dynamically based on which recency columns actually exist.
|
|
425
|
+
order_exprs = []
|
|
426
|
+
if has_dsp_op:
|
|
427
|
+
# Flag actual DSP rows (op in A/I/L/X = 1) vs HVR rows or union-placeholder nulls (= 0).
|
|
428
|
+
# DSP rows sort first (desc), so DSP always wins over HVR for the same key in a mixed batch.
|
|
429
|
+
is_dsp_record = when(col("__operation_type").isin("A", "I", "L", "X"), lit(1)).otherwise(lit(0))
|
|
430
|
+
order_exprs.append(is_dsp_record.desc())
|
|
431
|
+
if has_src_commit_time:
|
|
432
|
+
# Within the same source type, pick the most recent record by commit timestamp.
|
|
433
|
+
order_exprs.append(col("src_commit_time").desc_nulls_last())
|
|
434
|
+
if has_hvr_key:
|
|
435
|
+
# Final tiebreaker for HVR rows: higher integ_key = later change from source.
|
|
436
|
+
order_exprs.append(col("hvr_integ_key").desc_nulls_last())
|
|
437
|
+
if not order_exprs:
|
|
438
|
+
# Defensive fallback: no recency column available, treat all rows as equal.
|
|
439
|
+
order_exprs.append(lit(1))
|
|
440
|
+
windowSpec = Window.partitionBy(self.keyColsList).orderBy(*order_exprs)
|
|
432
441
|
|
|
433
442
|
microBatchOutputDF = microBatchOutputDF.withColumn("latest_record", row_number().over(windowSpec)).filter(
|
|
434
443
|
"latest_record == 1").drop("latest_record")
|
|
@@ -436,24 +445,60 @@ class SilverMerge:
|
|
|
436
445
|
if self.enable_delete_filter == 'Y':
|
|
437
446
|
microBatchOutputDF = self.filter_deleted_records(microBatchOutputDF)
|
|
438
447
|
|
|
439
|
-
#
|
|
440
|
-
# Referencing s.__operation_type when the source is HVR-only causes DELTA_MERGE_UNRESOLVED_EXPRESSION.
|
|
448
|
+
# Build delete/update conditions only from operation columns present in the batch to avoid UNRESOLVED_EXPRESSION errors.
|
|
441
449
|
delete_parts = []
|
|
450
|
+
update_insert_parts = []
|
|
451
|
+
|
|
452
|
+
# HVR: source_operation 0=delete, 3=before-image; all others are insert/update.
|
|
442
453
|
if has_source_operation:
|
|
443
|
-
delete_parts.append(
|
|
454
|
+
delete_parts.append(
|
|
455
|
+
"(s.source_operation IS NOT NULL AND s.source_operation in (0,3))"
|
|
456
|
+
)
|
|
457
|
+
update_insert_parts.append(
|
|
458
|
+
"(s.source_operation IS NOT NULL AND s.source_operation not in (0,3))"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# DSP: __operation_type X=delete; A/I/L are insert/update variants.
|
|
444
462
|
if has_operation_type:
|
|
445
|
-
delete_parts.append(
|
|
463
|
+
delete_parts.append(
|
|
464
|
+
"(s.__operation_type IS NOT NULL AND s.__operation_type in ('X'))"
|
|
465
|
+
)
|
|
466
|
+
update_insert_parts.append(
|
|
467
|
+
"(s.__operation_type IS NOT NULL AND s.__operation_type in ('A','I','L'))"
|
|
468
|
+
)
|
|
446
469
|
|
|
447
|
-
|
|
470
|
+
# At least one operation column must exist; otherwise the batch is malformed.
|
|
471
|
+
if not delete_parts or not update_insert_parts:
|
|
448
472
|
raise Exception("No operation column found in source batch")
|
|
449
473
|
|
|
450
|
-
|
|
451
|
-
|
|
474
|
+
# OR joins HVR and DSP branches because a row belongs to exactly one source system.
|
|
475
|
+
delete_condition = " OR ".join(delete_parts)
|
|
476
|
+
update_insert_condition = " OR ".join(update_insert_parts)
|
|
477
|
+
|
|
478
|
+
# Freshness guard: prevent a late/older microbatch from overwriting a newer target row.
|
|
479
|
+
# src_commit_time is the common recency column populated for both HVR and DSP flows.
|
|
480
|
+
# Example:
|
|
481
|
+
# Batch 1 (arrives first): src_commit_time = '2024-01-01 10:00' → written to Silver (t.src_commit_time = '2024-01-01 10:00')
|
|
482
|
+
# Batch 2 (arrives late): src_commit_time = '2024-01-01 08:00' → BLOCKED because s.src_commit_time < t.src_commit_time
|
|
483
|
+
# Batch 3 (arrives later): src_commit_time = '2024-01-01 12:00' → ALLOWED because s.src_commit_time >= t.src_commit_time
|
|
484
|
+
# New key (no target row): t.src_commit_time IS NULL → ALLOWED (first-time insert)
|
|
485
|
+
freshness_condition = """
|
|
486
|
+
(
|
|
487
|
+
t.src_commit_time IS NULL
|
|
488
|
+
OR (s.src_commit_time IS NOT NULL AND s.src_commit_time >= t.src_commit_time)
|
|
489
|
+
)
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
# Apply freshness to matched delete so stale deletes cannot remove newer target rows.
|
|
493
|
+
matched_delete_condition = f"({delete_condition}) AND ({freshness_condition})"
|
|
494
|
+
|
|
495
|
+
# Apply freshness to matched update so older records do not overwrite newer silver state.
|
|
496
|
+
matched_update_condition = f"({update_insert_condition}) AND ({freshness_condition})"
|
|
452
497
|
|
|
453
498
|
print(f"[SilverMerge] Executing merge for batch {batchId} into {self.tgtDatabaseName}.{self.tgtTableName}...")
|
|
454
499
|
tgtDeltaTable.alias("t").merge(microBatchOutputDF.alias("s"), self.condition) \
|
|
455
|
-
.whenMatchedDelete(
|
|
456
|
-
.whenMatchedUpdate(condition=
|
|
500
|
+
.whenMatchedDelete(matched_delete_condition) \
|
|
501
|
+
.whenMatchedUpdate(condition=matched_update_condition, set=self.columnsDict) \
|
|
457
502
|
.whenNotMatchedInsert(condition=update_insert_condition, values=self.columnsDict) \
|
|
458
503
|
.execute()
|
|
459
504
|
print(f"[SilverMerge] Merge for batch {batchId} complete.")
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/WorkflowBuilder.py
RENAMED
|
@@ -72,7 +72,7 @@ class WorkflowBuilder:
|
|
|
72
72
|
@staticmethod
|
|
73
73
|
def createTask(taskKey, jobClusterKey, notebookPath, srcDatabaseName, srcTableName, tgtDatabaseName, tgtTableName,
|
|
74
74
|
tgtCheckpoint, tgtTablePath, tgtPartitionColumns, derivedColExpr, triggerOnce, dropColumnStr,
|
|
75
|
-
pruneColumn, cluster_table_path, enable_delete_filter, enable_autoloader, modified_after, backfill_interval, library):
|
|
75
|
+
pruneColumn, cluster_table_path, enable_delete_filter, enable_autoloader, modified_after, backfill_interval, is_dsp_kernel, src_dsp_path, library):
|
|
76
76
|
"""
|
|
77
77
|
desc:
|
|
78
78
|
A method for creating the dict/json for a single task
|
|
@@ -141,7 +141,9 @@ class WorkflowBuilder:
|
|
|
141
141
|
"enable_delete_filter": str(enable_delete_filter),
|
|
142
142
|
"enable_autoloader": enable_autoloader,
|
|
143
143
|
"modified_after": modified_after,
|
|
144
|
-
"backfill_interval": backfill_interval
|
|
144
|
+
"backfill_interval": backfill_interval,
|
|
145
|
+
"is_dsp_kernel": is_dsp_kernel,
|
|
146
|
+
"src_dsp_path": src_dsp_path
|
|
145
147
|
}
|
|
146
148
|
},
|
|
147
149
|
"timeout_seconds": timeout_seconds,
|
|
@@ -360,11 +362,19 @@ class WorkflowBuilder:
|
|
|
360
362
|
backfill_interval = task['backfill_interval']
|
|
361
363
|
except:
|
|
362
364
|
backfill_interval = ""
|
|
365
|
+
try:
|
|
366
|
+
is_dsp_kernel = task['is_dsp_kernel']
|
|
367
|
+
except:
|
|
368
|
+
is_dsp_kernel = ""
|
|
369
|
+
try:
|
|
370
|
+
src_dsp_path = task['src_dsp_path']
|
|
371
|
+
except:
|
|
372
|
+
src_dsp_path = ""
|
|
363
373
|
|
|
364
374
|
taskTemp = self.createTask(taskKey, jobClusterKey, notebookPath, srcDatabaseName, srcTableName,
|
|
365
375
|
tgtDatabaseName, tgtTableName, tgtCheckpoint, tgtTablePath,
|
|
366
376
|
tgtPartitionColumns, derivedColExpr, triggerOnce, dropColumnStr,
|
|
367
|
-
pruneColumn, cluster_table_path, enable_delete_filter, enable_autoloader, modified_after, backfill_interval, self.library)
|
|
377
|
+
pruneColumn, cluster_table_path, enable_delete_filter, enable_autoloader, modified_after, backfill_interval, is_dsp_kernel, src_dsp_path, self.library)
|
|
368
378
|
|
|
369
379
|
tasks.append(taskTemp)
|
|
370
380
|
if jobId is None:
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Maintenance.py
RENAMED
|
File without changes
|
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/PerfAnalyzer.py
RENAMED
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/sparksession.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/OperationsTest.py
RENAMED
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/__init__.py
RENAMED
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/main_test.py
RENAMED
|
File without changes
|
{silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/utils_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|