Skip to main content

Jinja templating in SQL

Ascend uses Jinja templating to make SQL Components dynamic and reusable. This guide covers Jinja syntax, macros, and common patterns.

Basic syntax​

Expressions​

Output values using double curly braces:

SELECT * FROM {{ ref('upstream_component') }}

Statements​

Control flow and logic using {% %}:

{% if is_incremental() %}
WHERE updated_at > '{{ get_last_run_timestamp() }}'
{% endif %}

Comments​

Add Jinja comments that don't appear in rendered SQL:

{# This is a Jinja comment #}
SELECT * FROM {{ ref('source') }}

Built-in functions​

Ascend provides these built-in Jinja functions:

FunctionDescription
ref('component_name')Reference another Component
ref('component', flow='flow_name')Cross-Flow reference
config()Access component configuration
is_incremental()Check if running incrementally
with_test(...)Add inline tests

Component references​

-- Same Flow
SELECT * FROM {{ ref('upstream_component') }}

-- Cross-Flow reference
SELECT * FROM {{ ref('shared_table', flow='shared_flow') }}

Incremental processing​

{% if is_incremental() %}
SELECT * FROM {{ ref('source') }}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% else %}
SELECT * FROM {{ ref('source') }}
{% endif %}

Variables​

Set variables​

{% set table_name = 'customers' %}
{% set threshold = 100 %}
{% set columns = ['id', 'name', 'email'] %}

SELECT * FROM {{ ref(table_name) }}
WHERE amount > {{ threshold }}

Dictionary variables​

{% set config = {
'schema': 'analytics',
'table': 'events',
'date_column': 'event_date'
} %}

SELECT * FROM {{ config.schema }}.{{ config.table }}
WHERE {{ config.date_column }} > '2024-01-01'

Loops​

Basic loop​

SELECT
{% for col in ['revenue', 'cost', 'profit'] %}
SUM({{ col }}) as total_{{ col }}{% if not loop.last %},{% endif %}
{% endfor %}
FROM {{ ref('financial_data') }}

Loop with index​

{% for metric in metrics %}
{{ metric }} as metric_{{ loop.index }}{% if not loop.last %},{% endif %}
{% endfor %}

Loop variables​

VariableDescription
loop.indexCurrent iteration (1-indexed)
loop.index0Current iteration (0-indexed)
loop.firstTrue if first iteration
loop.lastTrue if last iteration
loop.lengthTotal number of items

Conditionals​

If/else​

SELECT
order_id,
{% if data_plane_type == "snowflake" %}
TO_TIMESTAMP(order_date) as order_timestamp
{% elif data_plane_type == "bigquery" %}
TIMESTAMP(order_date) as order_timestamp
{% else %}
order_date as order_timestamp
{% endif %}
FROM {{ ref('orders') }}

Conditional expressions​

SELECT
{{ 'user_id' if include_user else 'NULL as user_id' }}
FROM {{ ref('events') }}

Macros​

Macros are reusable code snippets defined in the macros/ directory.

Define a macro​

macros/utils.sql
{% macro safe_divide(numerator, denominator) %}
CASE
WHEN {{ denominator }} = 0 THEN NULL
ELSE {{ numerator }} / {{ denominator }}
END
{% endmacro %}

{% macro date_trunc_day(column) %}
DATE_TRUNC('day', {{ column }})
{% endmacro %}

Import and use macros​

flows/analytics/components/metrics.sql
{% from 'macros/utils.sql' import safe_divide, date_trunc_day %}

SELECT
{{ date_trunc_day('created_at') }} as day,
{{ safe_divide('revenue', 'orders') }} as avg_order_value
FROM {{ ref('orders') }}
GROUP BY 1

Macro with default parameters​

macros/aggregations.sql
{% macro sum_if(column, condition, alias=none) %}
SUM(CASE WHEN {{ condition }} THEN {{ column }} ELSE 0 END)
{% if alias %} as {{ alias }}{% endif %}
{% endmacro %}
{% from 'macros/aggregations.sql' import sum_if %}

SELECT
{{ sum_if('amount', "status = 'completed'", 'completed_amount') }},
{{ sum_if('amount', "status = 'pending'", 'pending_amount') }}
FROM {{ ref('transactions') }}

Common patterns​

Multi-source union​

Combine multiple sources dynamically:

{% set source_tables = ['orders_2022', 'orders_2023', 'orders_2024'] %}

{% for table in source_tables %}
SELECT
'{{ table }}' as source_table,
*
FROM {{ ref(table) }}
{% if not loop.last %}UNION ALL{% endif %}
{% endfor %}

Dynamic column selection​

{% set dimensions = ['region', 'product', 'channel'] %}
{% set metrics = ['revenue', 'quantity'] %}

SELECT
{% for dim in dimensions %}
{{ dim }},
{% endfor %}
{% for metric in metrics %}
SUM({{ metric }}) as total_{{ metric }}{% if not loop.last %},{% endif %}
{% endfor %}
FROM {{ ref('sales') }}
GROUP BY
{% for dim in dimensions %}
{{ dim }}{% if not loop.last %},{% endif %}
{% endfor %}

Platform-specific SQL​

Handle differences between data platforms:

{% if data_plane_type == "snowflake" %}
SELECT DATEADD('day', -7, CURRENT_DATE()) as week_ago
{% elif data_plane_type == "bigquery" %}
SELECT DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY) as week_ago
{% elif data_plane_type == "databricks" %}
SELECT DATE_SUB(CURRENT_DATE(), 7) as week_ago
{% endif %}

Conditional joins​

SELECT
o.order_id,
o.amount
{% if include_customer_details %}
, c.name as customer_name
, c.email as customer_email
{% endif %}
FROM {{ ref('orders') }} o
{% if include_customer_details %}
LEFT JOIN {{ ref('customers') }} c ON o.customer_id = c.id
{% endif %}

Filters​

Jinja filters modify values:

{% set name = 'hello world' %}
{{ name | upper }} {# HELLO WORLD #}
{{ name | replace(' ', '_') }} {# hello_world #}

{% set items = ['a', 'b', 'c'] %}
{{ items | join(', ') }} {# a, b, c #}

Common filters:

  • upper, lower: Change case
  • replace(old, new): String replacement
  • join(separator): Join list items
  • default(value): Provide default if undefined
  • trim: Remove whitespace

Best practices​

  1. Keep macros focused: Each macro should do one thing well
  2. Use descriptive names: Make macro purpose clear from the name
  3. Document parameters: Comment what each macro parameter expects
  4. Avoid deep nesting: Limit nested loops and conditionals for readability
  5. Test incrementally: Build complex templates step by step
  6. Use whitespace control: Use {%- and -%} to control output whitespace

Whitespace control​

Control whitespace in output with -:

{%- for col in columns -%}
{{ col }}
{%- if not loop.last %}, {% endif -%}
{%- endfor -%}

Next steps​