Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Check [dbt Hub](https://hub.getdbt.com/dbt-labs/dbt_utils/latest/) for the lates
- [Introspective macros](#introspective-macros)
- [get\_column\_values (source)](#get_column_values-source)
- [get\_filtered\_columns\_in\_relation (source)](#get_filtered_columns_in_relation-source)
- [get\_columns\_by\_pattern (source)](#get_columns_by_pattern-source)
- [get\_relations\_by\_pattern (source)](#get_relations_by_pattern-source)
- [get\_relations\_by\_prefix (source)](#get_relations_by_prefix-source)
- [get\_query\_results\_as\_dict (source)](#get_query_results_as_dict-source)
Expand Down Expand Up @@ -759,6 +760,44 @@ to pull column names in a non-filtered fashion, also bringing along with it othe
...
```

### get_columns_by_pattern ([source](macros/sql/get_columns_by_pattern.sql))

This macro returns an iterable Jinja list of column names from a given [relation](https://docs.getdbt.com/docs/writing-code-in-dbt/class-reference/#relation) that match a SQL LIKE pattern.

- supports SQL LIKE pattern syntax (`%` for wildcard, `_` for single character)
- optionally exclude columns matching a pattern
- the input values are case-insensitive

**Args:**

- `from` (required): a [Relation](https://docs.getdbt.com/reference/dbt-classes#relation) (a `ref` or `source`) to get columns from
- `pattern` (required): SQL LIKE pattern to match column names (case-insensitive)
- `exclude` (optional, default=`''`): SQL LIKE pattern to exclude matching columns

**Usage:**

```sql
-- Get all columns ending with '_item'
{% set item_columns = dbt_utils.get_columns_by_pattern(
from=ref('sales'),
pattern='%_item'
) %}

-- Get year columns (2020_, 2021_, etc.) excluding archived ones
{% set year_columns = dbt_utils.get_columns_by_pattern(
from=ref('revenue'),
pattern='20%',
exclude='%_archived'
) %}

-- Use in a for loop
select
{% for col in item_columns %}
max({{ col }}) as max_{{ col }}{% if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('sales') }}
```

### get_relations_by_pattern ([source](macros/sql/get_relations_by_pattern.sql))

Returns a list of [Relations](https://docs.getdbt.com/docs/writing-code-in-dbt/class-reference/#relation)
Expand Down
4 changes: 4 additions & 0 deletions integration_tests/data/sql/data_columns_by_pattern.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
user_id,current_item,last_item,state,revenue_2024,cost_2024,revenue_2023,revenue_2024_archived,name
1,apple,banana,active,1000,500,900,800,alice
2,orange,grape,inactive,1500,700,1100,1200,bob
3,pear,melon,active,2000,900,1300,1600,charlie
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
current_item,last_item
pear,melon
5 changes: 5 additions & 0 deletions integration_tests/models/sql/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ models:
- dbt_utils.equality:
compare_model: ref('data_filtered_columns_in_relation_expected')

- name: test_get_columns_by_pattern
data_tests:
- dbt_utils.equality:
compare_model: ref('data_columns_by_pattern_expected')

- name: test_get_relations_by_prefix_and_union
columns:
- name: event
Expand Down
16 changes: 16 additions & 0 deletions integration_tests/models/sql/test_get_columns_by_pattern.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% set pattern = '%_item' %}
{% set column_names = dbt_utils.get_columns_by_pattern(from=ref('data_columns_by_pattern'), pattern=pattern) %}

with data as (

select

{% for column_name in column_names %}
max({{ column_name }}) as {{ column_name }} {% if not loop.last %},{% endif %}
{% endfor %}

from {{ ref('data_columns_by_pattern') }}

)

select * from data
80 changes: 80 additions & 0 deletions macros/sql/get_columns_by_pattern.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% macro get_columns_by_pattern(from, pattern, exclude='') -%}
{{ return(adapter.dispatch('get_columns_by_pattern', 'dbt_utils')(from, pattern, exclude)) }}
{% endmacro %}

{% macro default__get_columns_by_pattern(from, pattern, exclude='') -%}
{%- do dbt_utils._is_relation(from, 'get_columns_by_pattern') -%}
{%- do dbt_utils._is_ephemeral(from, 'get_columns_by_pattern') -%}

{# Prevent querying of db in parsing mode. This works because this macro does not create any new refs. #}
{%- if not execute -%}
{{ return([]) }}
{% endif %}

{%- set matched_cols = [] %}
{%- set cols = adapter.get_columns_in_relation(from) -%}

{# Convert SQL LIKE pattern to regex-compatible pattern #}
{# % becomes .*, _ becomes ., escape special regex chars #}
{%- set pattern_lower = pattern | lower -%}
{%- set exclude_lower = exclude | lower -%}

{%- for col in cols -%}
{%- set col_name_lower = col.column | lower -%}

{# Check if column matches the pattern #}
{%- if dbt_utils._matches_pattern(col_name_lower, pattern_lower) -%}
{# Check if column should be excluded #}
{%- if exclude == '' or not dbt_utils._matches_pattern(col_name_lower, exclude_lower) -%}
{% do matched_cols.append(col.column) %}
{%- endif -%}
{%- endif -%}
{%- endfor %}

{{ return(matched_cols) }}

{%- endmacro %}

{# Helper macro to match SQL LIKE patterns #}
{% macro _matches_pattern(text, pattern) -%}
{# Convert SQL LIKE pattern (%, _) to simple string matching #}
{# This is a simplified implementation that handles common cases #}

{%- set pattern_parts = [] -%}
{%- set current_part = '' -%}
{%- set i = 0 -%}

{# Parse the pattern into parts #}
{%- if pattern.startswith('%') and pattern.endswith('%') -%}
{# Pattern is %text% - match if contains #}
{%- set search_text = pattern[1:-1] -%}
{%- if search_text in text -%}
{{ return(true) }}
{%- else -%}
{{ return(false) }}
{%- endif -%}
{%- elif pattern.startswith('%') -%}
{# Pattern is %text - match if ends with #}
{%- set search_text = pattern[1:] -%}
{%- if text.endswith(search_text) -%}
{{ return(true) }}
{%- else -%}
{{ return(false) }}
{%- endif -%}
{%- elif pattern.endswith('%') -%}
{# Pattern is text% - match if starts with #}
{%- set search_text = pattern[:-1] -%}
{%- if text.startswith(search_text) -%}
{{ return(true) }}
{%- else -%}
{{ return(false) }}
{%- endif -%}
{%- else -%}
{# No wildcards - exact match #}
{%- if text == pattern -%}
{{ return(true) }}
{%- else -%}
{{ return(false) }}
{%- endif -%}
{%- endif -%}
{%- endmacro %}
Loading