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