omnata-plugin-runtime 0.10.12a263__tar.gz → 0.10.13a266__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: omnata-plugin-runtime
3
- Version: 0.10.12a263
3
+ Version: 0.10.13a266
4
4
  Summary: Classes and common runtime components for building and running Omnata Plugins
5
5
  Author: James Weakley
6
6
  Author-email: james.weakley@omnata.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "omnata-plugin-runtime"
3
- version = "0.10.12-a263"
3
+ version = "0.10.13-a266"
4
4
  description = "Classes and common runtime components for building and running Omnata Plugins"
5
5
  authors = ["James Weakley <james.weakley@omnata.com>"]
6
6
  readme = "README.md"
@@ -270,7 +270,9 @@ class SnowflakeViewColumn(BaseModel):
270
270
  This is allowed in Snowflake, as long as the aliased column is defined before it's used in a later column
271
271
  So we need to sort the columns so that if the name of the column appears (in quotes) in the expression of another column, it is ordered first
272
272
  """
273
-
273
+ logger.debug(
274
+ f"Ordering columns by reference for stream: {current_stream_name} ({len(columns)} columns)"
275
+ )
274
276
  # Collect columns to be moved
275
277
  columns_to_move:List[Self] = []
276
278
  # Collect Omnata System columns and keep them at the front
@@ -562,10 +564,32 @@ class SnowflakeViewParts(BaseModel):
562
564
  # Until this generate function is called with the raw stream names, we don't know which streams the user has actually selected, nor which
563
565
  # fields are actually available (some may be dropped due to something like an unsupported formula).
564
566
  # So now there's a pruning process where we remove columns that reference fields that are not available.
565
- # We'll start by doing a first pass and removing unavailable columns from other streams
567
+ # First, explicitly check for circular references between tables, erroring if they are found.
568
+ circular_refs = {}
569
+ for part in [main_stream_view_part] + joined_parts:
570
+ for column in part.columns:
571
+ if column.referenced_columns:
572
+ for ref_stream_name, ref_fields in column.referenced_columns.items():
573
+ # Record this reference
574
+ if (part.stream_name, ref_stream_name) not in circular_refs:
575
+ circular_refs[(part.stream_name, ref_stream_name)] = []
576
+ circular_refs[(part.stream_name, ref_stream_name)].append((column.original_name, ref_fields))
577
+
578
+ # Check for circular references
579
+ for (stream1, stream2), refs1 in circular_refs.items():
580
+ if (stream2, stream1) in circular_refs:
581
+ # Found a potential circular reference between stream1 and stream2
582
+ refs2 = circular_refs[(stream2, stream1)]
583
+ raise ValueError(f"""Cyclic dependency detected: Circular reference between {stream1} and {stream2}.
584
+ {stream1} -> {stream2}: {refs1}
585
+ {stream2} -> {stream1}: {refs2}""")
586
+
587
+ # Now proceed with the actual pruning process
588
+ # First, removing unavailable columns from other streams
566
589
  # then, we can do a final pass and remove columns that reference fields that are not available in the current stream
590
+
567
591
  prune_count = 0
568
- while prune(main_stream_view_part,joined_parts):
592
+ while prune(main_stream_view_part, joined_parts):
569
593
  prune_count += 1
570
594
  if prune_count > 10000:
571
595
  raise ValueError("Pruning of columns from the view has entered an infinite loop")
@@ -574,37 +598,75 @@ class SnowflakeViewParts(BaseModel):
574
598
 
575
599
  def prune(view_part: SnowflakeViewPart, joined_parts: List[SnowflakeViewPart]) -> bool:
576
600
  """
577
- Iteratively prunes columns from the main view part that reference fields
578
- that are not available in the joined parts.
601
+ Prunes columns from view parts that reference fields that don't exist in the referenced streams.
602
+
603
+ This function handles:
604
+ 1. Direct dependencies - removing columns that directly reference non-existent columns
605
+ 2. Transitive dependencies - removing columns that depend on columns that were removed
606
+
607
+ Returns True if any columns were removed, False otherwise.
608
+ Raises ValueError if a cyclic dependency is detected.
579
609
  """
580
- stack = [(view_part, joined_parts)]
581
610
  columns_removed = False
582
-
583
- while stack:
584
- current_part, current_joined_parts = stack.pop()
585
- for column in current_part.columns[:]: # Iterate over a copy to allow safe removal
586
- if column.referenced_columns:
587
- for referenced_stream_name, referenced_fields in column.referenced_columns.items():
588
- if referenced_stream_name == current_part.stream_name:
589
- part = current_part
590
- else:
591
- part = next((p for p in current_joined_parts if p.stream_name == referenced_stream_name), None)
592
- if part is None:
593
- logger.warning(f"Column {column.name} in stream {current_part.stream_name} references stream {referenced_stream_name}, but it was not provided")
594
- current_part.columns.remove(column)
595
- columns_removed = True
596
- break
597
-
598
- columns_missing_from_join = part.columns_missing(referenced_fields)
599
- if columns_missing_from_join:
600
- logger.warning(f"Column {column.name} in stream {current_part.stream_name} references fields {columns_missing_from_join} in stream {referenced_stream_name}, but they were not provided")
601
- current_part.columns.remove(column)
602
- columns_removed = True
603
- break
604
- else:
605
- if part != current_part:
606
- stack.append((part, current_joined_parts))
607
-
611
+
612
+ # Helper function to find a view part by stream name
613
+ def find_part(stream_name: str) -> Optional[SnowflakeViewPart]:
614
+ if stream_name == view_part.stream_name:
615
+ return view_part
616
+ return next((p for p in joined_parts if p.stream_name == stream_name), None)
617
+
618
+ # Helper function to check if a column should be kept or removed
619
+ def should_keep_column(column: SnowflakeViewColumn, part: SnowflakeViewPart) -> bool:
620
+ """
621
+ Checks if a column should be kept based on its dependencies.
622
+ Returns True if the column should be kept, False if it should be removed.
623
+ """
624
+ # If no references, keep the column
625
+ if not column.referenced_columns:
626
+ return True
627
+
628
+ # Check each referenced stream and its fields
629
+ for ref_stream_name, ref_fields in column.referenced_columns.items():
630
+ # Find the referenced part
631
+ ref_part = find_part(ref_stream_name)
632
+
633
+ # If referenced stream doesn't exist, remove the column
634
+ if ref_part is None:
635
+ logger.warning(
636
+ f"Column {column.name} in stream {part.stream_name} references stream "
637
+ f"{ref_stream_name}, but it was not provided"
638
+ )
639
+ return False
640
+
641
+ # Check each referenced field
642
+ for ref_field in ref_fields:
643
+ # Find the referenced column
644
+ ref_column = next((c for c in ref_part.columns if c.original_name == ref_field), None)
645
+
646
+ # If referenced column doesn't exist, remove the column
647
+ if ref_column is None:
648
+ logger.warning(
649
+ f"Column {column.name} in stream {part.stream_name} references field "
650
+ f"{ref_field} in stream {ref_stream_name}, but it was not provided"
651
+ )
652
+ return False
653
+
654
+ # All dependencies are satisfied
655
+ return True
656
+
657
+ # Process columns for removal
658
+ for column in view_part.columns[:]: # Use a copy to allow safe removal
659
+ if not should_keep_column(column, view_part):
660
+ view_part.columns.remove(column)
661
+ columns_removed = True
662
+
663
+ # Process joined parts
664
+ for joined_part in joined_parts:
665
+ for column in joined_part.columns[:]: # Use a copy to allow safe removal
666
+ if not should_keep_column(column, joined_part):
667
+ joined_part.columns.remove(column)
668
+ columns_removed = True
669
+
608
670
  return columns_removed
609
671
 
610
672
  class JsonSchemaTopLevel(BaseModel):
@@ -711,6 +773,9 @@ def normalized_view_part(
711
773
  - A list of SnowflakeViewColumn objects, representing the columns to create in the view
712
774
  - A list of SnowflakeViewJoin objects, representing the joins to create in the view
713
775
  """
776
+ logger.debug(
777
+ f"Building normalized view part for stream: {stream_name}"
778
+ )
714
779
  snowflake_columns: List[SnowflakeViewColumn] = []
715
780
  if include_default_columns:
716
781
  snowflake_columns.append(
@@ -757,6 +822,9 @@ def normalized_view_part(
757
822
  joins = []
758
823
  comment = None
759
824
  if stream_schema is not None:
825
+ logger.debug(
826
+ f"Building view columns for stream: {stream_name}"
827
+ )
760
828
  json_schema = JsonSchemaTopLevel.model_validate(stream_schema)
761
829
  view_columns += json_schema.build_view_columns(
762
830
  column_name_environment=column_name_environment,