-
Notifications
You must be signed in to change notification settings - Fork 144
Support BigQuery nested STRUCT fields in anomaly tests #1012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tlangton3
wants to merge
7
commits into
elementary-data:master
Choose a base branch
from
tlangton3:bigquery-nested-struct-support
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d45a775
Support BigQuery nested STRUCT fields in anomaly tests
tlangton3 8c7b36e
Fix REPEATED ancestor handling and non-BigQuery wrapper safety
tlangton3 e73e9c5
Preserve identifier quoting and guard dimension expressions
tlangton3 17c3574
Merge branch 'master' into bigquery-nested-struct-support
tlangton3 db5061b
test: integration tests for nested STRUCT column and dimension anomalies
haritamar e791b3e
refactor: extract BigQuery nested-column helpers, drop the column wra…
haritamar a5730b1
fix: require identifier-shaped segments in bq_is_nested_identifier
haritamar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,13 @@ | |
| {%- set timestamp_column = metric_properties.timestamp_column %} | ||
| {% set prefixed_dimensions = [] %} | ||
| {% for dimension_column in dimensions %} | ||
| {% do prefixed_dimensions.append("dimension_" ~ dimension_column) %} | ||
| {% if elementary.bq_is_nested_identifier(dimension_column) %} | ||
| {% do prefixed_dimensions.append( | ||
| "dimension_" ~ elementary.bq_safe_alias(dimension_column) | ||
| ) %} | ||
| {% else %} | ||
| {% do prefixed_dimensions.append("dimension_" ~ dimension_column) %} | ||
| {% endif %} | ||
| {% endfor %} | ||
|
|
||
| {% set metric_types = [] %} | ||
|
|
@@ -53,7 +59,7 @@ | |
| ), | ||
| filtered_monitored_table as ( | ||
| select | ||
| {{ column_obj.quoted }}, | ||
| {{ column_obj.quoted }}{% if column_obj.is_nested %} as {{ adapter.quote(column_obj.safe_alias) }}{% endif %}, | ||
| {%- if dimensions -%} | ||
| {{ | ||
| elementary.select_dimensions_columns( | ||
|
|
@@ -78,7 +84,7 @@ | |
| {%- else %} | ||
| filtered_monitored_table as ( | ||
| select | ||
| {{ column_obj.quoted }}, | ||
| {{ column_obj.quoted }}{% if column_obj.is_nested %} as {{ adapter.quote(column_obj.safe_alias) }}{% endif %}, | ||
| {%- if dimensions -%} | ||
| {{ | ||
| elementary.select_dimensions_columns( | ||
|
|
@@ -94,7 +100,7 @@ | |
| column_metrics as ( | ||
|
|
||
| {%- if column_metrics %} | ||
| {%- set column = column_obj.quoted -%} | ||
| {%- set column = adapter.quote(column_obj.safe_alias) if column_obj.is_nested else column_obj.quoted -%} | ||
| select | ||
| {%- if timestamp_column %} | ||
| edr_bucket_start as bucket_start, edr_bucket_end as bucket_end, | ||
|
|
@@ -341,17 +347,135 @@ | |
| {% endif %} | ||
| {% endmacro %} | ||
|
|
||
| {# Segment-quotes nested dimensions on BigQuery and sanitises the alias suffix. | ||
| Backward compatible for non-nested columns and non-BQ adapters. #} | ||
| {% macro select_dimensions_columns(dimension_columns, as_prefix="") %} | ||
| {% set select_statements %} | ||
| {%- for column in dimension_columns -%} | ||
| {{ column }} | ||
| {%- if as_prefix -%} | ||
| {{ " as " ~ as_prefix ~ "_" ~ column }} | ||
| {%- endif -%} | ||
| {%- if not loop.last -%} | ||
| {{ ", " }} | ||
| {%- set _is_nested_bq = elementary.bq_is_nested_identifier(column) -%} | ||
| {%- set _source = elementary.bq_segment_quote(column) if _is_nested_bq else column -%} | ||
| {%- set _alias_suffix = elementary.bq_safe_alias(column) if _is_nested_bq else column -%} | ||
| {{ _source }}{{ " as " ~ as_prefix ~ "_" ~ _alias_suffix }} | ||
| {%- else -%} | ||
| {{ column }} | ||
| {%- endif -%} | ||
| {%- if not loop.last -%}{{ ", " }}{%- endif -%} | ||
| {%- endfor -%} | ||
| {% endset %} | ||
| {{ return(select_statements) }} | ||
| {% endmacro %} | ||
|
|
||
|
|
||
| {# ---------------------------------------------------------------------- #} | ||
| {# BigQuery STRUCT nested-field helpers. #} | ||
| {# ---------------------------------------------------------------------- #} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all the utilities here should not be in this file, but in dedicated files |
||
|
|
||
| {# True only on BigQuery and only when `name` is a plain dotted identifier path | ||
| (e.g. user.address.city) — i.e. an actual nested STRUCT reference. Returns | ||
| false for plain identifiers, SQL expressions (dimensions are documented as | ||
| accepting arbitrary expressions, which must pass through untouched) and | ||
| non-BigQuery adapters. #} | ||
| {% macro bq_is_nested_identifier(name) %} | ||
| {%- if target.type != 'bigquery' or name is not string -%} | ||
| {{ return(false) }} | ||
| {%- endif -%} | ||
| {{ return(modules.re.match('^\\w+(\\.\\w+)+$', name) is not none) }} | ||
| {% endmacro %} | ||
|
|
||
| {# Segment-quote a nested identifier path for BigQuery: | ||
| user.address.city -> `user`.`address`.`city`. | ||
| Anything that is not a nested identifier path (plain identifiers, SQL | ||
| expressions, non-BigQuery adapters) is returned unchanged, preserving | ||
| existing behaviour at all callsites. #} | ||
| {% macro bq_segment_quote(name) %} | ||
| {%- if elementary.bq_is_nested_identifier(name) -%} | ||
| {%- set parts = [] -%} | ||
| {%- for seg in name.split('.') -%} | ||
| {%- do parts.append('`' ~ seg ~ '`') -%} | ||
| {%- endfor -%} | ||
| {{ parts | join('.') }} | ||
| {%- else -%} | ||
| {{ name }} | ||
| {%- endif -%} | ||
| {% endmacro %} | ||
|
|
||
| {# Convert a (possibly dotted) identifier into a dot-free alias safe to use | ||
| as a SQL identifier. No-op for names without dots. #} | ||
| {% macro bq_safe_alias(name) %} | ||
| {{- name | replace('.', '__') -}} | ||
| {% endmacro %} | ||
|
|
||
| {# Wrap a Column / BigQueryColumn with a dict carrying the SQL identifier | ||
| representation (.quoted, segment-quoted for nested), a CTE-projection-safe | ||
| alias (.safe_alias, dot-free) and an .is_nested flag. For non-nested columns | ||
| and non-BigQuery adapters the wrapper mirrors the original Column's values | ||
| (safe_alias falls back to .quoted so identifier quoting is never lost), so | ||
| downstream consumers see no behavioural difference. #} | ||
| {% macro wrap_column_for_struct_support(column_obj) %} | ||
| {%- set name = column_obj.name -%} | ||
| {%- set is_nested = elementary.bq_is_nested_identifier(name) -%} | ||
| {%- if is_nested -%} | ||
| {%- set quoted_segments = [] -%} | ||
| {%- for seg in name.split('.') -%} | ||
| {%- do quoted_segments.append('`' ~ seg ~ '`') -%} | ||
| {%- endfor -%} | ||
| {%- set quoted = quoted_segments | join('.') -%} | ||
| {%- set safe_alias = name | replace('.', '__') -%} | ||
| {%- else -%} | ||
| {%- set quoted = column_obj.quoted -%} | ||
| {%- set safe_alias = column_obj.quoted -%} | ||
| {%- endif -%} | ||
| {# `fields` only exists on BigQueryColumn; guard so non-BigQuery | ||
| adapters (Snowflake, Postgres, Redshift, ...) don't trip on the | ||
| attribute access. #} | ||
| {%- set fields = column_obj.fields if column_obj.fields is defined else [] -%} | ||
| {{ return({ | ||
| 'name': name, | ||
| 'column': column_obj.column, | ||
| 'quoted': quoted, | ||
| 'safe_alias': safe_alias, | ||
| 'is_nested': is_nested, | ||
| 'dtype': column_obj.dtype, | ||
| 'data_type': column_obj.data_type, | ||
| 'fields': fields, | ||
| }) }} | ||
| {% endmacro %} | ||
|
|
||
| {# Walk a BigQuery STRUCT tree and collect dotted leaf names that are safe to | ||
| monitor without UNNEST — i.e. no REPEATED ancestor anywhere in the path, | ||
| and the leaf itself is not REPEATED. `BigQueryColumn.flatten()` returns leaf | ||
| columns with the leaf's own mode but discards ancestor modes, so this walker | ||
| is the source of truth for "which leaves can we project directly?". #} | ||
| {% macro bq_safe_leaf_names(column_obj) %} | ||
| {%- set safe_names = [] -%} | ||
| {%- if column_obj.mode != 'REPEATED' | ||
| and column_obj.fields is defined | ||
| and column_obj.fields | length > 0 -%} | ||
| {%- for child in column_obj.fields -%} | ||
| {%- do elementary._bq_walk_collect( | ||
| child, [column_obj.column], false, safe_names | ||
| ) -%} | ||
| {%- endfor -%} | ||
| {%- endif -%} | ||
| {{ return(safe_names) }} | ||
| {% endmacro %} | ||
|
|
||
| {# Recursive helper: walks a google.cloud.bigquery.SchemaField subtree, | ||
| propagating whether any ancestor was REPEATED. Append safe leaf names to | ||
| `safe_names`. #} | ||
| {% macro _bq_walk_collect(field, prefix, has_repeated_ancestor, safe_names) %} | ||
| {%- set new_prefix = prefix + [field.name] -%} | ||
| {%- if field.fields | length == 0 -%} | ||
| {%- if not has_repeated_ancestor and field.mode != 'REPEATED' -%} | ||
| {%- do safe_names.append(new_prefix | join('.')) -%} | ||
| {%- endif -%} | ||
| {%- else -%} | ||
| {%- set new_has_repeated = has_repeated_ancestor or (field.mode == 'REPEATED') -%} | ||
| {%- for child in field.fields -%} | ||
| {%- do elementary._bq_walk_collect( | ||
| child, new_prefix, new_has_repeated, safe_names | ||
| ) -%} | ||
| {%- endfor -%} | ||
| {%- endif -%} | ||
| {% endmacro %} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General note - unless very complicated, we need integration tests of column + dimension tests with struct fields.