Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## 2.1.0

### UX improvements

- **Data Modeling canvas selects keep dropdown text sized with zoom.** Single- and multi-select menus in the visual editor stay inside the canvas transform so option text matches node chrome when you zoom in or out; truncated labels show the full value on hover.

## 2.0.2

- **Python model run tracking in Airflow.** DJ-generated `etl_helper.py` records each python model run (success, error, skipped, upstream_failed) to a Trino meta table configured via `run_tracking` (`catalog`, `schema`, and `table` are required in the `dj_python_source_config` Airflow Variable). Mapped tasks reconcile failures that occur before model code runs, and expose helpers your DAG can call for end-of-run reconciliation and `[Python Models]` failure email summaries.
Expand Down
144 changes: 101 additions & 43 deletions docs/examples/jaffle_shop/macros/_ext_/strategies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,122 @@
{%- set dest_columns = arg_dict["dest_columns"] -%}
{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}

{#- 1. Parse the table's configuration to see how it is partitioned. Split on
commas that are not inside parentheses so Iceberg bucket transforms such as
bucket(tenant_name, 32) stay intact as a single entry regardless of spacing. -#}
{%- if "partitioning" in config_properties -%}
{%- set raw_partitioning = config_properties["partitioning"] | string -%}
{%- set cleaned_partitioning = raw_partitioning | replace("ARRAY[", "") | replace("]", "") | replace("'", "") -%}
{%- set partitioned_by = modules.re.split(',[ ]*(?![^(]*[)])', cleaned_partitioning) | map('trim') | list -%}
{%- else -%}
{%- set partitioned_by = [] -%}
{%- endif -%}

{%- set partitioned_by = partitioned_by | reject('==', '') | list -%}
{%- set mat_relation = temp_relation.incorporate(path={"identifier": temp_relation.identifier ~ "_mat"}) -%}

{% if execute %}
{# 1. Create the materialized table once #}
{%- do run_query("create or replace table " ~ mat_relation ~ " as (select " ~ dest_cols_csv ~ " from " ~ temp_relation ~ ")") -%}
{% endif %}

{% if is_incremental() and partitioned_by | length > 0 %}
{%- set target_columns = adapter.get_columns_in_relation(target_relation) -%}
{%- set col_types = {} -%}
{%- for col in target_columns -%}
{%- do col_types.update({col.name | lower: col.data_type}) -%}
{%- endfor -%}
{% if is_incremental() %}
{#- 2. Pull the raw variable string -#}
{%- set raw_date_var = var('execute_date', var('event_dates', modules.datetime.date.today().strftime('%Y-%m-%d'))) | string -%}

{%- set get_partitions_sql -%}
select distinct {{ partitioned_by | join(", ") }} from {{ mat_relation }}
{%- endset -%}
{%- set partition_results = run_query(get_partitions_sql) -%}
{#- 3. Handle ~ range, comma-separated list (source_etl), or single date -#}
{%- set is_range = false -%}
{%- set is_date_list = false -%}
{%- set date_list = [] -%}
{%- set month_list = [] -%}
{%- set start_date = none -%}
{%- set end_date = none -%}
{%- set start_month = none -%}
{%- set end_month = none -%}

{% if execute and partition_results.rows | length > 0 %}
{# 3. Run individual DELETEs.
Trino treats these as simple metadata drops. No OR-complexity issues. #}
{%- for row in partition_results.rows -%}
{%- set row_conditions = [] -%}
{%- for val in row.values() -%}
{%- set col_name = partitioned_by[loop.index0] | replace('"', '') | replace('`', '') | lower -%}
{%- set col_type = col_types.get(col_name, 'varchar') | lower -%}

{%- if val is none -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " IS NULL") -%}
{%- elif 'date' in col_type -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = DATE '" ~ val ~ "'") -%}
{%- elif 'timestamp' in col_type -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = CAST('" ~ val ~ "' AS " ~ col_type ~ ")") -%}
{%- else -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = '" ~ (val | string | replace("'", "''")) ~ "'") -%}
{%- if '~' in raw_date_var -%}
{%- set date_parts = raw_date_var.split('~') -%}
{%- set start_date = date_parts[0] | trim -%}
{%- set end_date = date_parts[1] | trim -%}
{%- set start_month = start_date[0:7] ~ "-01" -%}
{%- set end_month = end_date[0:7] ~ "-01" -%}
{%- set is_range = true -%}
{%- elif ',' in raw_date_var -%}
{%- set is_date_list = true -%}
{%- for date_part in raw_date_var.split(',') -%}
{%- set date_part = date_part | trim -%}
{%- if date_part -%}
{%- do date_list.append(date_part) -%}
{%- set month_part = date_part[0:7] ~ "-01" -%}
{%- if month_part not in month_list -%}
{%- do month_list.append(month_part) -%}
{%- endif -%}
{%- endfor -%}

delete from {{ target_relation }} where {{ row_conditions | join(" AND ") }};
{%- endif -%}
{%- endfor -%}
{% endif %}
{% elif is_incremental() %}
delete from {{ target_relation }};
{%- else -%}
{%- set start_date = raw_date_var | trim -%}
{%- set start_month = start_date[0:7] ~ "-01" -%}
{%- endif -%}

{#- 4. Build the delete conditions based on the precise structural rules -#}
{%- set delete_conditions = [] -%}

{#- Monthly rule applies if column is a partition -#}
{%- if 'portal_partition_monthly' in partitioned_by -%}
{%- if is_range -%}
{%- do delete_conditions.append("portal_partition_monthly BETWEEN DATE '" ~ start_month ~ "' AND DATE '" ~ end_month ~ "'") -%}
{%- elif is_date_list -%}
{%- if month_list | length == 1 -%}
{%- do delete_conditions.append("portal_partition_monthly = DATE '" ~ month_list[0] ~ "'") -%}
{%- else -%}
{%- set month_literals = [] -%}
{%- for month_part in month_list -%}
{%- do month_literals.append("DATE '" ~ month_part ~ "'") -%}
{%- endfor -%}
{%- do delete_conditions.append("portal_partition_monthly IN (" ~ month_literals | join(", ") ~ ")") -%}
{%- endif -%}
{%- else -%}
{%- do delete_conditions.append("portal_partition_monthly = DATE '" ~ start_month ~ "'") -%}
{%- endif -%}
{%- endif -%}

{#- Daily and Hourly tables both get the additional daily filter block -#}
{%- if 'portal_partition_daily' in partitioned_by or 'portal_partition_hourly' in partitioned_by -%}
{%- if is_range -%}
{%- do delete_conditions.append("portal_partition_daily BETWEEN DATE '" ~ start_date ~ "' AND DATE '" ~ end_date ~ "'") -%}
{%- elif is_date_list -%}
{%- if date_list | length == 1 -%}
{%- do delete_conditions.append("portal_partition_daily = DATE '" ~ date_list[0] ~ "'") -%}
{%- else -%}
{%- set daily_literals = [] -%}
{%- for date_part in date_list -%}
{%- do daily_literals.append("DATE '" ~ date_part ~ "'") -%}
{%- endfor -%}
{%- do delete_conditions.append("portal_partition_daily IN (" ~ daily_literals | join(", ") ~ ")") -%}
{%- endif -%}
{%- else -%}
{%- do delete_conditions.append("portal_partition_daily = DATE '" ~ start_date ~ "'") -%}
{%- endif -%}
{%- endif -%}

{#- Dynamic environment filtering if 'env_type' is passed and exists as a partition -#}
{%- if 'wd_env_type' in partitioned_by and var('env_type', none) is not none -%}
{%- do delete_conditions.append("wd_env_type = '" ~ var('env_type') ~ "'") -%}
{%- endif -%}

{#- Run the clear down if conditions were built -#}
{%- if delete_conditions | length > 0 %}
delete from {{ target_relation }}
where {{ delete_conditions | join(" and ") }};
{%- else -%}
{#-
FALLBACK FOR UNPARTITIONED TABLES:
If the table has no partitions, clear the entire table to prevent
data duplication during the subsequent INSERT phase.
-#}
delete from {{ target_relation }};
{%- endif -%}

{% endif %}

{# 4. Finally, insert the new data #}
{#- 5. Stream data directly from the view straight to the target table -#}
insert into {{ target_relation }} ({{ dest_cols_csv }})
select {{ dest_cols_csv }} from {{ mat_relation }};
(
select {{ dest_cols_csv }}
from {{ temp_relation }}
);

drop table if exists {{ mat_relation }};
{% endmacro %}
144 changes: 101 additions & 43 deletions docs/examples/jaffle_shop_lightdash/macros/_ext_/strategies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,122 @@
{%- set dest_columns = arg_dict["dest_columns"] -%}
{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}

{#- 1. Parse the table's configuration to see how it is partitioned. Split on
commas that are not inside parentheses so Iceberg bucket transforms such as
bucket(tenant_name, 32) stay intact as a single entry regardless of spacing. -#}
{%- if "partitioning" in config_properties -%}
{%- set raw_partitioning = config_properties["partitioning"] | string -%}
{%- set cleaned_partitioning = raw_partitioning | replace("ARRAY[", "") | replace("]", "") | replace("'", "") -%}
{%- set partitioned_by = modules.re.split(',[ ]*(?![^(]*[)])', cleaned_partitioning) | map('trim') | list -%}
{%- else -%}
{%- set partitioned_by = [] -%}
{%- endif -%}

{%- set partitioned_by = partitioned_by | reject('==', '') | list -%}
{%- set mat_relation = temp_relation.incorporate(path={"identifier": temp_relation.identifier ~ "_mat"}) -%}

{% if execute %}
{# 1. Create the materialized table once #}
{%- do run_query("create or replace table " ~ mat_relation ~ " as (select " ~ dest_cols_csv ~ " from " ~ temp_relation ~ ")") -%}
{% endif %}

{% if is_incremental() and partitioned_by | length > 0 %}
{%- set target_columns = adapter.get_columns_in_relation(target_relation) -%}
{%- set col_types = {} -%}
{%- for col in target_columns -%}
{%- do col_types.update({col.name | lower: col.data_type}) -%}
{%- endfor -%}
{% if is_incremental() %}
{#- 2. Pull the raw variable string -#}
{%- set raw_date_var = var('execute_date', var('event_dates', modules.datetime.date.today().strftime('%Y-%m-%d'))) | string -%}

{%- set get_partitions_sql -%}
select distinct {{ partitioned_by | join(", ") }} from {{ mat_relation }}
{%- endset -%}
{%- set partition_results = run_query(get_partitions_sql) -%}
{#- 3. Handle ~ range, comma-separated list (source_etl), or single date -#}
{%- set is_range = false -%}
{%- set is_date_list = false -%}
{%- set date_list = [] -%}
{%- set month_list = [] -%}
{%- set start_date = none -%}
{%- set end_date = none -%}
{%- set start_month = none -%}
{%- set end_month = none -%}

{% if execute and partition_results.rows | length > 0 %}
{# 3. Run individual DELETEs.
Trino treats these as simple metadata drops. No OR-complexity issues. #}
{%- for row in partition_results.rows -%}
{%- set row_conditions = [] -%}
{%- for val in row.values() -%}
{%- set col_name = partitioned_by[loop.index0] | replace('"', '') | replace('`', '') | lower -%}
{%- set col_type = col_types.get(col_name, 'varchar') | lower -%}

{%- if val is none -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " IS NULL") -%}
{%- elif 'date' in col_type -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = DATE '" ~ val ~ "'") -%}
{%- elif 'timestamp' in col_type -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = CAST('" ~ val ~ "' AS " ~ col_type ~ ")") -%}
{%- else -%}
{%- do row_conditions.append(partitioned_by[loop.index0] ~ " = '" ~ (val | string | replace("'", "''")) ~ "'") -%}
{%- if '~' in raw_date_var -%}
{%- set date_parts = raw_date_var.split('~') -%}
{%- set start_date = date_parts[0] | trim -%}
{%- set end_date = date_parts[1] | trim -%}
{%- set start_month = start_date[0:7] ~ "-01" -%}
{%- set end_month = end_date[0:7] ~ "-01" -%}
{%- set is_range = true -%}
{%- elif ',' in raw_date_var -%}
{%- set is_date_list = true -%}
{%- for date_part in raw_date_var.split(',') -%}
{%- set date_part = date_part | trim -%}
{%- if date_part -%}
{%- do date_list.append(date_part) -%}
{%- set month_part = date_part[0:7] ~ "-01" -%}
{%- if month_part not in month_list -%}
{%- do month_list.append(month_part) -%}
{%- endif -%}
{%- endfor -%}

delete from {{ target_relation }} where {{ row_conditions | join(" AND ") }};
{%- endif -%}
{%- endfor -%}
{% endif %}
{% elif is_incremental() %}
delete from {{ target_relation }};
{%- else -%}
{%- set start_date = raw_date_var | trim -%}
{%- set start_month = start_date[0:7] ~ "-01" -%}
{%- endif -%}

{#- 4. Build the delete conditions based on the precise structural rules -#}
{%- set delete_conditions = [] -%}

{#- Monthly rule applies if column is a partition -#}
{%- if 'portal_partition_monthly' in partitioned_by -%}
{%- if is_range -%}
{%- do delete_conditions.append("portal_partition_monthly BETWEEN DATE '" ~ start_month ~ "' AND DATE '" ~ end_month ~ "'") -%}
{%- elif is_date_list -%}
{%- if month_list | length == 1 -%}
{%- do delete_conditions.append("portal_partition_monthly = DATE '" ~ month_list[0] ~ "'") -%}
{%- else -%}
{%- set month_literals = [] -%}
{%- for month_part in month_list -%}
{%- do month_literals.append("DATE '" ~ month_part ~ "'") -%}
{%- endfor -%}
{%- do delete_conditions.append("portal_partition_monthly IN (" ~ month_literals | join(", ") ~ ")") -%}
{%- endif -%}
{%- else -%}
{%- do delete_conditions.append("portal_partition_monthly = DATE '" ~ start_month ~ "'") -%}
{%- endif -%}
{%- endif -%}

{#- Daily and Hourly tables both get the additional daily filter block -#}
{%- if 'portal_partition_daily' in partitioned_by or 'portal_partition_hourly' in partitioned_by -%}
{%- if is_range -%}
{%- do delete_conditions.append("portal_partition_daily BETWEEN DATE '" ~ start_date ~ "' AND DATE '" ~ end_date ~ "'") -%}
{%- elif is_date_list -%}
{%- if date_list | length == 1 -%}
{%- do delete_conditions.append("portal_partition_daily = DATE '" ~ date_list[0] ~ "'") -%}
{%- else -%}
{%- set daily_literals = [] -%}
{%- for date_part in date_list -%}
{%- do daily_literals.append("DATE '" ~ date_part ~ "'") -%}
{%- endfor -%}
{%- do delete_conditions.append("portal_partition_daily IN (" ~ daily_literals | join(", ") ~ ")") -%}
{%- endif -%}
{%- else -%}
{%- do delete_conditions.append("portal_partition_daily = DATE '" ~ start_date ~ "'") -%}
{%- endif -%}
{%- endif -%}

{#- Dynamic environment filtering if 'env_type' is passed and exists as a partition -#}
{%- if 'wd_env_type' in partitioned_by and var('env_type', none) is not none -%}
{%- do delete_conditions.append("wd_env_type = '" ~ var('env_type') ~ "'") -%}
{%- endif -%}

{#- Run the clear down if conditions were built -#}
{%- if delete_conditions | length > 0 %}
delete from {{ target_relation }}
where {{ delete_conditions | join(" and ") }};
{%- else -%}
{#-
FALLBACK FOR UNPARTITIONED TABLES:
If the table has no partitions, clear the entire table to prevent
data duplication during the subsequent INSERT phase.
-#}
delete from {{ target_relation }};
{%- endif -%}

{% endif %}

{# 4. Finally, insert the new data #}
{#- 5. Stream data directly from the view straight to the target table -#}
insert into {{ target_relation }} ({{ dest_cols_csv }})
select {{ dest_cols_csv }} from {{ mat_relation }};
(
select {{ dest_cols_csv }}
from {{ temp_relation }}
);

drop table if exists {{ mat_relation }};
{% endmacro %}
5 changes: 2 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"lib": [
"ES2023",
"ESNext",
"ESNext.Decorators"
],
"module": "CommonJS",
"moduleResolution": "node",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "out",
"paths": {
"admin": [
Expand Down
32 changes: 32 additions & 0 deletions web/src/elements/SelectDropdownContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createContext, useContext } from 'react';

/**
* How a select dropdown is positioned relative to its trigger.
*
* - `anchored` — body-portaled menu at screen-native text size (forms, dialogs).
* - `inline` — body-portaled menu (so option clicks are never trapped by React
* Flow node stacking) with option text sized to the canvas zoom via fontSize.
*/
export type SelectDropdownPlacement = 'anchored' | 'inline';

export type SelectDropdownContextValue = {
placement: SelectDropdownPlacement;
/** React Flow viewport zoom; `1` off-canvas. */
zoom: number;
};

export const SelectDropdownContext =
createContext<SelectDropdownContextValue | null>(null);

/** Resolves prop override → canvas/context default → anchored. */
export function useSelectDropdownPlacement(
prop?: SelectDropdownPlacement,
): SelectDropdownPlacement {
const fromContext = useContext(SelectDropdownContext);
return prop ?? fromContext?.placement ?? 'anchored';
}

/** Canvas zoom from context, or `1` when not under a provider. */
export function useSelectDropdownZoom(): number {
return useContext(SelectDropdownContext)?.zoom ?? 1;
}
Loading