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.
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.
See every change first
Every table, column, type mapping, write strategy, route and cursor, before anything runs. Problems come with the fix.
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"]
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.
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
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.
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
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.
# 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)
APIs without the plumbing
Yield dicts, Arrow data or JSON bytes. HTTP pagination, fan-out, retries and rate limits run in Rust.
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.