sqlite-vtable-opendal 0.2.0

SQLite virtual tables for querying cloud object store metadata using OpenDAL
Documentation
//! SQLite virtual table implementation
//!
//! This module implements the SQLite virtual table interface using rusqlite's vtab API.
//! It connects SQLite queries to our StorageBackend trait implementations.

use crate::types::{columns, FileMetadata};
use rusqlite::{
    ffi,
    vtab::{self, CreateVTab, IndexInfo, VTab, VTabCursor, VTabKind},
    Result,
};
use std::os::raw::c_int;

/// Virtual table for cloud storage backends
///
/// This is a generic implementation that can work with any storage backend.
/// Configuration is passed through hidden columns in SQL queries.
#[repr(C)]
pub struct OpenDalTable {
    /// Base SQLite virtual table structure (required by SQLite)
    base: ffi::sqlite3_vtab,
}

/// Cursor for iterating through virtual table results
///
/// The cursor maintains the current position and holds the fetched data.
#[repr(C)]
pub struct OpenDalCursor {
    /// Base SQLite cursor structure (required by SQLite)
    base: ffi::sqlite3_vtab_cursor,
    /// All fetched file metadata
    files: Vec<FileMetadata>,
    /// Current row index
    current_row: usize,
}

impl OpenDalCursor {
    /// Create a new cursor instance
    fn new() -> Self {
        Self {
            base: ffi::sqlite3_vtab_cursor::default(),
            files: Vec::new(),
            current_row: 0,
        }
    }
}

unsafe impl VTabCursor for OpenDalCursor {
    /// Filter/initialize the cursor with query parameters
    ///
    /// This is called when a query begins. Backend implementations
    /// will override this to fetch files from their specific storage.
    fn filter(
        &mut self,
        _idx_num: c_int,
        _idx_str: Option<&str>,
        _args: &vtab::Filters<'_>,
    ) -> Result<()> {
        // This base implementation does nothing
        // Concrete backend implementations will override this
        self.files = Vec::new();
        self.current_row = 0;
        Ok(())
    }

    /// Move to the next row
    fn next(&mut self) -> Result<()> {
        self.current_row += 1;
        Ok(())
    }

    /// Check if we've reached the end of results
    fn eof(&self) -> bool {
        self.current_row >= self.files.len()
    }

    /// Get the value for a specific column in the current row
    fn column(&self, ctx: &mut vtab::Context, col_index: c_int) -> Result<()> {
        if self.current_row >= self.files.len() {
            return Ok(());
        }

        let file = &self.files[self.current_row];

        match col_index {
            columns::PATH => ctx.set_result(&file.path),
            columns::SIZE => ctx.set_result(&(file.size as i64)),
            columns::LAST_MODIFIED => ctx.set_result(&file.last_modified),
            columns::ETAG => ctx.set_result(&file.etag),
            columns::IS_DIR => ctx.set_result(&file.is_dir),
            columns::CONTENT_TYPE => ctx.set_result(&file.content_type),
            columns::NAME => ctx.set_result(&file.name),
            columns::CONTENT => {
                if let Some(ref content) = file.content {
                    ctx.set_result(&content.as_slice())
                } else {
                    ctx.set_result::<Option<&[u8]>>(&None)
                }
            }
            _ => Ok(()),
        }
    }

    /// Get the unique row ID for the current row
    fn rowid(&self) -> Result<i64> {
        Ok(self.current_row as i64)
    }
}

impl CreateVTab<'_> for OpenDalTable {
    /// Virtual table kind (eponymous means no CREATE VIRTUAL TABLE needed)
    const KIND: VTabKind = VTabKind::EponymousOnly;
}

unsafe impl VTab<'_> for OpenDalTable {
    type Aux = ();
    type Cursor = OpenDalCursor;

    /// Connect to the virtual table
    ///
    /// This defines the table schema that SQLite will use.
    fn connect(
        _db: &mut vtab::VTabConnection,
        _aux: Option<&Self::Aux>,
        _args: &[&[u8]],
    ) -> Result<(String, Self)> {
        // Define the virtual table schema
        // Columns 0-7 are the data columns
        // Additional HIDDEN columns can be added by backends for credentials
        let schema = "
            CREATE TABLE x(
                path TEXT,
                size INTEGER,
                last_modified TEXT,
                etag TEXT,
                is_dir INTEGER,
                content_type TEXT,
                name TEXT,
                content BLOB
            )
        ";

        Ok((
            schema.to_owned(),
            OpenDalTable {
                base: ffi::sqlite3_vtab::default(),
            },
        ))
    }

    /// Determine the best index strategy for a query
    ///
    /// This tells SQLite how to optimize the query.
    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
        // For now, we accept any query with a default cost
        // Future iterations will implement predicate pushdown here
        info.set_estimated_cost(1000.0);
        Ok(())
    }

    /// Open a new cursor for iterating through results
    fn open(&mut self) -> Result<Self::Cursor> {
        Ok(OpenDalCursor::new())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cursor_creation() {
        let cursor = OpenDalCursor::new();
        assert_eq!(cursor.current_row, 0);
        assert!(cursor.files.is_empty());
        assert!(cursor.eof());
    }

    #[test]
    fn test_cursor_navigation() {
        let mut cursor = OpenDalCursor::new();
        assert!(cursor.eof());

        cursor.next().unwrap();
        assert_eq!(cursor.current_row, 1);
    }
}