1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! 宿主文件系统(DuckDB 的 VFS)访问的底层半边:注册期捕获、回调期随时取用。
//!
//! The low-level half of host file system (DuckDB's VFS) access: captured at registration time,
//! usable from any callback. The convenience file helpers live in [`crate::duck_vfs`] too.
//!
//! # 为什么需要它 / Why this exists
//!
//! DuckDB 的 C API 只在部分回调里给出客户端上下文:标量函数有 `bind`/`init` 回调可以调
//! `duckdb_scalar_function_get_client_context`,表函数有 `duckdb_table_function_get_client_context`,
//! `COPY TO` 的四个回调也各有自己的入口。**聚合函数一个都没有** —— 它既没有 bind 回调,
//! 也没有 `duckdb_aggregate_function_get_client_context`,所以 `update` / `finalize` 里
//! 拿不到 `ClientContext`,也就拿不到 `FileSystem`。
//!
//! DuckDB's C API hands out a client context in *some* callbacks: scalar functions can call
//! `duckdb_scalar_function_get_client_context`, table functions
//! `duckdb_table_function_get_client_context`, and each of the four `COPY TO` callbacks has its own
//! entry point. **Aggregate functions have none of them** — no bind callback, no
//! `duckdb_aggregate_function_get_client_context` — so `update` / `finalize` cannot reach a
//! `ClientContext`, and therefore cannot reach a `FileSystem` either.
//!
//! 注册期(扩展加载)是唯一的窗口,但**不能保存那条连接**:quack-rs 的入口点是
//! `access.get_database()` → `duckdb_connect(db)` → 调注册闭包 → `duckdb_disconnect`,
//! 注册返回后连接即失效;而 `duckdb_client_context` 与 `duckdb_file_system` 在 C API 里都是
//! **引用语义**(分别包着 `ClientContext &` 与 `FileSystem &`,`destroy` 只是 `delete` 包装对象),
//! 活不过它们的来源连接。
//!
//! Registration time (extension load) is the only window — but the connection handed to the entry
//! point **must not be kept**: quack-rs' entry point runs `access.get_database()` →
//! `duckdb_connect(db)` → your registration closure → `duckdb_disconnect`, so that connection dies
//! when registration returns. Both `duckdb_client_context` and `duckdb_file_system` are
//! **reference-semantic** in the C API (they wrap `ClientContext &` and `FileSystem &`
//! respectively; `destroy` merely `delete`s the wrapper), so neither outlives its connection.
//!
//! # 机制 / How it works
//!
//! [`crate::register_all_duckfn`] 在跑注册项之前调用 [`capture`],用
//! [`Connection::as_raw_database`] 打开一条**自有长连接**([`OwnedConnection`] 持有数据库实例的
//! `shared_ptr`,能活过扩展加载)并存进进程级静态变量;静态变量永不析构,所以这条连接与它
//! [ClientData](https://duckdb.org) 里的客户端文件系统一直有效。
//!
//! [`crate::register_all_duckfn`] calls [`capture`] before running the registration items. It opens
//! an **owned, long-lived connection** with [`Connection::as_raw_database`] ([`OwnedConnection`]
//! holds a `shared_ptr` to the database instance, so it outlives extension loading) and stores it in
//! a process-level static. Statics are never dropped, so that connection — and the client file
//! system inside its `ClientData` — stays valid for the rest of the process.
//!
//! 取用时(例如聚合函数的 `update` / `finalize`)从这条连接现取 `ClientContext` → `FileSystem`,
//! 句柄随 guard 一起释放,不会逸出。底层读文件走的是**实例级** VFS(`ClientFileSystem` 的
//! `GetFileSystem()` 返回 `config.file_system`),所以 `httpfs` 注册的 `s3://` / `http(s)://`、
//! 内存文件系统都与本地磁盘一样可用。
//!
//! At use time (say an aggregate's `update` / `finalize`) the guard takes a fresh `ClientContext` →
//! `FileSystem` from that connection and releases both when dropped, so no handle escapes. Reads go
//! through the **instance-level** VFS (`ClientFileSystem::GetFileSystem()` returns
//! `config.file_system`), which is why `s3://` / `http(s)://` from `httpfs` and in-memory file
//! systems work exactly like local disk.
//!
//! # 用法 / Usage
//!
//! ```ignore
//! use duckfn::{DuckResult, duck_error};
//! use duckfn::duck_vfs::{FileOpenOptions, ErrorData};
//!
//! fn file_len(path: &std::ffi::CStr) -> DuckResult<i64> {
//! duckfn::duck_vfs::with_file_system(|fs| {
//! let options = FileOpenOptions::read_only();
//! let handle = fs.open(path, &options).map_err(file_error)?;
//! let mut buffer = Vec::new();
//! handle.read_to_end(&mut buffer).map_err(file_error)?;
//! Ok(buffer.len() as i64)
//! })
//! }
//!
//! // `ErrorData` 是 quack-rs 的结构化错误(`message()` 取 DuckDB 给的消息),
//! // 也由 `duckfn::duck_vfs` 再导出。
//! // `ErrorData` is quack-rs' structured error (`message()` returns DuckDB's message), also
//! // re-exported by `duckfn::duck_vfs`.
//! fn file_error(error: ErrorData) -> quack_rs::error::ExtensionError {
//! duck_error(error.message().unwrap_or_else(|| "file system error".to_string()))
//! }
//! ```
//!
//! 需要自己持有 guard 时用 [`file_system`] / [`client_context`];两者都实现了 `Deref`,
//! 可以直接调底层方法。
//!
//! Use [`file_system`] / [`client_context`] when you want to hold the guard yourself; both
//! implement `Deref`, so the underlying methods are available directly.
//!
//! # 局限 / Limitations
//!
//! - 进程级只有一份,以**首个完成注册的实例**为准。同一进程里存在多个数据库实例时,
//! 用的是先注册那个实例的 VFS。需要按实例隔离时,走「手写适配器 + 把自己的长连接挂在
//! 附加数据里」那条路(`duckfn::DuckExtraInfo` 配合 `quack_rs::query::OwnedConnection`),
//! 本模块不参与。
//! - 自有连接的 `FileOpener` 与查询连接不是同一个,**连接级** `SET` 不保证等价;实例级配置与
//! secrets 正常生效。
//! - guard 持有连接上的互斥锁:同一线程里嵌套取用会死锁,多线程取用会串行化。要读多个文件
//! 就把读取本身放进同一次 [`with_file_system`],或把结果缓存进自己的状态(聚合函数推荐在
//! `finalize` / 每组首行取一次,不要每行都取)。
//! - **不能用在「同一个线程里同步执行 DuckDB」的运行时里**:注册期就在当前线程上打开连接并重入
//! 引擎,那类运行时(单线程 wasm、DuckDB-Wasm 的 Node blocking 绑定)会直接死锁,表现为扩展
//! LOAD / 注册卡住不动。Node 的 worker 模式、浏览器与原生 CLI 都不受影响 —— 这也是本 crate
//! 把它单独放在 `owned-connection` feature 后面的原因。
//! - 需要 DuckDB 1.5.0+ 与本 crate 的 `owned-connection` feature(它自己依赖 `duckdb-1-5`),
//! 否则整个模块不存在。
//!
//! - There is exactly one process-level entry, and it is the **first instance to finish
//! registering** that wins. With several database instances in one process, the VFS of that first
//! instance is used. For per-instance isolation, hand-write an adapter and keep your own
//! connection in its extra data (`duckfn::DuckExtraInfo` plus
//! `quack_rs::query::OwnedConnection`); this module stays out of the way.
//! - The owned connection's `FileOpener` is not the query connection's, so **connection-level**
//! `SET`s are not guaranteed to be equivalent; instance-level configuration and secrets do apply.
//! - The guard holds a mutex on the connection: nesting it on one thread deadlocks and concurrent
//! take-ups serialize. Read several files inside a single [`with_file_system`] call, or cache the
//! result in your own state (aggregates should take it once per group — in `finalize` or on the
//! group's first row — never per row).
//! - **Must not be used in a runtime that executes DuckDB synchronously on one thread**: the
//! connection is opened, and the engine re-entered, on the current thread during registration,
//! which deadlocks such a runtime (single-threaded wasm, DuckDB-Wasm's Node blocking bindings) —
//! the extension LOAD just hangs. Node's worker mode, the browser and the native CLI are
//! unaffected, which is why this sits behind the crate's `owned-connection` feature.
//! - Requires DuckDB 1.5.0+ and this crate's `owned-connection` feature (which itself needs
//! `duckdb-1-5`); without them the whole module is absent.
use Deref;
use ;
use ClientContext;
use Connection;
use FileSystem;
use OwnedConnection;
use crate::;
/// 注册期打开的自有长连接;进程级唯一,且静态永不析构。
///
/// The owned long-lived connection opened at registration time; one per process, never dropped
/// (statics do not run `Drop`), which is exactly why the handles derived from it stay valid.
static VFS_CONNECTION: = new;
/// 捕获失败的原因,供后续调用报出可读的错误。
///
/// Why capturing failed, so later calls can report a readable error.
static CAPTURE_ERROR: = new;
/// 注册期捕获:打开自有长连接并保存;`register_all_duckfn` 会调用它,用户不需要直接调。
///
/// Capture at registration time: opens the owned long-lived connection and stores it.
/// [`crate::register_all_duckfn`] calls this; users never call it directly.
///
/// best-effort:失败只记下原因(后续取用时才会报错),绝不阻断扩展注册 —— 用不上文件系统的扩展
/// 不该因为这一项失败而加载不了。`OnceLock::set` 天然幂等,重复注册(同一进程多个实例)时
/// 首个成功的实例胜出。
///
/// Best-effort: a failure is only recorded (and surfaces when something asks for the file system);
/// registration never fails because of it — an extension that never touches the file system must not
/// fail to load over this. `OnceLock::set` is naturally idempotent, so when several instances
/// register in one process the first one to succeed wins.
pub
/// 取一份可用的 DuckDB 文件系统,随 guard 一起释放。
///
/// Takes a usable DuckDB file system; it is released together with the returned guard.
///
/// 返回的 [`DuckFileSystem`] 实现 `Deref<Target = FileSystem>`,可以直接 `open()`;
/// guard 释放时会按「文件系统 → 客户端上下文 → 连接锁」的顺序收尾。
///
/// The returned [`DuckFileSystem`] implements `Deref<Target = FileSystem>`, so `open()` is available
/// directly; dropping the guard releases the file system, then the client context, then the lock.
///
/// # Errors
///
/// 扩展还没注册完(或注册时打开连接失败)时返回可读的错误。
///
/// Returns a readable error when the extension has not finished registering yet, or when capturing
/// failed.
/// 在闭包里用一次文件系统:句柄不逸出,最省心的形态。
///
/// Uses the file system inside a closure: no handle escapes, the least error-prone form.
///
/// 聚合函数推荐在 `finalize`(每组一次)或每组首行调用一次,把结果缓存进自己的状态,
/// 而不是在 `update` 的每一行都取 —— guard 会串行化对这条连接的访问。
///
/// Aggregates should call it once per group — in `finalize`, or on the group's first row — and cache
/// the result in their own state, rather than taking it on every `update` row: the guard serializes
/// access to that connection.
///
/// # Errors
///
/// 同 [`file_system`];闭包自身返回的错误原样传出。
///
/// Same as [`file_system`]; an error returned by the closure is passed through unchanged.
/// 取一份可用的客户端上下文(连接级配置项、连接 ID)。
///
/// Takes a usable client context (connection-level configuration options, connection ID).
///
/// 注意 `ClientContext::catalog` 需要活跃事务,在空闲连接上会返回 `None`;文件系统请走
/// [`file_system`]。
///
/// Note that `ClientContext::catalog` needs an active transaction and returns `None` on an idle
/// connection; use [`file_system`] for the file system.
///
/// # Errors
///
/// 同 [`file_system`]。
///
/// Same as [`file_system`].
/// 文件系统 guard:`Deref` 到 quack-rs 的 [`FileSystem`],drop 时按声明顺序释放各句柄。
///
/// File-system guard: `Deref`s to quack-rs' [`FileSystem`]; dropping it releases the handles in
/// declaration order.
///
/// 字段顺序即析构顺序(`fs` → `context` → `connection`):先放开对连接内文件的包装,再删客户端
/// 上下文包装,最后解锁。类型自动是 `!Send + !Sync`(内含两个引用语义的句柄),跨线程误用会
/// 在编译期被挡住。
///
/// Field order *is* drop order (`fs` → `context` → `connection`): release the file-system wrapper
/// first, then the client-context wrapper, then the lock. The type is automatically
/// `!Send + !Sync` (it holds two reference-semantic handles), so misuse across threads is a compile
/// error.
/// 客户端上下文 guard:`Deref` 到 quack-rs 的 [`ClientContext`]。
///
/// Client-context guard: `Deref`s to quack-rs' [`ClientContext`].
/// 取连接锁;未捕获时给出可读错误。
///
/// Takes the connection lock; reports a readable error when nothing was captured.
/// 构造「还没准备好」的错误,区分「注册失败」与「尚未注册」两种情况。
///
/// Builds the "not ready" error, distinguishing "capture failed" from "not registered yet".
/// [`not_ready_error`] 的纯逻辑部分:拿得到捕获失败原因就用它,否则说「尚未注册」。
///
/// The pure part of [`not_ready_error`]: report the captured failure when there is one, otherwise
/// say "not registered yet". Kept free of global state so it can be tested deterministically.