Filesystem¶
omniload supports reading and writing different file formats from various local and remote filesystem types.
Filesystem handlers support globbing, gzip decompression, as well as format and reader hints.
Note
A concrete filesystem source path must match an existing file. If it does not,
the ingest fails during file discovery. A glob may match zero files and still
succeed. Supported glob syntax uses *, ?, and [...]; braces such as
{a,b} are treated as literal filename characters. This rule is based on
matched files, not loaded rows, so a parseable file with no data rows and an
incremental re-run with no new rows remain valid.
Supported formats¶
The file format is inferred from the file extension (with optional .gz
suffix), or by using a format hint if your resource
URI does not include file extensions.
Format |
Description |
Extensions |
Format hint |
Read |
Write |
|---|---|---|---|---|---|
Apache Avro object container files |
.avro |
#avro |
✅ |
❌ |
|
Binary JSON (MongoDB dump format) |
.bson |
#bson |
✅ |
❌ |
|
Concise Binary Object Representation (RFC 8949) |
.cbor |
#cbor |
✅ |
❌ |
|
Comma-separated values with a header row |
.csv |
#csv |
✅ |
✅ |
|
CSV (DuckDB) |
Comma-separated values read with DuckDB |
.csv_duckdb |
#csv_duckdb |
✅ |
❌ |
CSV (headless) |
Comma-separated values without a header row |
.csv |
#csv_headless |
✅ |
❌ |
Feather V2, the Apache Arrow IPC file format |
.feather, .arrow, .ipc |
#feather |
✅ |
✅ |
|
One JSON document: an object or an array |
.json |
#json |
✅ |
✅ |
|
Newline-delimited JSON |
.jsonl |
#jsonl |
✅ |
✅ |
|
Efficient binary serialization format |
.msgpack |
#msgpack |
✅ |
❌ |
|
OpenDocument spreadsheet format |
.ods |
#ods |
✅ |
❌ |
|
Apache ORC format |
.orc |
#orc |
✅ |
✅ |
|
Apache Parquet format |
.parquet |
#parquet |
✅ |
✅ |
|
Vortex high-performance columnar data format |
.vortex |
#vortex |
✅ |
✅ |
|
Excel spreadsheet format |
.xlsx |
#xlsx |
✅ |
✅ |
|
XML format |
.xml |
#xml |
✅ |
❌ |
|
YAML format |
.yaml, .yml |
#yaml |
✅ |
✅ |
Note
Supported formats for write operations are currently CSV, Feather, JSON, JSONL, ORC, Parquet, Vortex, XLSX, and YAML.
Supported filesystems¶
Name |
Description |
Protocol scheme |
Read |
Write |
|---|---|---|---|---|
Files on local and mounted filesystems |
file:// |
✅ |
✅ |
|
Azure Blob and Data Lake Storage |
az://, adls://, abfss:// |
✅ |
✅ |
|
Databricks files on volumes and workspaces |
dbfs:// |
✅ |
❌ |
|
Dropbox |
dropbox:// |
✅ |
❌ |
|
Amazon S3 and compatible filesystems |
s3:// |
✅ |
✅ |
|
File transfer protocol (FTP) |
ftp:// |
✅ |
❌ |
|
Google Cloud Storage |
gs:// |
✅ |
✅ |
|
Google Drive |
gdrive:// |
✅ |
❌ |
|
Hadoop distributed file system |
hdfs:// |
✅ |
❌ |
|
Files addressed by an HTTP or HTTPS URL |
http://, https:// |
✅ |
❌ |
|
Oracle Cloud Infrastructure Object Storage |
oci:// |
✅ |
❌ |
|
Microsoft OneDrive |
onedrive:// |
✅ |
❌ |
|
Alibaba Object Storage Service (OSS) |
oss:// |
✅ |
❌ |
|
Cloudflare R2 |
r2:// |
✅ |
❌ |
|
Microsoft SharePoint |
sharepoint:// |
✅ |
❌ |
|
SSH File Transfer Protocol |
sftp:// |
✅ |
❌ |
|
Web Distributed Authoring and Versioning (WebDAV) |
http+webdav://, https+webdav:// |
✅ |
❌ |
|
WebHDFS REST API for Hadoop |
webhdfs:// |
✅ |
❌ |
Note
omniload supports read and write operations on both local and remote filesystems.
For some filesystems, write support has not been unlocked yet, but we expect it to
land during the upcoming releases.
Incremental file selection¶
Filesystem sources append every matching file again on a normal re-run. Add
--filesystem-incremental to keep an mtime cursor and append rows only from new
or newly modified files:
omniload ingest \
--source-uri 'file:///data/events/*.jsonl' \
--dest-uri duckdb:///local.duckdb \
--dest-table 'public.events' \
--filesystem-incremental
This mode is opt-in and append-only. Its cursor needs durable dlt pipeline state:
the default temporary pipeline directory works when the destination supports
state sync, while other destinations require a stable --pipelines-dir. Files
at the current maximum modification time are deduplicated by their URL, and an
older-mtime backfill requires --full-refresh to reset the cursor.
For HTTP and HTTPS, each selected file must return a valid Last-Modified
header. An incremental directory-index glob makes one metadata request per
matched file to obtain it. A missing or malformed header stops extraction
instead of substituting the current time.
Note
--filesystem-incremental and --columns describe the reader, not the storage
service, so they apply to every scheme on this page and mean the same thing on
each. Everything else a source URI carries is that filesystem’s own connection
configuration.
The single-file csv:// and file:// destinations are rejected because they
replace their output using only the rows selected for the current run.
See Filesystem sources for the state, boundary, source-identity, and reset details.
Format hints¶
If a file is correctly encoded but has a non-standard extension, append a
#format fragment to tell omniload how to read it:
omniload ingest \
--source-uri 'file://data/event-data#jsonl' \
--source-table 'events' \
--dest-uri duckdb:///local.duckdb \
--dest-table 'public.events'
If the format hint is not one of the known formats, the path is preserved 1:1,
so file://data/vendor#1/report.csv reads the file
at data/vendor#1/report.csv as CSV.
Example: CSV files without headers
For CSV files without a header row, use the #csv_headless hint and optionally
supply column names with --columns:
omniload ingest \
--source-uri 'file://data/raw-data.csv#csv_headless' \
--source-table 'raw' \
--columns "id:bigint,name:text,value:double" \
--dest-uri duckdb:///local.duckdb \
--dest-table 'public.raw_data'
Without column names, columns are auto-named unknown_col_0, unknown_col_1,
and so on.
Reader hints¶
The URI fragment is also a channel to convey reader hints: Besides a #format
token, it can carry #key=value pairs that a reader may use to parametrize how a
file is read. For example, the spreadsheet reader takes a sheet_name parameter.
A format hint and named hints can coexist in one fragment, &-separated.
file://quotes.dat#xlsx&sheet_name=daily&header=0
The named-hint grammar:
Values are percent-decoded, so
#sheet_name=My%20Sheetgives the valueMy Sheetand#sheet_name=R%26DgivesR&D.Only the first
=splits key from value, so a value may itself contain=(#range=A1=B2givesrange=A1=B2).An empty value is preserved (
#sheet_name=givessheet_name=""); a reader decides whether that means “unset”.Duplicate keys take the last value (
#sheet_name=a&sheet_name=bgivesb).JSON arrays select several values when the reader accepts a list, for example
#sheet_name=["events","inventory"].If any segment of the fragment is neither a
key=valuepair nor a single known format, the whole#...is treated as a literal part of the path, so a real#in a filename keeps working. Percent-encode a literal#as%23when a trailingpath#key=valuewould otherwise be read as a fragment.
Note
Reader hints can be used to forward additional parameters as key=value pairs
to the underlying pipeline element implementation.
For example, CSV and XLSX readers forward corresponding parameters to
the polars.read_csv and polars.read_excel functions, and the XML
reader requires a #tagname=<row-tag> hint to define the repeated element
that represents one record / row.
Workbook tables¶
XLSX and ODS workbooks are multi-table sources. Without a worksheet selector, omniload reads every worksheet and uses each worksheet name as its destination table name. Empty worksheets are skipped.
--dest-table keeps its usual <dataset>.<table> syntax. For a plural workbook
load, the dataset component is used and the table component is a placeholder:
omniload ingest \
--source-uri 'file://data/workbook.xlsx' \
--dest-uri 'duckdb:///local.duckdb' \
--dest-table 'landing.workbook'
A worksheet named Quarterly Sales becomes quarterly_sales under the default
schema naming convention. With --schema-naming direct, its name remains
Quarterly Sales. Distinct worksheet names that resolve to the same table under
the active convention fail before loading, with both worksheet and workbook
names in the error.
When a glob matches several workbooks, worksheets with the same original name merge into one destination table. Different original names that collide after normalization fail instead of merging silently.
Select one worksheet with #sheet_name=<name> or #sheet_id=<number>. A single
selection keeps the existing one-table behavior and uses the table component of
--dest-table. Select several worksheets with a JSON array such as
#sheet_name=["events","inventory"] or #sheet_id=[1,2].
Note
The csv:// and file:// destinations each produce one output file, and the
cloud blob destinations currently address one table path. They reject a plural
workbook load. Select one worksheet or use a dataset-capable destination.
Database files¶
An object whose name carries a database extension is a database, not a file to
decode. omniload hands it to the SQL source instead of a reader, so
--source-table selects a table inside the database and the usual SQL
reflection, chunking, and type mapping apply:
omniload ingest \
--source-uri 's3://analytics/snapshots/events.duckdb?access_key_id=ACCESS&secret_access_key=SECRET' \
--source-table 'main.events' \
--dest-uri 'duckdb:///local.duckdb' \
--dest-table 'raw.events'
Extension |
Engine |
|---|---|
.db |
SQLite or DuckDB, read from header |
.ddb, .duckdb |
DuckDB |
.sqlite, .sqlite3 |
SQLite |
The engine is read from the file header, so a mislabeled object still loads. An
empty file falls back to its extension, and a .db file that is neither engine
reports both candidates.
Databases are read from s3://, r2://, gs://, az://, adls://, and
abfss://. Credentials and storage options are the same query parameters the
matching filesystem source takes. The URI names one
object: globs, # fragments, and credentials in the authority are rejected
rather than silently reinterpreted. The object path also has to ride
--source-uri, because --source-table names a table inside the database
rather than the object, unlike the split form the file sources accept.
Local databases need no filesystem source. Address them with
DuckDB or SQLite directly, as
duckdb:///path/to/events.duckdb.
Note
The object is downloaded whole into a run-scoped temporary directory before it is opened, so the database must fit on local disk. The copy is removed after success and after failure, and changes to it are never written back. Remote databases are sources only.
File format routing¶
omniload reads each file format through the best available path rather than one generic reader. This section explains how that routing works, so the individual per-format pages (Avro, BSON, CBOR, Feather, MessagePack, ORC, Parquet, XML, YAML) can stay focused on how to use each format.
In general, omniload builds mostly upon the excellent fsspec, polars and iterabledata packages for local and remote filesystem access and format decoding.
Format |
Library |
Description |
|---|---|---|
CSV ( |
|
Built-ins. |
CSV ( |
|
DuckDB-backed CSV reader. |
Avro |
|
Whole-file format. |
BSON |
Dedicated in-tree codec |
Needs extended-type normalization. |
CBOR |
|
Whole-file format. |
Feather |
|
Batched reader and writer. |
JSON |
|
Whole-document parse. |
MessagePack |
|
Streamed record-by-record. |
ODS |
|
Whole-file format. |
ORC |
|
Striped reader and writer. |
Parquet |
|
Batched reader. |
Vortex |
|
Batched reader and writer. |
XML |
|
Whole-file parse, hardened. |
XLSX |
|
Whole-file reader, row writer. |
YAML |
|
Whole-file decode, safe. |
omniload uses the iterabledata package for reading or decoding a few formats
not covered by native reader implementations. Install the iterable extra to
make them available to your environment.
pip install 'omniload[iterable]'
iterabledata exposes a uniform per-format class that yields record dicts from a file object. When routing to iterabledata, omniload feeds it the source’s fsspec filesystem handle that is already authenticated, so remote filesystem access to Amazon S3, Azure Blob Storage, Google Cloud Storage, or SFTP works transparently.
Where using iterabledata is not applicable, for example to enhance error handling, or applying stronger security policies, omniload directly uses relevant low-level decoder libraries.
Vortex needs the vortex extra, which requires Python 3.11 or newer.
pip install 'omniload[vortex]'
omniload installs the BSON, DuckDB CSV and spreadsheet decoders itself. Installing
the filesystem package on its own, as dlt-filesystem, leaves them to its bson,
duckdb and spreadsheet extras.
File format notes¶
Performance¶
Where available, omniload uses Polars to read and decode files from local and remote filesystems. Polars builds upon Apache Arrow and is written in Rust. This guarantees robustness and speed.
Whole-file decode¶
Some formats are whole-file rather than streaming. For those, omniload decodes the bytes with the format’s own library directly.
JSON and JSONL¶
.json and .jsonl are separate formats. .jsonl is always read as one record
per line. .json is read as one document: an object loads as a single row, an
array loads as one row per element, and indentation makes no difference. A
.json file that turns out to hold line-delimited records still loads, because
records are the fallback once the document does not parse as one value.
Note
Both are available for write operations, and each writes the shape its reader
expects: .jsonl one record per line, .json a single array document.
Streaming¶
Records are pulled in batches until the reader signals end-of-file, and flushed per file so a multi-file glob never drops a partial final chunk. iterabledata rewinds the handle on construction, which fails on a non-seekable stream (a pipe, some compressed or SFTP handles); such a stream is spooled into memory first, while a seekable handle streams straight through.
Type normalization¶
Binary formats carry types JSON does not implement, for example raw bytes,
timestamps, tagged values. The decoders hand some of those back as native
Python objects that a text or Parquet loader can not serialize. omniload
normalizes rows to portable values before handing data to the loader.
bytesbecomes a base64-encoded string, which is portable across text loaders and Parquet alike. This covers CBOR / MessagePack binary value types and YAML!!binaryvalues.An unknown CBOR tag becomes a plain
{"tag": ..., "value": ...}object rather than crashing the load.A MessagePack
Timestampextension becomes a UTC datetime.XML doesn’t need any normalization: Its values are all strings or nested objects/lists, which every loader handles equally well.
A YAML
!!settype becomes a list.
Some values are made portable by the decoder itself rather than by omniload:
cbor2 decodes the standard CBOR tags (datetime, big integers, decimals)
into native Python types directly.
Handed one of these Python values directly, a format with no column type of its own for
it writes it as text rather than refusing it, so a decimal lands in a JSON, JSONL, YAML
or CSV file as 1.50, with the scale a float would drop, while Parquet carries it as a
decimal column. Nested maps and arrays are handled recursively. The exact per-format
mapping is on each format’s own documentation page under “Extended-type handling”, for
that format’s decoder or its writer as the format allows; several are read-only.
Whether a writer is handed such a value at all is a separate question, decided by the staging format rather than by the destination: see What a load delivers. Under the default staging a decimal reaches every writer as a string, so it is written as text even where the format could hold a decimal column.
CSV and XLSX are the write formats with neither a nested nor a binary column type. A
struct, a list or a bytes value is written there the way the JSON writers write it:
JSON text for a document, a base64 string for binary. So a nested source exports to
either rather than failing, and the cell holds something a reader can parse.
What a load delivers¶
An ingest is not the reader and writer back to back: dlt stages the rows between them, and the staging format decides what a writer receives. So the values that reach a destination file are chosen by the staging format, not by how much the destination format can hold.
The table below is for a local file:// destination, which stages gzip JSONL
by default. A warehouse destination that supports Parquet staging, DuckDB and
BigQuery among them, selects it automatically from a filesystem source, so such
a load follows the right-hand column without being asked.
Source column |
Default staging (gzip JSONL) |
|
|---|---|---|
integer, float, boolean, string |
itself |
itself |
date, timestamp, time, binary, decimal |
ISO or base64 string |
itself, retyped by dlt’s schema |
list, struct |
itself |
JSON string |
all-null column |
dropped |
dropped |
duration |
load fails |
load fails |
“Itself” describes what staging hands the writer, not what the file ends up
holding, and it carries one qualification: dlt infers a schema rather than
copying the source’s, so a string shaped like a timestamp is retyped as one.
2026-09-01T12:00:00 reaches a default-staged load as the text
2026-09-01T12:00:00+00:00, and a Parquet-staged one as a UTC timestamp column.
Warning
ORC cannot store a time column at all. A source carrying one loads to .orc
under the default staging, where the column is text by then, and fails under
Parquet staging with Unknown or unsupported Arrow type: time64[us].
Two of those rows are worth spelling out.
A duration column cannot be loaded at all. dlt’s extract step serializes
rows as JSON and refuses a Timedelta, so the run fails with
Type is not JSON serializable: Timedelta before any writer sees it. Cast such
a column in the source query if you need it.
A column that is null in every row does not reach the output. dlt omits a null key per row, so a wholly null column has no keys anywhere and the writer never learns it existed.
Under Parquet staging dlt applies its own schema rather than the source’s, so a
naive timestamp arrives as UTC and a decimal is converted to dlt’s default
scale of 9. That conversion has to be lossless rather than rounding: it
fails the load with Rescaling Decimal value would cause data loss where it
would discard a nonzero fractional digit. 3.14 converts, and so does a value
padded with trailing zeros past scale 9; 3.1400000001 does not. The precision the
destination then records is the writer’s choice, not dlt’s: a source
decimal128(38, 2) carrying 3.14 lands as decimal128(10, 9) in Feather and
ORC, and as decimal128(38, 9) in Parquet.
Use the default staging when you want the text form, and Parquet staging when
you want typed columns.
Integrity and truncation¶
The read mechanism determines how a damaged file behaves, and it is worth knowing which guarantee you get.
Empty files raise reader errors for Avro, CSV, Feather, ORC, and Parquet. Empty JSON and JSONL files yield no rows.
Whole-file decode (CBOR, XML, YAML) raises on a corrupt or malformed file rather than loading partial data. CBOR additionally must be a single top-level value; files that concatenate several top-level objects are read only up to the first, a decoder limitation that cannot be detected at read time. XML additionally rejects an entity-expansion bomb and a mismatched encoding declaration.
Block-structured formats (Avro) carry no trailing index or record count, so a tail cut exactly at a block boundary leaves a shorter valid file that nothing can distinguish from the original: the surviving blocks load and nothing is reported. A cut one byte into the next block’s header is detectable corruption and is nonetheless accepted the same way, which is a limitation of the reader rather than of the format; two or more bytes in does raise. Feather, ORC and Parquet each carry a footer that a truncation destroys, so they raise on the same damage.
Streaming formats (MessagePack) carry no length prefix, so a truncated tail reads as a clean end-of-file: the partial trailing record, and anything after a mid-stream corruption, are dropped silently. Validate file integrity upstream if partial loads would be a problem.