An incremental model keeps an existing table and processes a selected batch on later runs.
The first run needs the complete source query because the target doesn't exist yet. On later runs, is_incremental() can enable a filter that reads the stored relation through this. The materialization setting doesn't create that filter for you.
Append inserts every row it receives. It fits immutable events when the batch identifies new arrivals. Use the ingestion clock for that contract: an event that happened last week can arrive today.
A strict greater-than watermark handles repeated runs when arrival clocks advance between batches, but equal timestamps need an additional rule.
These exercises create the target, change the source, and execute your model twice. You'll see missing late arrivals, duplicate replays, and a tied clock that needs both an inclusive boundary and an existing-key check.
{{ config(materialized='incremental', incremental_strategy='append') }}
select distinct event_id, user_id, event_at, loaded_at
from {{ source('pulse', 'events') }}
where event_id is not null and loaded_at is not null
{% if is_incremental() %}
and loaded_at > (select coalesce(max(loaded_at), timestamp '1900-01-01') from {{ this }})
{% endif %}
The initial query reads eligible events; the conditional branch limits later append batches by their stored arrival clock.