For the complete documentation index, see llms.txt. This page is also available as Markdown.

Models

SQL model files, config declaration, materialization modes, template functions, macros, seeds, hooks, and data tests.

A model is a .sql file with a SELECT query that defines a table or view in the target database. Models can use Jinja templating for dynamic SQL, reference other models for dependency resolution, and declare configuration with front-matter or a config block.

Config Declaration

There are two ways to declare model configuration. If both are present, YAML front-matter takes priority and the config() block becomes a no-op.

Sling reads front-matter from the leading comment of a model file. Any standard SQL comment style is accepted, so the file stays valid for editors and linters.

Quick guide:

  • A few keys? Use -- { ... } on a single line — it stays out of the way.

  • More config (hooks, ranges, tests, multi-line lists)? Use /* { ... } */ or /** ... **/ for a proper block.

-- { ... } line comment (best for short config)

Inline { ... } flow object — good when you need only a key or two:

-- {schema: marts}
SELECT id, name FROM raw_customers
-- {mode: incremental, unique_key: id, update_key: updated_at}
SELECT id, name, updated_at FROM raw_customers WHERE {{ incremental_where_cond() }}

You can also span multiple -- lines, as long as the joined body forms a { ... } object:

-- {
--   mode: incremental,
--   unique_key: id,
--   update_key: updated_at
-- }
SELECT id, name, updated_at FROM raw_customers WHERE {{ incremental_where_cond() }}

/* { ... } */ block comment (best for richer config)

Use a regular block comment with a { ... } body when you have hooks, ranges, or anything that is easier in multi-line YAML:

JSON syntax also works in /* ... */:

/** ... **/ doc-block (plain YAML, no braces required)

A doc-block lets you write plain YAML, with bare keys, and no { } around it. Use this when you prefer block YAML over flow syntax:

Why the { ... } requirement for the -- and /* */ styles? It separates config from a regular comment. A line like -- Pre-statement: build temp table would otherwise look like YAML and be read as front-matter. The /** ... **/ doc-block style is unambiguous and accepts any YAML, including bare keys.

Jinja Config Block (dbt Compatibility)

Use {%- config(...) -%} for a syntax that dbt users know. Sling applies it only when the file has no YAML front-matter.

Config Priority

  1. YAML front-matter (/** ... **/, -- {...}, or /* {...} */)

  2. Jinja config() block (ignored if front-matter exists)

  3. sling_build.yml defaults section

  4. Built-in default: full-refresh

tags and hooks are additive: model values are added to the defaults values instead of replacing them.

Config Options

Option
Type
Default
Description

mode

string

full-refresh

Materialization mode. See Modes

materialized

string

dbt alias for mode. Accepts table, view, incremental. An explicit mode wins

unique_key

string or list

Primary key column(s) for incremental merge. Also the ClickHouse ORDER BY

merge_strategy

string

delete+insert

Merge strategy: delete+insert, update+insert, insert

update_key

string

Watermark column. Required for mode: incremental

tags

list

[]

Tags for selector filtering

hooks

object

Start/end hooks to run before and after the model. See Hooks

tests

list

[]

Declarative data tests. See Data Tests

schema

string

(from folder)

Override the schema derived from the folder structure

database

string

Override the database. Three-part dialects only. See Database

enabled

bool

true

Set to false to remove this model from the DAG

engine

string

MergeTree()

ClickHouse ENGINE clause override

range

object

Range block for incremental models: start / advance / lookback. See Incremental

drop_cascade

bool

false

Add CASCADE to DROP TABLE / DROP VIEW when the dialect supports it

rewrite

bool

true

Set false to skip bare-name table rewriting. ref(), src(), and @name still resolve

Materialization Modes

Each model has a materialization mode that sets how Sling writes its SQL output to the target database.

Mode
Description

full-refresh

Default. Replaces the table with CREATE TABLE AS SELECT

truncate

Truncates the table and re-inserts. Keeps DDL, GRANTs, and indexes

incremental

Merges new and updated rows through a temp table and a merge strategy

append

Appends the full result set with INSERT INTO. No deduplication

view

Creates or replaces a SQL view instead of a table

Sling also accepts these aliases:

Alias
Resolves to
Note

table

full-refresh

dbt compatibility

snapshot

append

Deprecated. Gives a warning

ephemeral

Not supported. Gives an error — use view or table

Sling validates the mode at project load, before it runs anything. An unrecognized mode gives an error that lists the valid values:

model '<name>': unknown mode 'incremenal'; expected one of: append, full-refresh, incremental, truncate, view

The same check applies to defaults.mode in sling_build.yml:

<path>/sling_build.yml: unknown defaults.mode 'bogus'; expected one of: append, full-refresh, incremental, truncate, view

full-refresh

The default mode. Replaces the table on every run.

Run
SQL Behavior

First

CREATE TABLE {table} AS ({sql})

Subsequent

DROP TABLE {table}CREATE TABLE {table} AS ({sql})

When to use: Dimension tables, lookup tables, and small tables where a full reload is fast.

If a view of the same name exists, Sling drops it first.

truncate

Similar to full-refresh, but keeps the existing table structure: columns, constraints, permissions, and indexes.

Run
SQL Behavior

First

CREATE TABLE {table} AS ({sql})

Subsequent

TRUNCATE TABLE {table}INSERT INTO {table} SELECT ...

When to use: When you must keep GRANTs, indexes, or other DDL applied to the table.

incremental

Merges new and updated rows into the target table. Needs a unique_key and an update_key.

Run
SQL Behavior

First

Falls back to full-refresh (creates the table)

Subsequent

Load into a temp table → merge into the target with the merge strategy

Sling supports two styles of incremental models: dbt-compatible (is_incremental() + {{ this }}) and sling-native ({{ incremental_where_cond() }} + optional range: front-matter). See Incremental for the full comparison.

Merge Strategies

Strategy
Behavior
Best For

delete+insert (default)

DELETE matching rows by unique_key, then INSERT all new rows

All databases, including ClickHouse

update+insert

UPDATE existing rows, INSERT new rows (SQL MERGE)

Postgres, Snowflake, BigQuery

insert

Append only — no deduplication

Event logs, append-only tables

An unrecognized value falls back to delete+insert.

Use --full-refresh to force a full rebuild of incremental models: sling build run --full-refresh. This drops and recreates the table, then runs the model with no incremental condition.

append

Appends the complete result set to the target table on every run. No deduplication.

Run
SQL Behavior

First

CREATE TABLE {table} AS ({sql})

Subsequent

INSERT INTO {table} SELECT ...

When to use: Audit tables, change history, and time-series snapshots where you want to track state over time.

view

Creates a SQL view instead of a materialized table. No data is stored.

Run
SQL Behavior

Every run

CREATE OR REPLACE VIEW {table} AS ({sql})

When to use: Logical groupings, lightweight transformations, or when queries must always show the latest source data.

Mode Priority

  1. --full-refresh CLI flag → forces full-refresh for all models

  2. Model config (mode in front-matter or config())

  3. sling_build.yml defaults.mode

  4. Built-in default: full-refresh

Database Specifics

ClickHouse:

  • Sling adds ENGINE = MergeTree() and ORDER BY (from unique_key) to CREATE TABLE statements

  • The engine config option overrides the default ENGINE clause

  • Incremental mode always uses delete+insert, because ClickHouse does not support standard SQL MERGE. Any other strategy gives a warning

SQL Server family: uses SELECT * INTO ... FROM (...) instead of CTAS, and CREATE OR ALTER VIEW for views.

DuckDB family: these are single-writer databases, so Sling forces the thread count to 1.

Template Functions

ref()

Resolves a model or seed name to its full table name and creates a DAG dependency.

If stg_orders resolves to staging.stg_orders and country_codes is a seed at staging.country_codes, the compiled SQL becomes:

An unknown name gives the error ref('<name>'): model or seed not found in project.

src() / source()

References an external table that the build does not manage. This creates no DAG dependency.

this

Resolves to the model's own full table name. This is a variable, not a function.

is_incremental()

Returns true when all of these are true:

  1. The model's mode is incremental

  2. The target table already exists

  3. The --full-refresh flag is not set

On the first run the table does not exist, so is_incremental() returns false and the full dataset loads. On later runs, only new rows are selected.

incremental_where_cond()

A Jinja function that returns a WHERE clause body bounded by the model's update_key range. Sling-native incremental models use it in place of is_incremental(). On the first run, or with no state, it returns 1=1.

See Incremental for how Sling resolves the bounds and how to use the range: front-matter block.

incremental_value()

A Jinja function that returns the current lower-bound value as a SQL literal (for example '2024-01-01'), or null on the first run. Use it for non-WHERE expressions that need the watermark value directly.

@model_name Shorthand

Prefix a model or seed name with @ to resolve it to the full table name and add a DAG dependency. This is a shorter form of ref().

Sling ignores @@name (MySQL system variables), and leaves an unknown @name untouched, so SQL Server @variable syntax still works.

Bare Variable References

Model and seed names also work directly as variables. They resolve to the full table name but do not create an explicit DAG dependency. Auto-detection may still find them.

User Variables

Variables defined in sling_build.yml or passed with --vars are top-level template variables.

Use {{ my_var }} directly. There is no var() function.

Auto-Detection of References

Sling parses FROM and JOIN clauses in the compiled SQL to find references to other project models. It adds these detected dependencies to the DAG together with the explicit ref() calls.

For example, if your SQL has FROM staging.stg_orders and stg_orders is a model in the project, Sling adds it as a dependency even with no ref() call.

Auto-detection supplements ref() but does not replace it. Use ref(), because it makes sure table names resolve correctly in both dev and prod modes. Set rewrite: false on a model to turn off bare-name rewriting.

Multi-Statement SQL

A model file can hold more than one SQL statement, separated by semicolons. Sling splits them into:

  • Pre-statements — statements before the main SELECT query, for example creating temp tables

  • Model query — the single SELECT/WITH query used for materialization

  • Post-statements — statements after the model query, for example cleanup or GRANT

Sling uses SQL classification to find the model query. A file must have exactly one SELECT query. Zero or more than one gives an error.

Hooks

Models support start and end hooks that run before and after the model. They use the same hook types as replications and pipelines: query, log, check, http, shell, and others.

Declare hooks in YAML front-matter under the hooks key:

Execution Order

When a model has hooks, pre-statements, post-statements, and tests, the order is:

  1. Start hookshooks.start from front-matter

  2. Pre-statements — SQL statements before the model query

  3. Model query — the SELECT used for materialization

  4. Post-statements — SQL statements after the model query

  5. Data teststests from front-matter

  6. End hookshooks.end from front-matter

Available State Variables

Hooks can use these template variables:

Variable
Description

model.name

Model name, for example fct_orders

model.schema

Model schema, for example marts

model.full_name

Full table name, for example marts.core_fct_orders

model.mode

Execution mode, for example incremental

target.name

Target connection name

store.*

Shared key-value store for communication between hooks

timestamp.*

Date and time fields (YYYY, MM, DD, and others)

Data Tests

Declare data tests in the tests front-matter key. Each test compiles to a query that counts violating rows against the materialized table. A count above 0 fails the model, and its downstream models are skipped.

Test Types

Test
Syntax
Checks

not_null

not_null: id or not_null: [id, name]

No listed column is NULL

unique

unique: id or unique: [a, b]

The column set has no duplicate rows

accepted_values

accepted_values: {column: status, values: [a, b]}

Non-null values of the column are all in the list

expr

expr: sum(amount) >= 0

The SQL expression is true over the table

Running Tests Only

Use sling build test to run the data tests and skip materialization:

In this mode Sling walks the selected DAG, skips all seeds, and runs only the tests. Models with no tests are no-ops and report OK.

tests is a front-matter-only key. You cannot set it in a config() block.

Macros

Macros are reusable Jinja blocks that remove repeated SQL patterns across models. Define them in .macros.sql files. Sling makes them available to models by directory scope.

Defining Macros

Create a file that ends in .macros.sql with one or more {% macro %} definitions.

Sling treats files that end in .macros.sql as macro files, not models. It does not compile or execute them against the database.

Using Macros in Models

Call macros in any model SQL with standard Jinja syntax.

Compiles to:

Scoping Rules

Macros are scoped by directory. A macro file is available to models in the same directory and all child directories.

Macro File Location
Available To

Root-level, for example utils.macros.sql

All models in the project

staging/helpers.macros.sql

Models in staging/ and its subdirectories

marts/core/core_utils.macros.sql

Models in marts/core/ and its subdirectories

Macro Name Conflicts

When the same macro name exists at more than one scope level, the closest scope wins. Sling prints a warning in debug mode when it finds macro shadowing.

Sling loads all .macros.sql files in a directory. It concatenates them root-first, then by directory, then alphabetically by filename, for deterministic behavior.

Seeds

Seeds are static data files that Sling loads into the target database before models execute. Use them for reference data, lookup tables, and test fixtures.

Supported Formats

Format
Extension
Notes

CSV

.csv

Parsed with automatic type inference

JSON

.json

Nested structures are flattened

Parquet

.parquet

Schema kept from file metadata

Extension matching is not case-sensitive. Compressed and other formats, such as .csv.gz, .tsv, or .xlsx, are not supported as seeds.

Placement and Naming

Seed files follow the same naming rules as models. The first folder sets the schema. The file name is the table name.

Flat structure (default): seeds are together with models in schema folders.

DBT-compatible structure (dbt_project: true): seeds go in the seeds/ directory.

See Structure for the full naming rules.

Loading Behavior

  • Seeds always load in full-refresh mode. This ignores defaults.mode

  • Sling uses its task infrastructure to load them, so you get type inference, bulk loading, and all 30+ connectors

  • Seeds execute at depth 0 in the DAG, before any models

Referencing Seeds

Reference seeds in models with ref(), the same as any other model.

The ref('country_codes') resolves to staging.country_codes and adds a DAG dependency, so the seed loads before the model executes.

Skipping Seeds

Use --no-seeds to skip seed loading.

Seeds show as SKIP in the progress output. Downstream models still execute — a skipped seed does not cascade as a failure.

Seeds in the DAG

  • Seeds have no dependencies and are always at depth 0

  • More than one seed can execute in parallel when --threads is above 1

  • You can select and exclude seeds with --select and --exclude, the same as models

  • A tag: selector never matches a seed, because tags apply to models only

Complete Example

Last updated

Was this helpful?