Skip to main content

quack_rs/
file_system.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! File system access (`DuckDB` 1.5.0+).
7//!
8//! This module exposes `DuckDB`'s virtual file system (VFS) to extensions, so a
9//! custom table function, replacement scan, or copy function can read and write
10//! files through the *same* abstraction `DuckDB` uses internally. That means
11//! transparently honouring `httpfs` (`s3://`, `http://`), in-memory files, and
12//! any other registered file system — rather than reaching for `std::fs` and
13//! only ever seeing local disk.
14//!
15//! # Obtaining a [`FileSystem`]
16//!
17//! Get one from a [`ClientContext`] (which you can obtain from most function
18//! callbacks):
19//!
20//! ```rust,no_run
21//! use quack_rs::client_context::ClientContext;
22//! use quack_rs::file_system::{FileOpenOptions, FileSystem};
23//!
24//! # fn demo(ctx: &ClientContext) -> Option<()> {
25//! let fs = FileSystem::from_client_context(ctx)?;
26//! let opts = FileOpenOptions::read_only();
27//! let handle = fs.open(c"data.csv", &opts).ok()?;
28//! let mut buf = vec![0u8; handle.size().max(0) as usize];
29//! let _n = handle.read(&mut buf).ok()?;
30//! # Some(())
31//! # }
32//! ```
33
34use std::ffi::CStr;
35use std::os::raw::c_void;
36
37use libduckdb_sys::{
38    duckdb_client_context_get_file_system, duckdb_create_file_open_options,
39    duckdb_destroy_file_handle, duckdb_destroy_file_open_options, duckdb_destroy_file_system,
40    duckdb_file_flag, duckdb_file_flag_DUCKDB_FILE_FLAG_APPEND,
41    duckdb_file_flag_DUCKDB_FILE_FLAG_CREATE, duckdb_file_flag_DUCKDB_FILE_FLAG_CREATE_NEW,
42    duckdb_file_flag_DUCKDB_FILE_FLAG_READ, duckdb_file_flag_DUCKDB_FILE_FLAG_WRITE,
43    duckdb_file_handle, duckdb_file_handle_close, duckdb_file_handle_error_data,
44    duckdb_file_handle_read, duckdb_file_handle_seek, duckdb_file_handle_size,
45    duckdb_file_handle_sync, duckdb_file_handle_tell, duckdb_file_handle_write,
46    duckdb_file_open_options, duckdb_file_open_options_set_flag, duckdb_file_system,
47    duckdb_file_system_error_data, duckdb_file_system_open, DuckDBSuccess,
48};
49
50use crate::client_context::ClientContext;
51use crate::error_data::ErrorData;
52
53/// A file-open mode flag, mirroring `duckdb_file_flag`.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum FileFlag {
57    /// Open for reading.
58    Read,
59    /// Open for writing.
60    Write,
61    /// Create the file if it does not exist.
62    Create,
63    /// Create the file, failing if it already exists.
64    CreateNew,
65    /// Open in append mode.
66    Append,
67}
68
69impl FileFlag {
70    /// Converts to the `DuckDB` C API constant.
71    #[must_use]
72    const fn to_raw(self) -> duckdb_file_flag {
73        match self {
74            Self::Read => duckdb_file_flag_DUCKDB_FILE_FLAG_READ,
75            Self::Write => duckdb_file_flag_DUCKDB_FILE_FLAG_WRITE,
76            Self::Create => duckdb_file_flag_DUCKDB_FILE_FLAG_CREATE,
77            Self::CreateNew => duckdb_file_flag_DUCKDB_FILE_FLAG_CREATE_NEW,
78            Self::Append => duckdb_file_flag_DUCKDB_FILE_FLAG_APPEND,
79        }
80    }
81}
82
83/// RAII wrapper for `duckdb_file_open_options`.
84///
85/// Describes how a file should be opened. Automatically destroyed when dropped.
86pub struct FileOpenOptions {
87    options: duckdb_file_open_options,
88}
89
90impl FileOpenOptions {
91    /// Creates an empty set of file-open options.
92    #[must_use]
93    pub fn new() -> Self {
94        // SAFETY: duckdb_create_file_open_options allocates an owned handle.
95        let options = unsafe { duckdb_create_file_open_options() };
96        Self { options }
97    }
98
99    /// Creates options configured for read-only access.
100    #[must_use]
101    pub fn read_only() -> Self {
102        let opts = Self::new();
103        opts.set_flag(FileFlag::Read, true);
104        opts
105    }
106
107    /// Creates options configured for writing, creating the file if needed.
108    #[must_use]
109    pub fn write_create() -> Self {
110        let opts = Self::new();
111        opts.set_flag(FileFlag::Write, true);
112        opts.set_flag(FileFlag::Create, true);
113        opts
114    }
115
116    /// Sets a file-open flag, returning `true` on success.
117    pub fn set_flag(&self, flag: FileFlag, value: bool) -> bool {
118        if self.options.is_null() {
119            return false;
120        }
121        // SAFETY: self.options is a valid duckdb_file_open_options.
122        let state =
123            unsafe { duckdb_file_open_options_set_flag(self.options, flag.to_raw(), value) };
124        state == DuckDBSuccess
125    }
126
127    /// Returns the raw handle without consuming the options.
128    #[inline]
129    #[must_use]
130    pub const fn as_raw(&self) -> duckdb_file_open_options {
131        self.options
132    }
133}
134
135impl Default for FileOpenOptions {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl Drop for FileOpenOptions {
142    fn drop(&mut self) {
143        if !self.options.is_null() {
144            // SAFETY: self.options is a valid handle that we own.
145            unsafe { duckdb_destroy_file_open_options(&raw mut self.options) };
146        }
147    }
148}
149
150/// RAII wrapper for a `duckdb_file_system`.
151///
152/// Automatically destroyed when dropped.
153pub struct FileSystem {
154    fs: duckdb_file_system,
155}
156
157impl FileSystem {
158    /// Obtains the file system associated with a [`ClientContext`].
159    ///
160    /// Returns `None` if `DuckDB` does not provide one.
161    #[must_use]
162    pub fn from_client_context(context: &ClientContext) -> Option<Self> {
163        // SAFETY: context.as_raw() is a valid duckdb_client_context.
164        let fs = unsafe { duckdb_client_context_get_file_system(context.as_raw()) };
165        if fs.is_null() {
166            None
167        } else {
168            Some(Self { fs })
169        }
170    }
171
172    /// Wraps a raw `duckdb_file_system` handle, taking ownership.
173    ///
174    /// # Safety
175    ///
176    /// `fs` must be a valid, non-null `duckdb_file_system` handle that the caller
177    /// no longer manages.
178    #[inline]
179    #[must_use]
180    pub const unsafe fn from_raw(fs: duckdb_file_system) -> Self {
181        Self { fs }
182    }
183
184    /// Returns the raw handle.
185    #[inline]
186    #[must_use]
187    pub const fn as_raw(&self) -> duckdb_file_system {
188        self.fs
189    }
190
191    /// Opens `path` with the given `options`.
192    ///
193    /// # Errors
194    ///
195    /// Returns the structured [`ErrorData`] if the file cannot be opened.
196    pub fn open(&self, path: &CStr, options: &FileOpenOptions) -> Result<FileHandle, ErrorData> {
197        let mut handle: duckdb_file_handle = std::ptr::null_mut();
198        // SAFETY: self.fs, path, and options.as_raw() are all valid; handle is a
199        // valid out-pointer.
200        let state = unsafe {
201            duckdb_file_system_open(self.fs, path.as_ptr(), options.as_raw(), &raw mut handle)
202        };
203        if state == DuckDBSuccess && !handle.is_null() {
204            // SAFETY: open succeeded, so handle is a valid owned file handle.
205            Ok(unsafe { FileHandle::from_raw(handle) })
206        } else {
207            Err(self.error_data())
208        }
209    }
210
211    /// Returns the structured error from the most recent failed operation.
212    #[must_use]
213    pub fn error_data(&self) -> ErrorData {
214        // SAFETY: self.fs is valid; the call returns an owned error data handle.
215        let raw = unsafe { duckdb_file_system_error_data(self.fs) };
216        // SAFETY: raw is an owned duckdb_error_data (possibly null).
217        unsafe { ErrorData::from_raw(raw) }
218    }
219}
220
221impl Drop for FileSystem {
222    fn drop(&mut self) {
223        if !self.fs.is_null() {
224            // SAFETY: self.fs is a valid handle that we own.
225            unsafe { duckdb_destroy_file_system(&raw mut self.fs) };
226        }
227    }
228}
229
230/// RAII wrapper for an open `duckdb_file_handle`.
231///
232/// Automatically closed and destroyed when dropped.
233pub struct FileHandle {
234    handle: duckdb_file_handle,
235}
236
237impl FileHandle {
238    /// Wraps a raw `duckdb_file_handle`, taking ownership.
239    ///
240    /// # Safety
241    ///
242    /// `handle` must be a valid, non-null `duckdb_file_handle` that the caller no
243    /// longer manages.
244    #[inline]
245    #[must_use]
246    pub const unsafe fn from_raw(handle: duckdb_file_handle) -> Self {
247        Self { handle }
248    }
249
250    /// Returns the raw handle.
251    #[inline]
252    #[must_use]
253    pub const fn as_raw(&self) -> duckdb_file_handle {
254        self.handle
255    }
256
257    /// Reads up to `buf.len()` bytes into `buf`, returning the number of bytes
258    /// read (0 at end of file).
259    ///
260    /// # Errors
261    ///
262    /// Returns the structured [`ErrorData`] on read failure.
263    pub fn read(&self, buf: &mut [u8]) -> Result<usize, ErrorData> {
264        let size = i64::try_from(buf.len()).unwrap_or(i64::MAX);
265        // SAFETY: self.handle is valid; buf is writable for `size` bytes.
266        let n = unsafe {
267            duckdb_file_handle_read(self.handle, buf.as_mut_ptr().cast::<c_void>(), size)
268        };
269        if n < 0 {
270            Err(self.error_data())
271        } else {
272            Ok(usize::try_from(n).unwrap_or(0))
273        }
274    }
275
276    /// Writes up to `buf.len()` bytes from `buf`, returning the number written.
277    ///
278    /// # Errors
279    ///
280    /// Returns the structured [`ErrorData`] on write failure.
281    pub fn write(&self, buf: &[u8]) -> Result<usize, ErrorData> {
282        let size = i64::try_from(buf.len()).unwrap_or(i64::MAX);
283        // SAFETY: self.handle is valid; buf is readable for `size` bytes.
284        let n =
285            unsafe { duckdb_file_handle_write(self.handle, buf.as_ptr().cast::<c_void>(), size) };
286        if n < 0 {
287            Err(self.error_data())
288        } else {
289            Ok(usize::try_from(n).unwrap_or(0))
290        }
291    }
292
293    /// Seeks to an absolute byte `position`.
294    ///
295    /// # Errors
296    ///
297    /// Returns the structured [`ErrorData`] if the seek fails.
298    pub fn seek(&self, position: u64) -> Result<(), ErrorData> {
299        let pos = i64::try_from(position).unwrap_or(i64::MAX);
300        // SAFETY: self.handle is valid.
301        let state = unsafe { duckdb_file_handle_seek(self.handle, pos) };
302        self.check(state)
303    }
304
305    /// Returns the current byte offset within the file.
306    #[must_use]
307    pub fn tell(&self) -> i64 {
308        // SAFETY: self.handle is valid.
309        unsafe { duckdb_file_handle_tell(self.handle) }
310    }
311
312    /// Returns the total size of the file in bytes.
313    #[must_use]
314    pub fn size(&self) -> i64 {
315        // SAFETY: self.handle is valid.
316        unsafe { duckdb_file_handle_size(self.handle) }
317    }
318
319    /// Flushes buffered writes to durable storage.
320    ///
321    /// # Errors
322    ///
323    /// Returns the structured [`ErrorData`] if the sync fails.
324    pub fn sync(&self) -> Result<(), ErrorData> {
325        // SAFETY: self.handle is valid.
326        let state = unsafe { duckdb_file_handle_sync(self.handle) };
327        self.check(state)
328    }
329
330    /// Closes the file. The handle is still destroyed on drop.
331    ///
332    /// # Errors
333    ///
334    /// Returns the structured [`ErrorData`] if the close fails.
335    pub fn close(&self) -> Result<(), ErrorData> {
336        // SAFETY: self.handle is valid.
337        let state = unsafe { duckdb_file_handle_close(self.handle) };
338        self.check(state)
339    }
340
341    /// Returns the structured error from the most recent failed operation.
342    #[must_use]
343    pub fn error_data(&self) -> ErrorData {
344        // SAFETY: self.handle is valid; the call returns an owned error data.
345        let raw = unsafe { duckdb_file_handle_error_data(self.handle) };
346        // SAFETY: raw is an owned duckdb_error_data (possibly null).
347        unsafe { ErrorData::from_raw(raw) }
348    }
349
350    /// Converts a `duckdb_state` into a `Result`, reading the handle's error
351    /// data on failure.
352    fn check(&self, state: libduckdb_sys::duckdb_state) -> Result<(), ErrorData> {
353        if state == DuckDBSuccess {
354            Ok(())
355        } else {
356            Err(self.error_data())
357        }
358    }
359}
360
361impl Drop for FileHandle {
362    fn drop(&mut self) {
363        if !self.handle.is_null() {
364            // SAFETY: self.handle is a valid handle that we own.
365            unsafe { duckdb_destroy_file_handle(&raw mut self.handle) };
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn file_flag_distinct_raw_values() {
376        let flags = [
377            FileFlag::Read,
378            FileFlag::Write,
379            FileFlag::Create,
380            FileFlag::CreateNew,
381            FileFlag::Append,
382        ];
383        for (i, a) in flags.iter().enumerate() {
384            for b in flags.iter().skip(i + 1) {
385                assert_ne!(a.to_raw(), b.to_raw(), "{a:?} and {b:?} share a raw value");
386            }
387        }
388    }
389}