duckrun 0.3.16__tar.gz → 0.3.17.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- duckrun-0.3.17.dev2/PKG-INFO +356 -0
- duckrun-0.3.17.dev2/README.md +327 -0
- duckrun-0.3.17.dev2/dbt/adapters/duckrun/__version__.py +1 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/delta_plugin.py +52 -41
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/engine.py +266 -20
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/impl.py +1 -25
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/remote.py +31 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/macros/materializations/_delta_core.sql +8 -1
- duckrun-0.3.17.dev2/duckrun/__init__.py +16 -0
- duckrun-0.3.17.dev2/duckrun/auth.py +73 -0
- duckrun-0.3.17.dev2/duckrun/delta_table.py +201 -0
- duckrun-0.3.17.dev2/duckrun/session.py +517 -0
- duckrun-0.3.17.dev2/duckrun.egg-info/PKG-INFO +356 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/duckrun.egg-info/SOURCES.txt +5 -4
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/duckrun.egg-info/requires.txt +7 -1
- duckrun-0.3.17.dev2/duckrun.egg-info/top_level.txt +2 -0
- duckrun-0.3.17.dev2/pyproject.toml +67 -0
- duckrun-0.3.16/PKG-INFO +0 -469
- duckrun-0.3.16/README.md +0 -445
- duckrun-0.3.16/dbt/adapters/duckrun/__version__.py +0 -1
- duckrun-0.3.16/duckrun.egg-info/PKG-INFO +0 -469
- duckrun-0.3.16/duckrun.egg-info/top_level.txt +0 -1
- duckrun-0.3.16/pyproject.toml +0 -53
- duckrun-0.3.16/tests/test_merge_spill.py +0 -395
- duckrun-0.3.16/tests/test_remote_secret_discovery.py +0 -157
- duckrun-0.3.16/tests/test_table_exists_guard.py +0 -188
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/LICENSE +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/MANIFEST.in +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/__init__.py +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/credentials.py +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/environment.py +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/adapters/duckrun/secret.py +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/__init__.py +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/dbt_project.yml +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/macros/catalog.sql +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/macros/materializations/delta.sql +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/macros/materializations/incremental.sql +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/dbt/include/duckrun/macros/materializations/table.sql +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/duckrun.egg-info/dependency_links.txt +0 -0
- {duckrun-0.3.16 → duckrun-0.3.17.dev2}/setup.cfg +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: duckrun
|
|
3
|
+
Version: 0.3.17.dev2
|
|
4
|
+
Summary: A dbt adapter that runs SQL in DuckDB and materializes to Delta Lake (delta_rs).
|
|
5
|
+
Author: mim
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/djouallah/duckrun
|
|
8
|
+
Project-URL: Repository, https://github.com/djouallah/duckrun
|
|
9
|
+
Project-URL: Issues, https://github.com/djouallah/duckrun/issues
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: dbt-duckdb>=1.8
|
|
14
|
+
Requires-Dist: dbt-core<2.0,>=1.8
|
|
15
|
+
Requires-Dist: duckdb==1.5.4.dev18
|
|
16
|
+
Requires-Dist: deltalake<1.5.1,>=1.5.0
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Provides-Extra: local
|
|
19
|
+
Requires-Dist: azure-identity; extra == "local"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
Requires-Dist: pyarrow; extra == "test"
|
|
23
|
+
Requires-Dist: pandas; extra == "test"
|
|
24
|
+
Provides-Extra: conformance
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "conformance"
|
|
26
|
+
Requires-Dist: pyarrow; extra == "conformance"
|
|
27
|
+
Requires-Dist: dbt-tests-adapter; extra == "conformance"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
<img src="https://raw.githubusercontent.com/djouallah/duckrun/main/duckrun.png" width="400" alt="duckrun">
|
|
31
|
+
|
|
32
|
+
[](https://badge.fury.io/py/duckrun)
|
|
33
|
+
|
|
34
|
+
> **Disclaimer:** This is a personal project, built and maintained in my own time. It is
|
|
35
|
+
> not affiliated with, endorsed by, or supported by any employer or vendor. No warranty —
|
|
36
|
+
> use it at your own risk.
|
|
37
|
+
|
|
38
|
+
**duckrun** is a [dbt](https://www.getdbt.com/) adapter that runs your model SQL in
|
|
39
|
+
**DuckDB** and writes the results to **Delta Lake** using
|
|
40
|
+
[`delta_rs`](https://delta-io.github.io/delta-rs/) (the `deltalake` Python package).
|
|
41
|
+
duckrun itself is just glue — it owns none of the heavy lifting. The real work is done
|
|
42
|
+
by **DuckDB** (executes the SQL), **delta-rs** (writes the Delta table), **Arrow** (the
|
|
43
|
+
zero-copy (kind of) bridge that hands query results from DuckDB to delta-rs), and **dbt** (orchestrates
|
|
44
|
+
the DAG). DuckDB is here for convenience as the SQL engine; the materialization is all
|
|
45
|
+
delta-rs and Arrow.
|
|
46
|
+
|
|
47
|
+
It is a thin wrapper around [`dbt-duckdb`](https://github.com/duckdb/dbt-duckdb). You
|
|
48
|
+
keep everything dbt-duckdb gives you — views, seeds, sources, tests, snapshots, the full
|
|
49
|
+
plugin ecosystem — and gain one extra thing: a Delta-backed `table` / `incremental`
|
|
50
|
+
materialization that writes real Delta tables.
|
|
51
|
+
|
|
52
|
+
The design rationale — why delta_rs and not DuckDB's native Delta writer, why Delta and not
|
|
53
|
+
Iceberg, why a separate adapter — lives in [docs/design_document.md](docs/design_document.md).
|
|
54
|
+
|
|
55
|
+
## How it fits together
|
|
56
|
+
|
|
57
|
+
DuckDB is a great query engine, Delta Lake is a great open table format, and dbt is the
|
|
58
|
+
right tool to orchestrate the DAG. duckrun wires the three together:
|
|
59
|
+
|
|
60
|
+
> **DuckDB executes · delta_rs materializes · dbt orchestrates.**
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install duckrun
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That single install pulls in `dbt-duckdb` (and therefore `duckdb`) plus `deltalake`.
|
|
69
|
+
|
|
70
|
+
## Configure your profile
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# ~/.dbt/profiles.yml
|
|
74
|
+
my_project:
|
|
75
|
+
target: dev
|
|
76
|
+
outputs:
|
|
77
|
+
dev:
|
|
78
|
+
type: duckrun
|
|
79
|
+
# No `threads:` needed — duckrun always runs single-threaded.
|
|
80
|
+
# DuckDB runs in-memory by default — the Delta tables are the only state.
|
|
81
|
+
# Default Delta location for models that don't set config(location=...).
|
|
82
|
+
root_path: './warehouse' # local path, or s3://..., gs://..., abfss://...
|
|
83
|
+
# storage_options: {} # passed through to deltalake for remote stores
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Persisted models are written to `<root_path>/<schema>/<model>` (e.g.
|
|
87
|
+
`./warehouse/dbo/orders`), or to an explicit `config(location=...)`.
|
|
88
|
+
|
|
89
|
+
### Fabric Lakehouse without a schema
|
|
90
|
+
|
|
91
|
+
A schema-less Lakehouse (tables straight under `Tables/`, no `Tables/<schema>/` grouping) is
|
|
92
|
+
a **bad pattern** — you lose the namespace that keeps a warehouse organized — but if you're
|
|
93
|
+
stuck with one, no special config is needed. Drop the trailing `Tables` from `root_path` and
|
|
94
|
+
let the schema fill that slot:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
schema: Tables
|
|
98
|
+
root_path: "abfss://<ws>@onelake.dfs.fabric.microsoft.com/<lh>.Lakehouse"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Since models are written to `<root_path>/<schema>/<model>`, this lands them at
|
|
102
|
+
`<lh>.Lakehouse/Tables/<model>` — exactly the flat layout the schema-less Lakehouse expects.
|
|
103
|
+
Prefer a schema-enabled Lakehouse (`root_path: .../Tables`, real schemas) whenever you can.
|
|
104
|
+
|
|
105
|
+
### Remote stores (S3 / GCS / ADLS)
|
|
106
|
+
|
|
107
|
+
Point `root_path` at the warehouse location and pass credentials through
|
|
108
|
+
`storage_options` — these flow straight to deltalake for writes and merges.
|
|
109
|
+
|
|
110
|
+
On Azure-backed stores, if `storage_options` carries a `bearer_token` (or `token` /
|
|
111
|
+
`access_token`), the adapter also auto-creates a matching DuckDB Azure secret, so
|
|
112
|
+
`delta_scan()` reads work with no extra config. In a notebook where the storage secret is
|
|
113
|
+
already provided to DuckDB, you can leave `storage_options` empty.
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
remote:
|
|
117
|
+
type: duckrun
|
|
118
|
+
schema: dbo
|
|
119
|
+
root_path: "s3://my-bucket/warehouse" # or abfss://... , gs://...
|
|
120
|
+
storage_options:
|
|
121
|
+
aws_access_key_id: "{{ env_var('AWS_ACCESS_KEY_ID') }}"
|
|
122
|
+
aws_secret_access_key: "{{ env_var('AWS_SECRET_ACCESS_KEY') }}"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Verified end-to-end against real remote object storage: `table` overwrite, `incremental`
|
|
126
|
+
merge, and `delta_scan` reads / tests.
|
|
127
|
+
|
|
128
|
+
## Materializations
|
|
129
|
+
|
|
130
|
+
| materialized | backed by | notes |
|
|
131
|
+
|-------------------|--------------------------|-----------------------------------------------------------------------|
|
|
132
|
+
| **`table`** | Delta (overwrite) | DuckDB runs the SQL; delta_rs writes the table fresh each run. |
|
|
133
|
+
| **`incremental`** | Delta (merge / append) | First run overwrites; later runs apply `incremental_strategy`. |
|
|
134
|
+
| `view` | in-memory DuckDB | Ephemeral staging within a run (inherited from dbt-duckdb). |
|
|
135
|
+
| `seed` | in-memory DuckDB | CSV fixtures (inherited from dbt-duckdb). |
|
|
136
|
+
| `delta` | Delta | Alias for `table`; honors `incremental=true`. Kept for convenience. |
|
|
137
|
+
|
|
138
|
+
The persisted materializations (`table`, `incremental`, `delta`) register a `delta_scan`
|
|
139
|
+
view over the new Delta table, so downstream `ref()` works.
|
|
140
|
+
|
|
141
|
+
### `table`
|
|
142
|
+
|
|
143
|
+
```sql
|
|
144
|
+
-- models/orders.sql
|
|
145
|
+
{{ config(materialized='table') }}
|
|
146
|
+
|
|
147
|
+
select status, count(*) as n, sum(amount) as total
|
|
148
|
+
from {{ ref('stg_orders') }}
|
|
149
|
+
group by status
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `incremental`
|
|
153
|
+
|
|
154
|
+
```sql
|
|
155
|
+
{{ config(materialized='incremental', unique_key='order_id', incremental_strategy='merge') }}
|
|
156
|
+
|
|
157
|
+
select * from {{ ref('stg_orders') }}
|
|
158
|
+
{% if is_incremental() %}
|
|
159
|
+
where updated_at > (select max(updated_at) from {{ this }})
|
|
160
|
+
{% endif %}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The first run (or `--full-refresh`, or a missing table) overwrites. Later runs apply the
|
|
164
|
+
`incremental_strategy`:
|
|
165
|
+
|
|
166
|
+
| `incremental_strategy` | behavior | requires |
|
|
167
|
+
|------------------------------------|-------------------------------------------|--------------|
|
|
168
|
+
| `merge` (default with `unique_key`) | upsert — update matched, insert new | `unique_key` |
|
|
169
|
+
| `insert` | insert only new keys (idempotent append) | `unique_key` |
|
|
170
|
+
| `append` (default without `unique_key`) | blind append | — |
|
|
171
|
+
| `safeappend` | append, but only if the table is unchanged since the model read it (else fail) — cheap, no dedup scan | — |
|
|
172
|
+
|
|
173
|
+
### `safeappend`
|
|
174
|
+
|
|
175
|
+
A cheap append for the common "load only what's new" pattern — when your model SQL **already
|
|
176
|
+
guarantees no duplicates** and you don't want to pay for a merge.
|
|
177
|
+
|
|
178
|
+
```sql
|
|
179
|
+
{{ config(materialized='incremental', incremental_strategy='safeappend') }}
|
|
180
|
+
|
|
181
|
+
select * from read_csv(getvariable('new_files'))
|
|
182
|
+
{% if is_incremental() %}
|
|
183
|
+
-- the dedup is your SQL's job: only load files not already in the table
|
|
184
|
+
where file not in (select distinct file from {{ this }})
|
|
185
|
+
{% endif %}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Why, reason 1 — performance.** `merge` / `insert` scan the target and join on the key to find
|
|
189
|
+
what's new — expensive on a large table. If the SQL above already excludes rows that are present,
|
|
190
|
+
that work is redundant. `safeappend` is a plain append: **no target data scan, no key join, and
|
|
191
|
+
DuckDB keeps its full memory budget** (the merge memory split is never applied — same as `append`
|
|
192
|
+
/ `overwrite`). The only thing it reads from the target is one Delta log entry to get the version.
|
|
193
|
+
|
|
194
|
+
**Why, reason 2 — a concurrency guard a blind `append` doesn't have.** Because the dedup is done
|
|
195
|
+
in SQL against `{{ this }}`, a plain `append` is unsafe under concurrency: if another writer
|
|
196
|
+
commits between your `not in (... from {{ this }})` read and your write, the file it added isn't
|
|
197
|
+
excluded and you get a duplicate. `safeappend` closes that gap — it commits **only if the table
|
|
198
|
+
version is unchanged since the model started** (captured *before* it reads `{{ this }}`); if
|
|
199
|
+
anything committed in between, it fails with `CommitFailedError` so the run re-runs against the new
|
|
200
|
+
state. No duplicate slips in.
|
|
201
|
+
|
|
202
|
+
This is **optimistic concurrency control** — it never locks the table or blocks other writers; it
|
|
203
|
+
appends, then validates at commit with a compare-and-swap on the version and aborts on a mismatch.
|
|
204
|
+
Its policy is the strictest of the strategies (abort on *any* concurrent change, rather than
|
|
205
|
+
reconcile like `merge` or auto-rebase like `append`), but the mechanism is optimistic, not
|
|
206
|
+
pessimistic. Re-running is safe and idempotent: the SQL dedup simply excludes whatever the previous
|
|
207
|
+
attempt already loaded.
|
|
208
|
+
|
|
209
|
+
First run (or `--full-refresh`, or a missing table) overwrites to create the table; `safeappend`
|
|
210
|
+
applies on later runs. A real example is the AEMO
|
|
211
|
+
[`fct_scada`](tests/integration_tests/aemo/models/marts/fct_scada.sql) model — the project's largest table,
|
|
212
|
+
which loads only not-yet-seen files and so uses `safeappend` instead of an expensive merge.
|
|
213
|
+
|
|
214
|
+
### Config options (`table` / `incremental` / `delta`)
|
|
215
|
+
|
|
216
|
+
| option | description |
|
|
217
|
+
|-------------------------|-----------------------------------------------------------------------------|
|
|
218
|
+
| `location` | Delta path. Defaults to `<root_path>/<schema>/<id>`. |
|
|
219
|
+
| `incremental_strategy` | `merge` \| `insert` \| `append` \| `safeappend` (incremental only). |
|
|
220
|
+
| `unique_key` | column(s) to merge on. |
|
|
221
|
+
| `merge_update_columns` | merge: update only these columns on match (others untouched). |
|
|
222
|
+
| `merge_exclude_columns` | merge: update all columns **except** these on match. |
|
|
223
|
+
| `merge_max_spill_size` | merge: memory ceiling in **bytes** for delta_rs's merge pool (not a disk budget). Defaults to ~60% of the **effective** limit — `min(physical RAM, container/cgroup limit, currently-free RAM)` — beyond which delta_rs spills the merge join to disk (like DuckDB's `memory_limit`). The other big consumer, DuckDB itself, is separately pinned to ~30% of the same effective limit on the merge path (it produces the merge source in the same process), so the two budgets sum under the cgroup cap; both log their chosen value at run start. Set `0` to disable. It bounds the merge pool, *not* the whole process (the Arrow source, read buffers, and spill-file page cache sit outside it), so on a tight container with a huge source the total can still exceed the cap — lower it if needed. A cap below the join's minimum (~hundreds of MB) makes the merge raise `Resources exhausted` instead of spilling. Requires deltalake 1.5.0 (pinned). |
|
|
224
|
+
| `incremental_predicates`| merge: extra predicates AND-ed into the merge condition (use `target.`/`source.`, or dbt's `DBT_INTERNAL_DEST`/`DBT_INTERNAL_SOURCE`). |
|
|
225
|
+
| `on_schema_change` | `ignore` (default) \| `append_new_columns` \| `fail`. (`sync_all_columns` only *adds* — delta_rs can't drop columns.) |
|
|
226
|
+
| `partition_by` | Delta partition column(s). |
|
|
227
|
+
| `merge_schema` | allow schema evolution on write. |
|
|
228
|
+
| `storage_options` | per-model override forwarded to deltalake. |
|
|
229
|
+
|
|
230
|
+
## Reading existing tables/files as sources
|
|
231
|
+
|
|
232
|
+
A source routed to the `duckrun` plugin can be a Delta table, a CSV, or a Parquet file.
|
|
233
|
+
`delta_table_path` always reads Delta; otherwise the path comes from `location` and the
|
|
234
|
+
format is taken from `format` (`csv` | `parquet` | `delta`) or inferred from the extension.
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
sources:
|
|
238
|
+
- name: lake
|
|
239
|
+
tables:
|
|
240
|
+
- name: customers # Delta table
|
|
241
|
+
meta:
|
|
242
|
+
plugin: duckrun
|
|
243
|
+
delta_table_path: 's3://bucket/lake/customers'
|
|
244
|
+
- name: events # CSV (read_csv_auto)
|
|
245
|
+
meta:
|
|
246
|
+
plugin: duckrun
|
|
247
|
+
format: csv
|
|
248
|
+
location: 's3://bucket/raw/events.csv'
|
|
249
|
+
- name: metrics # Parquet
|
|
250
|
+
meta:
|
|
251
|
+
plugin: duckrun
|
|
252
|
+
format: parquet
|
|
253
|
+
location: 's3://bucket/raw/metrics.parquet'
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## How it works
|
|
257
|
+
|
|
258
|
+
1. dbt compiles your model SQL.
|
|
259
|
+
2. The materialization stages it as a DuckDB view.
|
|
260
|
+
3. A `dbt-duckdb` plugin (a `store()` hook) hands that relation to deltalake over the
|
|
261
|
+
Arrow C-stream interface (`__arrow_c_stream__`) — no pyarrow required — which
|
|
262
|
+
`write_deltalake` / `DeltaTable.merge` consume natively.
|
|
263
|
+
4. The model relation becomes a `delta_scan` view over the new Delta table.
|
|
264
|
+
|
|
265
|
+
The adapter is a thin subclass of dbt-duckdb declaring `dependencies=['duckdb']`, so
|
|
266
|
+
`view`, `seed`, tests, and the rest are inherited directly; only `table` and
|
|
267
|
+
`incremental` are overridden to write Delta.
|
|
268
|
+
|
|
269
|
+
## Table maintenance (compaction & vacuum)
|
|
270
|
+
|
|
271
|
+
**duckrun maintains your Delta tables automatically — no configuration, no scheduled job, no
|
|
272
|
+
separate `OPTIMIZE`/`VACUUM` run to remember.** It happens inline on every write.
|
|
273
|
+
|
|
274
|
+
This matters because delta_rs has **no** automatic, post-commit maintenance of its own — and it
|
|
275
|
+
ignores Databricks-style auto-optimize table properties (`delta.autoOptimize.*`). Left alone, an
|
|
276
|
+
incremental table fragments into many small Parquet files and keeps every superseded file version
|
|
277
|
+
forever. duckrun runs the maintenance for you, right after each write:
|
|
278
|
+
|
|
279
|
+
| write | maintenance |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `table` / overwrite | `vacuum` + metadata cleanup every run |
|
|
282
|
+
| `append` | `optimize.compact` + `vacuum` + cleanup once the table exceeds **100 files** |
|
|
283
|
+
| `merge` / `insert` | same threshold-gated `compact` + `vacuum` + cleanup after the merge |
|
|
284
|
+
| `microbatch` / delete+insert | same threshold-gated maintenance |
|
|
285
|
+
|
|
286
|
+
Every `vacuum` uses delta_rs's **safe default retention (7 days / 168h)**, so files a
|
|
287
|
+
concurrent reader might still be reading are never deleted out from under it. The trade-off
|
|
288
|
+
is that a superseded file version lingers for the retention window before it can be
|
|
289
|
+
reclaimed — duckrun favors read-safety over immediate disk savings.
|
|
290
|
+
|
|
291
|
+
## Connection API (notebook)
|
|
292
|
+
|
|
293
|
+
Besides the dbt adapter, duckrun ships a storage-neutral, PySpark-shaped `duckrun.connect()` for
|
|
294
|
+
interactive/notebook use (local, S3, GCS, ADLS, OneLake). `conn.sql(...)` is **read-only** (including
|
|
295
|
+
time travel — `delta_scan('…', version => N)`); writes go through the Spark surface: a `DataFrame`
|
|
296
|
+
with `.write…saveAsTable()` (modes `overwrite` / `append` / `safeappend` / `ignore`) and a `DeltaTable` handle
|
|
297
|
+
(`conn.delta_table(name)` / `DeltaTable.forName`) with `.merge(...)`, `.delete()`, `.update()`,
|
|
298
|
+
`.replaceWhere()`, `.version()`, plus `conn.read` and `conn.catalog`.
|
|
299
|
+
|
|
300
|
+
`merge` is **snapshot-pinned by default** — Spark's single-snapshot MERGE, with no extra arguments:
|
|
301
|
+
the target version is captured and the commit is validated against it, so a concurrent writer fails
|
|
302
|
+
the commit loudly instead of silently interleaving. `mode("safeappend")` is the same optimistic,
|
|
303
|
+
fail-loud append as the dbt [`safeappend`](#safeappend) strategy: it commits only if the table is
|
|
304
|
+
unchanged since the call, else raises `CommitFailedError`.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
import duckrun
|
|
308
|
+
conn = duckrun.connect("abfss://ws@onelake.dfs.fabric.microsoft.com/lh.Lakehouse/Tables/dbo")
|
|
309
|
+
conn.sql("select * from orders").write.mode("overwrite").saveAsTable("orders_copy")
|
|
310
|
+
conn.table("orders_copy").show()
|
|
311
|
+
|
|
312
|
+
conn.delta_table("orders").delete("region = 'eu'") # delete / update / replaceWhere
|
|
313
|
+
|
|
314
|
+
# upsert — pinned automatically, nothing to pass
|
|
315
|
+
src = conn.sql("select * from updates")
|
|
316
|
+
conn.delta_table("orders").merge(src, "target.id = source.id") \
|
|
317
|
+
.whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
See [docs/connection-api.md](docs/connection-api.md) for the full per-method scorecard.
|
|
321
|
+
|
|
322
|
+
## Building with an AI assistant
|
|
323
|
+
|
|
324
|
+
duckrun ships a guide for AI coding assistants so they get the adapter's defaults right
|
|
325
|
+
(several differ from other dbt adapters). If you use **Claude Code**, install it once and
|
|
326
|
+
it loads on demand when you ask a duckrun question:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
/plugin marketplace add djouallah/duckrun
|
|
330
|
+
/plugin install duckrun-projects@duckrun
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Using a different assistant (Cursor, Copilot, Codex, …) or just have the repo checked
|
|
334
|
+
out? It reads the [`AGENTS.md`](AGENTS.md) at the repo root automatically, which points to
|
|
335
|
+
the full guide in
|
|
336
|
+
[`plugins/duckrun-projects/skills/duckrun-projects/SKILL.md`](plugins/duckrun-projects/skills/duckrun-projects/SKILL.md).
|
|
337
|
+
None of this is required to use duckrun — `pip install duckrun` is unaffected.
|
|
338
|
+
|
|
339
|
+
## Docs & test results
|
|
340
|
+
|
|
341
|
+
| Doc | What's in it |
|
|
342
|
+
|---|---|
|
|
343
|
+
| [Design document](docs/design_document.md) | Why delta_rs (not DuckDB's native Delta writer), why Delta (not Iceberg), why a separate adapter. |
|
|
344
|
+
| [Connection API](docs/connection-api.md) | The `duckrun.connect()` notebook API + the live per-method scorecard. |
|
|
345
|
+
| [dbt adapter conformance](docs/conformance.md) | Official `dbt-tests-adapter` results, regenerated on every push to `main`. |
|
|
346
|
+
| [Incremental MERGE benchmark](docs/merge-benchmark.md) | ~120M-row TPCH merge / append / overwrite scorecard — the release gate. |
|
|
347
|
+
|
|
348
|
+
**Testing.** `tests/integration_tests/aemo/` is a small dbt project built against OneLake, and
|
|
349
|
+
`tests/integration_tests/coffee/` is the connection-API coffee-shop scenario / stress test (CI:
|
|
350
|
+
[`integration.yml`](.github/workflows/integration.yml)); `tests/conformance/`
|
|
351
|
+
runs the official suite (above); `tests/correctness/` proves the concurrency guarantees. The cards
|
|
352
|
+
in those docs are rendered live by CI, so they always reflect the latest `main`.
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
MIT
|