faucet-source-sqlite
A SQLite query source for the faucet-stream ecosystem. Executes a SQL statement against a SQLite database (a file or an in-memory database), decodes each row into a typed serde_json::Value, and streams the result set back page-by-page via a sqlx row cursor so memory stays bounded no matter how large the table is.
SQLite is an in-process, file-based engine — there is no server, no network wire, and no auth. That makes this connector the simplest, lowest-overhead way to pull rows out of a local database and land them in any faucet-stream sink (a file, another database, a warehouse, a queue) with one declarative config and no glue code. It's also ideal for CI and locked-down environments where you don't want to stand up a server.
Feature highlights
- Native cursor streaming — overrides
Source::stream_pagesto drive a sqlx row cursor (Query::fetch) without buffering the whole result. Rows are accumulated into abatch_sizebuffer and yielded as aStreamPageas soon as it fills, so peak client-side memory isO(batch_size)and the sink can start writing while the rest of the table is still being read off disk. batch_size: 0"no batching" sentinel — drain the entire cursor into one page for small lookup tables or for sinks that prefer one large request to many small ones.- Dynamic-type aware decoding — SQLite's storage classes (INTEGER, REAL, TEXT, BLOB, NULL) are probed in order of specificity; TEXT that parses as JSON is returned as a native JSON value, BLOBs are base64-encoded so binary survives the round-trip.
- Connection pooling — a
sqlx::SqlitePoolis built once innew()and reused for every query; pool size is configurable. - Safe parameter binding —
{field}placeholders in the query are bound from the matrix / parent-record context as positional?parameters, so values are never string-interpolated into SQL (no injection). - File or in-memory — point at
sqlite:data.db, an absolute path, orsqlite::memory:.
Installation
# As a library:
# In the CLI (opt-in connector feature):
Or via the umbrella crate:
Quick start
# pipeline.yaml — faucet run pipeline.yaml
version: 1
name: sqlite_to_jsonl
pipeline:
source:
type: sqlite
config:
database_url: sqlite:./app.db
query: SELECT * FROM events ORDER BY ts
sink:
type: jsonl
config:
path: events.jsonl
Configuration reference
All fields live under pipeline.source.config.
| Field | Type | Default | Description |
|---|---|---|---|
database_url |
string | — (required) | SQLite database URL. A file path ("sqlite:data.db", "sqlite://./data/app.db", "sqlite:/abs/path.db") or in-memory ("sqlite::memory:"). |
query |
string | — (required) | SQL to execute. May contain {field} placeholders that are bound from the matrix / parent-record context as positional ? parameters at runtime. |
max_connections |
int (u32) |
10 |
Maximum connections in the sqlx pool. SQLite is single-writer; a small pool (2–5) is usually plenty for a read-only source. |
batch_size |
int (usize) |
1000 |
Rows per emitted StreamPage. 0 = no batching: the cursor is fully drained and the entire result set is emitted in a single page. Values above MAX_BATCH_SIZE (1,000,000) are rejected at construction. |
There is no auth, TLS, or credential configuration — SQLite is a local in-process engine.
Examples
Read a table into CSV (no network required)
version: 1
name: sqlite_to_csv
pipeline:
source:
type: sqlite
config:
database_url: sqlite://./data/app.db
query: SELECT id, email, created_at FROM users ORDER BY id
sink:
type: csv
config:
path: ./out/users.csv
delimiter: 44 # ','
write_headers: true
Project a JSON column out of a row
json_extract is a SQLite built-in; the extracted TEXT is returned as a native JSON value when it parses.
source:
type: sqlite
config:
database_url: sqlite:/var/data/app.db
query: |
SELECT id, name, created_at,
json_extract(metadata, '$.tags') AS tags
FROM items
WHERE active = 1
batch_size: 5000
Whole-table export with no re-chunking
source:
type: sqlite
config:
database_url: sqlite:analytics.db
query: SELECT * FROM daily_snapshot
batch_size: 0 # emit the entire result set as one page
Per-parent query in a matrix fan-out
A child matrix row can substitute values from each parent record into the query with {field} placeholders. Each value is bound as a positional parameter, so injection is impossible.
matrix:
- id: tenants
source:
ref: tenant_list
- id: rows
parent: tenants
source:
type: sqlite
config:
database_url: sqlite:tenants.db
query: SELECT * FROM records WHERE tenant_id = {tenants.id}
Streaming & batching
SqliteSource::stream_pages drives a sqlx row cursor (Query::fetch) without buffering the full result. Rows are accumulated into a batch_size buffer and yielded as a StreamPage once the buffer fills; the trailing partial page (if any) is yielded after the cursor drains. The pipeline writes each page to the sink as it arrives, so memory is bounded at O(batch_size) on both sides.
batch_size = 0 is the "no batching" sentinel — the cursor is drained completely and the entire result set is emitted in a single StreamPage. Use it for small lookup tables, or for downstream sinks (SQL COPY, BigQuery load jobs, Snowflake stage uploads) that prefer one large request to many small ones. Values larger than MAX_BATCH_SIZE (1,000,000) are rejected by faucet_core::validate_batch_size at construction time.
Note — SQLite is an in-process, file-based engine: there is no server-side cursor concept and no network wire to worry about. The streaming implementation bounds client-side memory and lets the sink begin writing as soon as the first batch is parsed off disk, rather than waiting for the whole result set to materialise in a
Vec.
The batch_size argument passed to stream_pages by the pipeline is informational — the source always uses its own config.batch_size so a pipeline-supplied hint cannot silently override an explicit config value.
No incremental / resume support
This is a one-shot query source. It does not implement bookmark-based resume, incremental replication, or exactly-once delivery: every emitted page carries bookmark: None, and each run re-executes the full query. For incremental loads, encode the watermark directly in the query (e.g. WHERE updated_at > '2026-01-01') and drive it from a matrix context or a ${now.*} token. If you need change-data-capture, use a CDC source (postgres-cdc / mysql-cdc / mongodb-cdc) instead.
Supported column types
SQLite has dynamic typing — values are stored as INTEGER, REAL, TEXT, BLOB, or NULL. The source probes each column value in order of specificity:
| SQLite storage class | JSON type |
|---|---|
| TEXT (valid JSON) | native JSON value |
| TEXT | string |
INTEGER (i64) |
number |
INTEGER (i32) |
number |
REAL (f64) |
number |
| BOOLEAN | boolean |
| BLOB | string (base64) |
| NULL / unsupported | null |
SQLite has no native datetime / UUID / decimal types — those are stored as TEXT/INTEGER/REAL and surface accordingly. A non-finite f64 (NaN / infinity) cannot be represented in JSON and decodes to null.
Config loading
Load from YAML/JSON, environment variables, or a .env file via the helpers in faucet_core::config:
use ;
use SqliteSourceConfig;
let config: SqliteSourceConfig = load_json?;
let config: SqliteSourceConfig = load_env_file?;
Example JSON config
Example .env file
SQLITE_SOURCE_DATABASE_URL=sqlite:data.db
SQLITE_SOURCE_QUERY=SELECT * FROM events
SQLITE_SOURCE_MAX_CONNECTIONS=10
SQLITE_SOURCE_BATCH_SIZE=1000
Schema introspection
Inspect the full JSON Schema for this connector's config with:
Programmatically, every Source exposes config_schema():
use Source;
let source = new.await?;
let schema = source.config_schema;
println!;
Library usage
use Source;
use ;
# async
To wire it into a pipeline with a sink, build a faucet_core::Pipeline (or call run_stream) over this source and any Sink — the pipeline drives stream_pages and writes each page as it arrives.
How it works
new()validatesbatch_sizeand builds asqlx::SqlitePool(SqlitePoolOptions::max_connections) once, reusing it for every query. A connection failure surfaces asFaucetError::Config.stream_pagesresolves any{field}context placeholders into positional?bind markers, binds the values by JSON type (string / integer / float / bool / null — integers up tou64::MAXbind exactly, with nof64precision loss), and opens a streaming cursor withQuery::fetch.- Rows are decoded one at a time (
row_to_json), buffered tobatch_size, and yielded asStreamPages; the final partial buffer is flushed at the end. - Each column value is decoded by probing storage classes in specificity order (see Supported column types).
Lineage dataset URI
sqlite://<path>?query=<sql> — e.g. sqlite:///var/db/app.db?query=SELECT id FROM events. The sqlite:// / sqlite: scheme prefix is stripped from database_url before the path is rendered.
Feature flags
This crate has no optional features of its own. Enable it in the CLI / umbrella crate via the source-sqlite feature. It is not in the CLI default build — opt in with cargo install faucet-cli --features source-sqlite.
Troubleshooting / FAQ
| Symptom | Likely cause & fix |
|---|---|
FaucetError::Config: SQLite connection failed at startup |
The database file doesn't exist or the path is wrong. sqlx does not create the file for a read-only URL — point at an existing .db, or use sqlite:data.db?mode=rwc to create it. Check the path is relative to the process working directory. |
unable to open database file |
The directory doesn't exist or the process lacks read permission on the file. Verify the path and file permissions. |
FaucetError::Config: SQLite query failed |
A SQL syntax error or a reference to a missing table/column. Run the query directly with the sqlite3 CLI to confirm it's valid. |
| In-memory DB looks empty / each query sees a fresh database | sqlite::memory: databases are per-connection. With a pool of more than one connection, different queries may hit different empty databases — use max_connections: 1 for :memory:, or a real file. |
A {placeholder} in the query wasn't substituted |
{field} substitution only happens when a matrix / parent-record context is present. For a non-matrix run there is no context, so the literal query is used as-is. Encode static filters as literals or bind via a matrix parent. |
database is locked errors under concurrent writes |
Another process holds a write lock. SQLite is single-writer; lower max_connections, or enable WAL mode on the database (PRAGMA journal_mode=WAL;) out of band. |
| A BLOB column arrives as a base64 string | Intentional — binary data is base64-encoded so it survives the JSON round-trip. Decode it in a downstream transform if you need the raw bytes. |
batch_size rejected at startup |
batch_size exceeds MAX_BATCH_SIZE (1,000,000). Lower it, or use 0 for the no-batching sentinel. |
| Need only changed rows, not the whole table | This source has no incremental/CDC mode. Add a WHERE watermark to the query (driven by ${now.*} or a matrix context), or use a CDC source. |
See also
- Connector reference & capability matrix
- CLI & config-file grammar
- State & resume cookbook
faucet-sink-sqlite— the matching SQLite sinkfaucet-source-postgres·faucet-source-mysql— server-based SQL sources
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.