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.
Files changed (22) hide show
  1. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/PKG-INFO +1 -1
  2. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Operations.py +2 -2
  3. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/SilverMerge.py +72 -27
  4. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/WorkflowBuilder.py +13 -3
  5. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/setup.py +1 -1
  6. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/PKG-INFO +1 -1
  7. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/Maintenance.py +0 -0
  8. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/PartitionAnalyzer.py +0 -0
  9. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/PerfAnalyzer.py +0 -0
  10. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/__init__.py +0 -0
  11. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/logging.py +0 -0
  12. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/sparksession.py +0 -0
  13. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/utils.py +0 -0
  14. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/main/utilsDatabricksAPI.py +0 -0
  15. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/OperationsTest.py +0 -0
  16. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/__init__.py +0 -0
  17. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/main_test.py +0 -0
  18. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/com/phida/tests/utils_test.py +0 -0
  19. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/setup.cfg +0 -0
  20. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/SOURCES.txt +0 -0
  21. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/dependency_links.txt +0 -0
  22. {silverstreamingapplication-1.7 → silverstreamingapplication-1.8}/silverstreamingapplication.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: silverstreamingapplication
3
- Version: 1.7
3
+ Version: 1.8
4
4
  Summary: initial version
5
5
  Home-page:
6
6
  Author:
@@ -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
- # spark.sql(f"REFRESH TABLE {srcDatabaseName}.{srcTableName}")
578
- # print(f"Table {srcDatabaseName}.{srcTableName} is Refreshed")
577
+ # spark.sql(f"REFRESH TABLE {srcDatabaseName}.{srcTableName}")
578
+ # print(f"Table {srcDatabaseName}.{srcTableName} is Refreshed")
@@ -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
- # 1) HVR-only (prod behavior): latest by hvr_integ_key
412
- # 2) DSP-only: latest by src_commit_time
413
- # 3) Mixed HVR + DSP: DSP rows win for same PK, then latest timestamp
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: keep existing prod behavior
420
- windowSpec = Window.partitionBy(self.keyColsList).orderBy(col("hvr_integ_key").desc())
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
- # DSP-only (or no hvr_integ_key): latest timestamp wins
431
- windowSpec = Window.partitionBy(self.keyColsList).orderBy(col("src_commit_time").desc()) #__timestamp is casted to src_commit_time in silverCleansing()
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
- # DSP_Change: Build merge conditions only from columns that actually exist in the batch.
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("s.source_operation in (0,3)")
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("s.__operation_type in ('X')")
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
- if not delete_parts:
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
- delete_condition = " OR ".join([f"({p})" for p in delete_parts])
451
- update_insert_condition = f"NOT ({delete_condition})"
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(delete_condition) \
456
- .whenMatchedUpdate(condition=update_insert_condition, set=self.columnsDict) \
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.")
@@ -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:
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='silverstreamingapplication',
5
- version='1.7',
5
+ version='1.8',
6
6
  packages=['com.phida.main', 'com.phida.tests'],
7
7
  url='',
8
8
  license='',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: silverstreamingapplication
3
- Version: 1.7
3
+ Version: 1.8
4
4
  Summary: initial version
5
5
  Home-page:
6
6
  Author: