不用Spark也能写Delta,DuckDB七月这三个新工具在偷偷填坑
MotherDuck 七月 newsletter 照例带来了 10 条 DuckDB 生态动态。大部分话题我们已经跟过——PostHog 把数仓从 ClickHouse 迁到 DuckDB、DuckCon #7 的 2.0 路线图、Quack 协议的性能测试。但有三样东西, newsletter 点到为止,我翻完源码才发现它们填的坑比表面看起来深得多。
● ● ●
duckrun:用AI写的Delta Lake写入补丁
DuckDB 读 Delta 很舒服,delta_scan 一行搞定。写 Delta?官方 delta 扩展只支持 blind INSERT——没有 UPDATE、没有 DELETE、没有 MERGE。这不是 DuckDB 偷懒,是设计取舍:官方路线走 Delta Kernel → Unity Catalog,压根没打算做文件系统原生的完整 DML。
duckrun 把这个窟窿补上了。
它的做法极其简单:DuckDB 执行 SQL,delta-rs 写 Delta,Arrow C-stream 在中间零拷贝传数据。不是新引擎,是胶水——但胶在关键位置:
| 方式 | 读 Delta | 写 Delta (merge/update/delete) | DuckDB SQL 引擎 |
|---|---|---|---|
| DuckDB alone | ✅ | ❌ (blind INSERT only) | ✅ |
| delta-rs alone | ✅ | ✅ | ❌ |
| duckrun | ✅ | ✅ | ✅ |
更有意思的是它的并发模型。Lakehouse 没有事务管理器,两个 pipeline 同时写同一张表,后写的那个会静默覆盖前者——丢数据不报错。duckrun 的做法是把每次读固定在某个 Delta 版本上,写入时如果发现中间有别人提交过,直接抛 CommitFailedError 而不是悄悄覆盖。乐观并发控制,文件系统原生,不需要协调器。
# 读:只读模式,不会误写
conn = duckrun.connect("abfss://<ws>@onelake.dfs.fabric.microsoft.com/<lh>/Tables/dbo")
conn.sql("select status, count(*) from orders group by status").show()
# 写:opt-in,纯 SQL
conn = duckrun.connect("...", read_only=False)
conn.sql("delete from clean_orders where amount = 0")
conn.sql("update clean_orders set status = 'shipped' where status = 'packed'")
作者 Mimoune Djouallah 说了一句我很有共鸣的话:"The AI writes the code; I own the problem and the shape of the solution." 他用 AI 写了这个项目,但设计决策全是他的——delta-rs 走写入、快照隔离做并发、dbt 适配器无 catalog 自发现。这种"AI 写代码,人做架构"的模式,可能就是我们未来的开发常态。
● ● ●
CereusDB:浏览器里的空间 SQL,但不是 duckdb-wasm
看到"WASM + 空间 SQL",第一反应是 duckdb-wasm 的 spatial 扩展。不是。
CereusDB 把整个 Apache SedonaDB 编译到了 WebAssembly。 SedonaDB 是 Rust 写的空间分析引擎,底层是 Apache DataFusion。编译到 WASM 之后,132 个 ST_ 空间函数、33 个 RS_ 栅格函数、空间 join、ST_KNN——全在浏览器里跑。
四个构建档,按需加载:
| Build | 功能 | Brotli 大小 |
|---|---|---|
| minimal | GEOS 谓词 + 空间 join + ST_KNN | 4.0 MB |
| standard | + PROJ / ST_Transform (坐标转换) | 6.0 MB |
| global | + S2 地理内核 (18 functions) | 6.7 MB |
| full | + GDAL 栅格 + GeoTIFF 读写 | 8.5 MB |
浏览器第一次加载后会被缓存,后续页面秒开。API 也干净:
const db = await CereusDB.create();
await db.registerRemoteParquet('cities', 'https://example.com/cities.parquet');
const result = await db.sqlJSON(`
SELECT a.name, b.name, ST_Distance(a.geom, b.geom) AS dist
FROM cities a, cities b
WHERE a.name < b.name
ORDER BY dist ASC LIMIT 10
`);
跟 duckdb-wasm 比:duckdb-wasm 是通用 OLAP 引擎附带空间扩展,CereusDB 是纯空间引擎。前者生态丰富(可运行任何 DuckDB SQL),后者空间能力更深(栅格、S2 地理、130+ ST_*)。不是替代关系,是互补。
● ● ●
Floe:用 YAML 定义数据质量的 Rust 守门员
数据工程有个永恒的问题:"这文件进来之前,谁检查过?"
Floe 的答案是:用 YAML 写数据合同,用 Rust + Polars 执行校验,用 Arrow 零拷贝写入 DuckDB。
entities:
- name: customers
source:
format: csv
path: /data/in/customers.csv
schema:
columns:
- name: customer_id
type: int64
not_null: true
- name: email
type: string
checks:
primary_key: [customer_id]
sink:
write_mode: merge_scd1
accepted:
format: duckdb
duckdb:
connection: "md:analytics"
table: customers
token: "${MOTHERDUCK_TOKEN}"
四级校验:文件结构 → 字段类型/非空 → 行级 → 跨文件唯一性。通过的进 DuckDB/MotherDuck,失败的路由到 rejected 输出。每个 flush 一个 BEGIN/COMMIT,读不会看到半截数据。
有意思的设计:默认 floe 二进制不含 DuckDB——libduckdb 太大。需要 DuckDB sink 时用 floe-duckdb companion,lean 版本自动检测并委托。这种"核心瘦 + 插件胖"的分发模式很实用。
Floe 跟 MotherDuck Flights 是一对好搭档:Floe 在本地/CI 里验证数据质量,Flights 在 MD 托管环境跑 Python + dlt 做增量加载。验证 + 加载,各司其职。
● ● ●
三个工具,一个信号
duckrun 补 DuckDB 原生 Delta 写的短板,CereusDB 打开浏览器端空间 SQL 的第二赛道,Floe 把数据质量门禁接到了 MotherDuck 上。它们不是 DuckDB 核心团队的产物,是社区在填坑——而且填得很专业。
DuckDB 从"SQLite for analytics"到"数据栈粘合剂"的转变,靠的不是一个版本,是这种工具一层层叠上去的。这三个值得放进你的工具箱。
GitHub: djouallah/duckrun · tobilg/cereusdb · malon64/floe