Skip to main content

sqlite_vtable_opendal/vtab/
mod.rs

1//! SQLite virtual table implementation
2//!
3//! This module implements the SQLite virtual table interface using rusqlite's vtab API.
4//! It connects SQLite queries to our StorageBackend trait implementations.
5
6use crate::types::{columns, FileMetadata};
7use rusqlite::{
8    ffi,
9    vtab::{self, CreateVTab, IndexInfo, VTab, VTabCursor, VTabKind},
10    Result,
11};
12use std::os::raw::c_int;
13
14/// Virtual table for cloud storage backends
15///
16/// This is a generic implementation that can work with any storage backend.
17/// Configuration is passed through hidden columns in SQL queries.
18#[repr(C)]
19pub struct OpenDalTable {
20    /// Base SQLite virtual table structure (required by SQLite)
21    base: ffi::sqlite3_vtab,
22}
23
24/// Cursor for iterating through virtual table results
25///
26/// The cursor maintains the current position and holds the fetched data.
27#[repr(C)]
28pub struct OpenDalCursor {
29    /// Base SQLite cursor structure (required by SQLite)
30    base: ffi::sqlite3_vtab_cursor,
31    /// All fetched file metadata
32    files: Vec<FileMetadata>,
33    /// Current row index
34    current_row: usize,
35}
36
37impl OpenDalCursor {
38    /// Create a new cursor instance
39    fn new() -> Self {
40        Self {
41            base: ffi::sqlite3_vtab_cursor::default(),
42            files: Vec::new(),
43            current_row: 0,
44        }
45    }
46}
47
48unsafe impl VTabCursor for OpenDalCursor {
49    /// Filter/initialize the cursor with query parameters
50    ///
51    /// This is called when a query begins. Backend implementations
52    /// will override this to fetch files from their specific storage.
53    fn filter(
54        &mut self,
55        _idx_num: c_int,
56        _idx_str: Option<&str>,
57        _args: &vtab::Values<'_>,
58    ) -> Result<()> {
59        // This base implementation does nothing
60        // Concrete backend implementations will override this
61        self.files = Vec::new();
62        self.current_row = 0;
63        Ok(())
64    }
65
66    /// Move to the next row
67    fn next(&mut self) -> Result<()> {
68        self.current_row += 1;
69        Ok(())
70    }
71
72    /// Check if we've reached the end of results
73    fn eof(&self) -> bool {
74        self.current_row >= self.files.len()
75    }
76
77    /// Get the value for a specific column in the current row
78    fn column(&self, ctx: &mut vtab::Context, col_index: c_int) -> Result<()> {
79        if self.current_row >= self.files.len() {
80            return Ok(());
81        }
82
83        let file = &self.files[self.current_row];
84
85        match col_index {
86            columns::PATH => ctx.set_result(&file.path),
87            columns::SIZE => ctx.set_result(&(file.size as i64)),
88            columns::LAST_MODIFIED => ctx.set_result(&file.last_modified),
89            columns::ETAG => ctx.set_result(&file.etag),
90            columns::IS_DIR => ctx.set_result(&file.is_dir),
91            columns::CONTENT_TYPE => ctx.set_result(&file.content_type),
92            columns::NAME => ctx.set_result(&file.name),
93            columns::CONTENT => {
94                if let Some(ref content) = file.content {
95                    ctx.set_result(&content.as_slice())
96                } else {
97                    ctx.set_result::<Option<&[u8]>>(&None)
98                }
99            }
100            _ => Ok(()),
101        }
102    }
103
104    /// Get the unique row ID for the current row
105    fn rowid(&self) -> Result<i64> {
106        Ok(self.current_row as i64)
107    }
108}
109
110impl CreateVTab<'_> for OpenDalTable {
111    /// Virtual table kind (eponymous means no CREATE VIRTUAL TABLE needed)
112    const KIND: VTabKind = VTabKind::EponymousOnly;
113}
114
115unsafe impl VTab<'_> for OpenDalTable {
116    type Aux = ();
117    type Cursor = OpenDalCursor;
118
119    /// Connect to the virtual table
120    ///
121    /// This defines the table schema that SQLite will use.
122    fn connect(
123        _db: &mut vtab::VTabConnection,
124        _aux: Option<&Self::Aux>,
125        _args: &[&[u8]],
126    ) -> Result<(String, Self)> {
127        // Define the virtual table schema
128        // Columns 0-7 are the data columns
129        // Additional HIDDEN columns can be added by backends for credentials
130        let schema = "
131            CREATE TABLE x(
132                path TEXT,
133                size INTEGER,
134                last_modified TEXT,
135                etag TEXT,
136                is_dir INTEGER,
137                content_type TEXT,
138                name TEXT,
139                content BLOB
140            )
141        ";
142
143        Ok((
144            schema.to_owned(),
145            OpenDalTable {
146                base: ffi::sqlite3_vtab::default(),
147            },
148        ))
149    }
150
151    /// Determine the best index strategy for a query
152    ///
153    /// This tells SQLite how to optimize the query.
154    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
155        // For now, we accept any query with a default cost
156        // Future iterations will implement predicate pushdown here
157        info.set_estimated_cost(1000.0);
158        Ok(())
159    }
160
161    /// Open a new cursor for iterating through results
162    fn open(&mut self) -> Result<Self::Cursor> {
163        Ok(OpenDalCursor::new())
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_cursor_creation() {
173        let cursor = OpenDalCursor::new();
174        assert_eq!(cursor.current_row, 0);
175        assert!(cursor.files.is_empty());
176        assert!(cursor.eof());
177    }
178
179    #[test]
180    fn test_cursor_navigation() {
181        let mut cursor = OpenDalCursor::new();
182        assert!(cursor.eof());
183
184        cursor.next().unwrap();
185        assert_eq!(cursor.current_row, 1);
186    }
187}