Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bc2937a
docs: add spec 34 — Polars engine conversion design
joefrost01 Jun 6, 2026
7e30865
docs: strengthen spec 34 motivation with runtime-extension constraint
joefrost01 Jun 6, 2026
c48f6db
docs: add Phase 1 implementation plan for Polars engine core
joefrost01 Jun 6, 2026
f1463ba
feat: scaffold PolarsEngine module alongside DuckDB engine
joefrost01 Jun 6, 2026
ed32f36
feat: PolarsEngine CSV scan, collect, row_count
joefrost01 Jun 6, 2026
09e235f
feat: PolarsEngine NDJSON scan
joefrost01 Jun 6, 2026
4a46c89
feat: PolarsEngine CSV writer with file/stdout sink
joefrost01 Jun 6, 2026
bb67cc5
feat: PolarsEngine Parquet scan and writer
joefrost01 Jun 6, 2026
f2cff76
feat: PolarsEngine NDJSON writer
joefrost01 Jun 6, 2026
cf6390a
feat: gzip/zstd compression for CSV/NDJSON output
joefrost01 Jun 6, 2026
0b7029a
fix: finalize compression frames explicitly to surface write errors
joefrost01 Jun 6, 2026
6e8aa7d
feat: PolarsEngine Excel scan via calamine
joefrost01 Jun 6, 2026
f69b629
test: make Excel scan test self-contained (testdata is gitignored)
joefrost01 Jun 6, 2026
2e85677
feat: PolarsEngine union-by-name concat (schema evolution)
joefrost01 Jun 6, 2026
19b910c
feat: PolarsEngine SQL execution via SQLContext
joefrost01 Jun 6, 2026
c901e41
feat: PolarsEngine lazy schema introspection
joefrost01 Jun 6, 2026
d1475d8
test: malformed input surfaces a clear error, never hangs
joefrost01 Jun 6, 2026
2685f08
test: cloud paths rejected with clear deferred-feature error
joefrost01 Jun 6, 2026
96f407b
style: cargo fmt after Phase 1
joefrost01 Jun 6, 2026
90cd7c2
harden: error on over-wide Excel rows; cover stdout/ndjson-gzip/missi…
joefrost01 Jun 6, 2026
8afd7f4
Merge branch 'worktree-polars-engine-core'
joefrost01 Jun 6, 2026
465e296
docs: add Phase 2 implementation plan for Polars engine cutover
joefrost01 Jun 6, 2026
8ccf97d
refactor: move shared format/schema types to types.rs (decouple from …
joefrost01 Jun 6, 2026
8ae8c76
feat: masking as native Polars DataFrame transform
joefrost01 Jun 6, 2026
0b04b43
feat: lineage columns as native Polars DataFrame transforms
joefrost01 Jun 6, 2026
62e476a
feat: explicit-schema coercion as Polars cast/project with type map
joefrost01 Jun 6, 2026
490f303
feat: reference tables loaded as Polars LazyFrames
joefrost01 Jun 6, 2026
722d8f2
feat: profiler computes statistics from a Polars DataFrame
joefrost01 Jun 6, 2026
8057e70
feat: crypto decrypt/encrypt/discover operate on Polars DataFrames
joefrost01 Jun 6, 2026
3233caa
fix: profiler distinct_count excludes nulls; schema match case-insens…
joefrost01 Jun 7, 2026
d837761
feat: output writer emits a Polars DataFrame via PolarsEngine
joefrost01 Jun 7, 2026
f19ee6f
feat: query pipeline runs on PolarsEngine end-to-end
joefrost01 Jun 7, 2026
e0dc2c1
feat: inspect and profile commands run on PolarsEngine
joefrost01 Jun 7, 2026
562eb67
feat: defer cloud in file resolution and fingerprint with clear errors
joefrost01 Jun 7, 2026
213d42b
refactor: remove DuckDB engine, dependency, and dead SQL helpers
joefrost01 Jun 7, 2026
715ae05
chore: drop accidentally committed .DS_Store
joefrost01 Jun 7, 2026
acfcc39
docs: update for Polars engine, SQL limitations, and cloud deferral
joefrost01 Jun 7, 2026
b97d9f1
Merge branch 'main' into remove-duck-db
joefrost01 Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
792 changes: 31 additions & 761 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
thiserror = "2"
duckdb = { version = "1", features = ["bundled"] }
glob = "0.3"
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"
Expand Down
81 changes: 46 additions & 35 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ A Rust CLI tool for data engineers to query, profile, and transform data across

## Core Engine

**DuckDB** (via `duckdb` Rust crate) — provides SQL execution, format readers (Parquet, CSV, NDJSON, Excel), glob support, cloud storage (S3/GCS/Azure via httpfs/azure extensions), temp tables, hashing, and UUID generation.
**Polars** (pure Rust, `polars` crate ~0.54.x) — provides SQL execution via `SQLContext`, format readers (Parquet, CSV/TSV, NDJSON, and Excel via the pure-Rust `calamine` backend), glob resolution, type-safe lazy evaluation, and the `concat_lf_diagonal` union-by-name schema evolution. There is no bundled C++ toolchain and no runtime extension downloads.

**Cloud storage** (S3/GCS/Azure) is **deferred in this build**: cloud paths (`s3://`, `gs://`, `az://`) return a clear `"cloud storage … is not supported in this build yet"` error. The cloud CLI flags (`--s3-region`, `--s3-profile`, `--gcs-project`, `--azure-account`) still parse for forward compatibility.

### SQL Surface Limitations

User-facing SQL (`--where`, `--filter-sql`, `--post-sql`, reference-table JOINs) runs on Polars `SQLContext`. Compared to DuckDB, the SQL surface is narrower:

- **`DELETE` / `UPDATE` statements are treated as transforms**, not mutations. For example, `DELETE FROM _ WHERE x` drops matching rows and returns a result set rather than raising an error. Do not rely on DML semantics in `--post-sql`.
- **Window functions** (`OVER (PARTITION BY … ORDER BY …)`) have known correctness issues in Polars SQL. Avoid them; dtoo does not attempt to detect or warn about their use.
- **Narrower function library**: some exotic DuckDB date, regex, and string functions are absent. Unsupported SQL returns a clear error (it never silently hangs — which was the motivation for the migration).
- Errors are always explicit `Result` values; the engine never hangs on malformed input.

---

Expand Down Expand Up @@ -36,9 +47,9 @@ Execution order within `dtoo query`:
1. Resolve file list (glob pattern / pipe input / explicit paths)
2. Apply --exclude patterns to filter file list
3. If --dry-run: display plan and exit
4. Init DuckDB in-memory, load extensions
5. Load reference tables into named DuckDB tables
6. Create temp_results table (schema from first file, or explicit schema)
4. Init Polars engine; scan reference table files into named LazyFrames
5. Scan each input file as a LazyFrame; register refs and _ in SQLContext
6. Create accumulated result (LazyFrame; schema from first file or explicit --schema)
7. For each file:
a. Register file as `_` (the magic table name)
b. Apply --where clause if specified: SELECT * FROM _ WHERE {where}
Expand Down Expand Up @@ -173,11 +184,11 @@ dtoo fingerprint <PATH>

Files are resolved from one of three sources (mutually exclusive):

1. **--glob**: DuckDB-compatible glob pattern. Supports `**` for recursive matching.
1. **--glob**: Glob pattern. Supports `**` for recursive matching (resolved by Polars native glob scan).
2. **--pipe file**: Newline-delimited file paths from stdin.
3. **--pipe data**: Raw data stream from stdin (requires `--stdin-format`).

Cloud paths (s3://, gs://, az://) are supported in all modes.
Cloud paths (`s3://`, `gs://`, `az://`) are **not supported in this build** — they return an explicit error. See Core Engine above.

Format is auto-detected from file extension:
- `.parquet` — Parquet
Expand All @@ -190,6 +201,8 @@ Format is auto-detected from file extension:
2. **`--sheet` flag**: applies to all `.xlsx` files matched by glob or pipe
3. **Default**: first sheet

**Excel reading behavior (calamine):** Every cell is read as a string; type inference is deferred to downstream SQL casts or explicit `--schema`. A data row wider than the header row is an error (no silent data loss).

Examples:
```bash
# All xlsx files, same sheet
Expand All @@ -205,11 +218,12 @@ find . -name "*.xlsx" | dtoo query --pipe file --sheet "Data"

### Schema Handling

**Default (no --schema):** Union-by-name with type promotion. The temp_results table schema evolves as new columns are encountered. DuckDB handles type promotion (e.g., INT -> BIGINT -> DOUBLE).
**Default (no --schema):** Union-by-name with type promotion. The accumulated result schema evolves as new columns are encountered. Polars `concat_lf_diagonal` handles type promotion (e.g., Int32 → Int64 → Float64) and fills missing columns with `null`.

**Explicit (--schema):** Schema file defines the target columns and types. Files are coerced to match. Extra columns in source files are ignored; missing columns become NULL.

Schema file format (YAML):
Schema file format (YAML). Type strings use DuckDB-style names (e.g. `INTEGER`, `VARCHAR`, `DECIMAL(10,2)`, `TIMESTAMP`) which are mapped to Polars dtypes at load time. Bare `DECIMAL` defaults to `DECIMAL(18,3)`.

```yaml
columns:
- name: id
Expand All @@ -226,12 +240,11 @@ columns:

When both are specified, `--where` is applied first as a pre-filter, then `--filter-sql` operates on the result:

```sql
```
-- Internal execution when both specified:
-- Step 1: Apply --where
CREATE TEMP VIEW _pre AS SELECT * FROM read_parquet('{file}') WHERE {where_clause};
-- Step 2: Apply --filter-sql (user's SQL, _ now points to _pre)
INSERT INTO temp_results SELECT ... FROM _pre ...;
-- Step 1: Scan file → LazyFrame; register as _; execute: SELECT * FROM _ WHERE {where_clause}
-- Step 2: Apply --filter-sql against the filtered LazyFrame (user's SELECT from _)
-- Step 3: Concatenate result into the accumulated LazyFrame (union-by-name)
```

When only `--where` is specified, it's equivalent to `--filter-sql "SELECT * FROM _ WHERE {clause}"`.
Expand All @@ -240,7 +253,7 @@ When only `--filter-sql` is specified, it runs directly against the file.

### Reference Tables

Loaded once at startup into named DuckDB tables:
Loaded once at startup into named LazyFrames registered in the `SQLContext`:

```
--ref regions=ref/regions.parquet --ref products=lookups/products.csv
Expand Down Expand Up @@ -281,7 +294,7 @@ NULL values remain NULL (not masked).

### Profiling

Uses DuckDB's analytical capabilities to produce per-column statistics:
Produces per-column statistics using Polars aggregate expressions:

| Metric | Applies To |
|--------|-----------|
Expand Down Expand Up @@ -533,22 +546,23 @@ CLI flags override config file values. Config file can be combined with CLI flag

| Crate | Purpose |
|-------|---------|
| `duckdb` | Core engine, SQL execution, file readers |
| `polars` | Core engine: SQL execution (`SQLContext`), lazy evaluation, format readers (Parquet, CSV, NDJSON) |
| `calamine` | Excel reader (pure Rust; bundled via Polars `excel` feature) |
| `flate2` | gzip output wrapping for CSV/NDJSON (Polars has no native text-output compression) |
| `zstd` | zstd output wrapping for CSV/NDJSON |
| `clap` | CLI argument parsing (derive API) |
| `serde` + `serde_yaml` | Config file and schema parsing |
| `uuid` | Lineage UUID generation |
| `sha2` + `hmac` | Column masking and file fingerprinting |
| `chrono` | Timestamp handling |
| `glob` | File pattern matching (fallback for non-DuckDB resolution) |
| `indicatif` | Progress bars for file processing |
| `comfy-table` | Terminal table output for inspect |
| `tokio` | Async runtime (needed for cloud storage operations) |

---

## v1.0 Scope (Open Source)

Everything described above, including cloud storage (S3, GCS, Azure) since DuckDB extensions make this nearly free.
Everything described above. **Cloud storage (S3, GCS, Azure) is deferred** in the current build — cloud paths return an explicit error. Cloud support will be re-enabled in a follow-up once the Polars engine is proven in local-file deployments.

---

Expand All @@ -560,7 +574,7 @@ Everything described above, including cloud storage (S3, GCS, Azure) since DuckD
| Core query/transform | Yes | Yes | Yes |
| Profile/inspect/fingerprint | Yes | Yes | Yes |
| Lineage, masking, pipe mode | Yes | Yes | Yes |
| Cloud storage (S3/GCS/Azure) | Yes | Yes | Yes |
| Cloud storage (S3/GCS/Azure) | Deferred | Deferred | Deferred |
| Config files, manifests | Yes | Yes | Yes |
| Data quality assertions | - | Yes | Yes |
| Incremental processing | - | Yes | Yes |
Expand Down Expand Up @@ -639,8 +653,7 @@ overall pass/fail status. Exit code 4 on assertion failure.
| `referential_integrity` | All values exist in a reference table column |
| `custom_sql` | User-provided SQL returning violation rows |

Implementation: each rule translates to a DuckDB query against the temp_results table. Runs
after post-sql but before output. `custom_sql` allows arbitrary validation:
Implementation: each rule translates to a query (via Polars `SQLContext`) against the accumulated result. Runs after post-sql but before output. `custom_sql` allows arbitrary validation:

```yaml
- rule: custom_sql
Expand Down Expand Up @@ -765,14 +778,14 @@ dtoo query --glob "data/**/*.parquet" \
```

**Supported databases:**
| Database | Connection String | DuckDB Support |
|----------|------------------|----------------|
| PostgreSQL | `postgres://...` | Native (postgres_scanner) |
| MySQL | `mysql://...` | Native (mysql_scanner) |
| SQLite | `sqlite:///path/to/db` | Native (sqlite_scanner) |
| Snowflake | `snowflake://account/db/schema?table=t` | Via ADBC driver |
| BigQuery | `bigquery://project/dataset?table=t` | Via ADBC driver |
| Redshift | `redshift://...` | Via Postgres wire protocol |
| Database | Connection String |
|----------|------------------|
| PostgreSQL | `postgres://...` |
| MySQL | `mysql://...` |
| SQLite | `sqlite:///path/to/db` |
| Snowflake | `snowflake://account/db/schema?table=t` |
| BigQuery | `bigquery://project/dataset?table=t` |
| Redshift | `redshift://...` (Postgres wire protocol) |

**Sink modes:**
- `append`: INSERT INTO target table. Schema must be compatible.
Expand Down Expand Up @@ -802,7 +815,7 @@ output/region=GB/year=2024/part-0.parquet
...
```

DuckDB's `COPY ... PARTITION_BY` handles this natively for Parquet and CSV.
Output is split by writing each partition subset to its own path.

Partition columns are removed from the data by default (they're encoded in the path).
Use `--partition-keep-columns` to retain them in the data as well.
Expand Down Expand Up @@ -1071,9 +1084,7 @@ Process files across multiple threads for large file sets:
dtoo query --glob "data/**/*.parquet" --parallel 8 --where "amount > 100"
```

**Implementation:** Spawn N worker threads, each with its own DuckDB connection. Each worker
processes files from a shared queue and inserts into a thread-local temp table. After all files
are processed, merge thread-local tables into the final temp_results.
**Implementation:** Spawn N worker threads, each scanning files from a shared queue into thread-local `LazyFrame`s. After all files are processed, merge per-worker frames into the final accumulated result via `concat_lf_diagonal`.

**Considerations:**
- Default: sequential (1 thread). Enterprise unlocks `--parallel N`.
Expand Down Expand Up @@ -1101,7 +1112,7 @@ are processed, merge thread-local tables into the final temp_results.

- **Watch mode**: Re-run on file changes
- **Plugin system**: Custom format readers
- **SQLite input**: Read from SQLite files via sqlite_scanner extension
- **SQLite input**: Read from SQLite files (would require a pure-Rust SQLite reader crate)
- **Avro input**: Via Rust avro crate as preprocessor
- **Web UI**: Browser-based dashboard for audit logs, pipeline status, and profiling reports
- **dtoo server**: Long-running daemon mode for API-driven pipeline execution
28 changes: 13 additions & 15 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ This guide covers day-to-day usage of `dtoo`, from first run through practical w

## What is dtoo?

`dtoo` is a Rust CLI for querying and profiling data files with DuckDB. It is designed for fast local analytics and reproducible pipelines across file trees.
`dtoo` is a Rust CLI for querying and profiling data files. It is built on **Polars** (pure Rust) and is designed for fast local analytics and reproducible pipelines across file trees.

Core capabilities:
- Query many files using SQL
- Read CSV, Parquet, NDJSON, and Excel (`.xlsx`, `.xls`)
- Join reference tables into your query
- Add lineage, masking, profiling, fingerprinting, and manifests
- Read from local files or cloud paths (S3/GCS/Azure)
- Read from local files (cloud paths S3/GCS/Azure are deferred — they return a clear error in this build)

## Install and Build

Expand Down Expand Up @@ -94,6 +94,12 @@ dtoo query --glob "data/**/*.parquet" \
--post-sql "SELECT passenger_count, COUNT(*) AS trips FROM _ GROUP BY 1"
```

**SQL limitations (Polars `SQLContext`):** `SELECT`, `WHERE`, `GROUP BY`, `JOIN`, `ORDER BY`, `LIMIT`, CTEs, `UNION`/`UNION ALL`, subqueries, and common string/date functions are supported. Known gaps:

- **Window functions** (`OVER (PARTITION BY … ORDER BY …)`) have correctness issues — avoid them.
- **`DELETE`/`UPDATE`** are treated as row-filtering transforms, not DML mutations. Do not rely on DML semantics in `--post-sql`.
- Some exotic DuckDB date/regex/string functions are absent. They return a clear error (never a silent hang).

### Excel Sheet Selection

You can control sheet selection in two ways:
Expand Down Expand Up @@ -223,21 +229,13 @@ dtoo fingerprint data/trips.parquet

## Cloud Paths

`dtoo` supports cloud URIs when credentials are configured for DuckDB extensions.

Common options:
- `--s3-region`
- `--s3-profile`
- `--gcs-project`
- `--azure-account`

Examples:
**Cloud storage is deferred in this build.** Paths beginning with `s3://`, `gs://`, or `az://` return an explicit error:

```bash
dtoo query --glob "s3://my-bucket/data/**/*.parquet" --s3-region us-east-1

dtoo query gs://my-bucket/input.csv --gcs-project my-project
```
cloud storage (s3://…) is not supported in this build yet
```

The cloud CLI flags (`--s3-region`, `--s3-profile`, `--gcs-project`, `--azure-account`) still parse so that config files written for a future cloud-enabled build remain valid.

## Practical Recipes

Expand Down
2 changes: 2 additions & 0 deletions docs/specs/03-duckdb-engine.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DuckDB Engine

> **Superseded by [docs/specs/34-polars-engine.md](34-polars-engine.md) — dtoo migrated from DuckDB to Polars. This spec is retained for historical context.**

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this spec.

**Goal:** Initialise and manage the in-memory DuckDB instance that powers all SQL execution, file reading, and data accumulation.
Expand Down
Loading
Loading