Rust-powered · Free and open source · Apache-2.0

Load data into your warehouse. Fast.

See the plan before any data moves.

When your files are already in a bucket or folder, the warehouse loads them itself, and no row passes through the loader. Everything else is staged as Parquet and bulk-loaded, not inserted row by row.

$ uv tool install sqlload
Read the docs View on GitHub
sqlload copy --no-color s3://your-bucket/sqlload/orders/ wh:SQLLOAD_RAW.ORDERS
Plan ready  1 selected, 2 objects to create, 1 to load

Objects (2)                                         sqlload apply
├── @"SQLLOAD_TEST"."SQLLOAD_RAW"."_SQLLOAD_STAGE"  create stage on s3://your-bucket/sqlload/
└── SQLLOAD_RAW.ORDERS                              create table
    ├── id            int             -> NUMBER(38,0)
    ├── customer_name text            -> VARCHAR
    ├── created_at    timestamp_tz    -> TIMESTAMP_TZ(9)
    ├── amount        decimal(12,2)   -> NUMBER(12,2)
    ├── is_active     bool            -> BOOLEAN
    ├── quantity      int             -> NUMBER(38,0)
    └── + _sqlload_source_file, _source_version, _source_row, _load_id, _loaded_at

Loads (1)                                           sqlload load
└── orders                                          replace
    ├── source  files  s3://your-bucket/sqlload/orders/  parquet
    ├── files   10 to read of 10 listed
    ├── route   Snowflake COPYs directly from s3://your-bucket/sqlload/orders/ (nothing through the loader)
    └── batch   1 batch of whole partitions, up to 1,000 files or about 1 GB per commit; …

✓ Plan complete  1 selected, 2 objects to create, 1 to load
created  @"SQLLOAD_TEST"."SQLLOAD_RAW"."_SQLLOAD_STAGE"  stage on s3://your-bucket/sqlload/
created  SQLLOAD_RAW.ORDERS                              11 columns
loaded   SQLLOAD_RAW.ORDERS                              10,000,000 rows  12.0s

✓ Completed  1 loaded, 10,000,000 rows, 19.6s

Faster than dlt and ingestr

How we measured →

S3 Parquet → Snowflake · 1M rows

sqlload
10.30 s
ingestr
19.47 s

Postgres → Snowflake · 1M rows

sqlload
14.56 s
ingestr
16.38 s
dlt
29.36 s

Parquet → DuckDB · 1M rows

sqlload
0.92 s
ingestr
2.12 s
dlt
138.45 s

Wall time, lower is faster. Median of 5 runs on one 4-vCPU machine, 6 November 2026: sqlload 0.11.1 and 0.11.0, ingestr 1.1.82, dlt 1.32.0 (its connectorx backend for Postgres). dlt was not run in the S3 scenario.

The loading framework you keep rebuilding

Hidden state drifts from reality

A stored copy of the schema or the cursor disagrees with the warehouse, and the fix is deleting state you can't see.

Types quietly change

Decimals lose digits, JSON turns into strings, and a stray value becomes a second column.

Rows go the long way round

Files already in a bucket are downloaded, parsed and uploaded again, when the warehouse could copy them itself.

You find out in production

Nothing shows which tables, columns and routes a run will touch until it has touched them.

Plan, apply, load

One sqlload.toml, three commands, and nothing kept outside your warehouse.

01 · Plan

See every change first

Every table, column, type mapping, write strategy, route and cursor, before anything runs. Problems come with the fix.

Reading a plan →

sqlload plan
Objects (2)                          sqlload apply
└── raw.app__orders                  create table
    ├── order_id    int           -> BIGINT
    ├── total       decimal(12,2) -> DECIMAL(12,2)
    └── updated_at  timestamp_tz  -> TIMESTAMP WITH TIME ZONE
…
Errors (1)
└── app__customers
    └── reason  no description; add description = "..."
                under [sources.app.tables."public.customers"]
02 · Load

Bulk copy, not row inserts

Files in S3 load into Snowflake straight from an external stage. Parquet loads into DuckDB through DuckDB's own reader. Rows are staged as Parquet and copied in bulk.

Load methods and routes →

sqlload load
…
└── events                     delete_insert (cursor: dt)
    ├── source  files  lake/events/  parquet
    ├── files   2 new of 3 listed
    └── route   DuckDB reads directly from lake/events/
…
loaded   raw.events      6 rows  0.0s
✓ Completed  2 loaded, 7 rows, 0.1s
03 · Stay stateless

The warehouse is the state

Incremental tables continue from the destination's own maximum. The schema is the live table. No local cache, no schema tables to fall out of sync.

Cursors and state →

sqlload load app__orders
└── app__orders                merge (unique_key: order_id)
    ├── source  postgres  app  public.orders
    └── cursor  updated_at from 2026-10-12 16:00:00+00
                (destination maximum)

loaded   raw.app__orders  3 rows  0.0s
✓ Completed  1 loaded, 3 rows, 0.1s
04 · Keep types exact

Declare what matters

Portable types, mapped per warehouse. Pin a column and SQLLoad widens the table in place; check declarations against live tables in CI.

Contracts and schema changes →

# sqlload.toml
[sources.app.tables."public.orders"]
columns = { total = "decimal(18,2)" }

sqlload plan app__orders
└── raw.app__orders            alter table
    └── widen  total decimal(12,2) -> decimal(18,2)  in place

sqlload contract diff
  OK    table:app__orders
0 contract difference(s)
05 · Write sources in Python

APIs without the plumbing

Yield dicts, Arrow data or JSON bytes. HTTP pagination, fan-out, retries and rate limits run in Rust.

Python sources →

import sqlload as sl

shop = sl.source("shop", connection="shop")
api = sl.http(shop, auth=sl.bearer(), rate_limit="10/s")

@shop.table(unique_key="order_id")
def orders(ctx):
    """Orders, one row per order."""
    return api.paginate("/orders", select="$.items",
                        paginator=sl.page_number("page"))

Each panel shows real sqlload output on a small orders example, cut down where marked ….

Start in a minute

Install it, then copy a folder or a Postgres table with one command. No configuration file needed.

  • Reads Postgres, including change data capture; Parquet, CSV, JSON and NDJSON in S3 or a folder; Python and HTTP APIs.
  • Writes Snowflake, Databricks, DuckDB, and Parquet files in S3 or a folder.
  • Linux and macOS; Python only for Python sources.
  • Free and open source, Apache-2.0.
uv tool install sqlload
sqlload copy 'postgresql://postgres:${ENV:PGPASSWORD}@localhost/postgres#public.orders' \
    duckdb://shop.duckdb#raw.orders --cursor-column updated_at
…
created  raw.orders  8 columns
loaded   raw.orders  1,001 rows  0.0s
✓ Completed  1 loaded, 1,001 rows, 0.1s

Load your first table

Follow the quickstart, or read how SQLLoad compares with dlt and ingestr.