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.
YAML Front-Matter (Recommended)
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:
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.
The config() block accepts a subset of the keys. You cannot set range, hooks, or tests in config() — use YAML front-matter for those. Unknown keys give a warning and are ignored. A schema set in config() also does not change the resolved table name, because Sling computes the table name before it renders the template. Use front-matter schema: instead.
Config Priority
YAML front-matter (
/** ... **/,-- {...}, or/* {...} */)Jinja
config()block (ignored if front-matter exists)sling_build.ymldefaultssectionBuilt-in default:
full-refresh
tags and hooks are additive: model values are added to the defaults values instead of replacing them.
Config Options
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
schema
string
(from folder)
Override the schema derived from the folder structure
enabled
bool
true
Set to false to remove this model from the DAG
engine
string
MergeTree()
ClickHouse ENGINE clause override
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
pre_hook and post_hook are not supported. Sling gives an error if it finds them. Use hooks.start and hooks.end instead.
Materialization Modes
Each model has a materialization mode that sets how Sling writes its SQL output to the target database.
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:
table
full-refresh
dbt compatibility
snapshot
append
Deprecated. Gives a warning
ephemeral
Not supported. Gives an error — use view or table
snapshot is deprecated and means append-only insert. Use append. A future release will make snapshot mean SCD2.
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.
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.
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.
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
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.
append
Appends the complete result set to the target table on every run. No deduplication.
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.
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.
If a model was a table before and you change its mode to view, Sling drops the existing table before it creates the view.
Mode Priority
--full-refreshCLI flag → forcesfull-refreshfor all modelsModel config (
modein front-matter orconfig())sling_build.ymldefaults.modeBuilt-in default:
full-refresh
Database Specifics
ClickHouse:
Sling adds
ENGINE = MergeTree()andORDER BY(fromunique_key) toCREATE TABLEstatementsThe
engineconfig option overrides the default ENGINE clauseIncremental 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:
The model's mode is
incrementalThe target table already exists
The
--full-refreshflag 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.
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.
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:
Start hooks —
hooks.startfrom front-matterPre-statements — SQL statements before the model query
Model query — the SELECT used for materialization
Post-statements — SQL statements after the model query
Data tests —
testsfrom front-matterEnd hooks —
hooks.endfrom front-matter
Available State Variables
Hooks can use these template variables:
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
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
The expr test injects your expression into the query without validation. Use it only with SQL you control.
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.
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.
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.
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
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.modeSling 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
--threadsis above 1You can select and exclude seeds with
--selectand--exclude, the same as modelsA
tag:selector never matches a seed, because tags apply to models only
Complete Example
Last updated
Was this helpful?