vibesql_storage/database/
cache.rs

1// ============================================================================
2// Database Columnar Cache Integration
3// ============================================================================
4
5use std::sync::Arc;
6
7use super::core::Database;
8use crate::{columnar_cache::ColumnarCache, StorageError};
9
10impl Database {
11    // ============================================================================
12    // Columnar Cache Methods
13    // ============================================================================
14
15    /// Get columnar representation of a table, using cache if available
16    ///
17    /// This method provides an Arc-wrapped columnar representation of the table,
18    /// enabling zero-copy sharing between queries. The cache automatically manages
19    /// memory via LRU eviction.
20    ///
21    /// # Arguments
22    /// * `table_name` - Name of the table to get columnar representation for
23    ///
24    /// # Returns
25    /// * `Ok(Some(Arc<ColumnarTable>))` - Cached or newly converted columnar data
26    /// * `Ok(None)` - Table not found
27    /// * `Err(StorageError)` - Conversion failed
28    ///
29    /// # Example
30    /// ```text
31    /// if let Some(columnar) = db.get_columnar("lineitem")? {
32    ///     // Use columnar data for SIMD operations
33    /// }
34    /// ```
35    pub fn get_columnar(
36        &self,
37        table_name: &str,
38    ) -> Result<Option<Arc<crate::ColumnarTable>>, StorageError> {
39        // Check cache first
40        if let Some(cached) = self.columnar_cache.get(table_name) {
41            return Ok(Some(cached));
42        }
43
44        // Table not in cache - need to get table and convert
45        let table = match self.get_table(table_name) {
46            Some(t) => t,
47            None => return Ok(None),
48        };
49
50        // Convert to columnar format
51        let columnar = table.scan_columnar()?;
52
53        // Insert into cache and return
54        let cached = self.columnar_cache.insert(table_name, columnar);
55        Ok(Some(cached))
56    }
57
58    /// Invalidate columnar cache entry for a table
59    ///
60    /// Called automatically when a table is modified (INSERT/UPDATE/DELETE)
61    /// to ensure the cache doesn't serve stale data.
62    pub fn invalidate_columnar_cache(&self, table_name: &str) {
63        self.columnar_cache.invalidate(table_name);
64    }
65
66    /// Clear all columnar cache entries
67    pub fn clear_columnar_cache(&self) {
68        self.columnar_cache.clear();
69    }
70
71    /// Get columnar cache statistics
72    ///
73    /// Returns statistics about cache hits, misses, evictions, and conversions.
74    /// Useful for monitoring cache effectiveness and tuning the cache budget.
75    pub fn columnar_cache_stats(&self) -> crate::columnar_cache::CacheStats {
76        self.columnar_cache.stats()
77    }
78
79    /// Get current columnar cache memory usage in bytes
80    pub fn columnar_cache_memory_usage(&self) -> usize {
81        self.columnar_cache.memory_usage()
82    }
83
84    /// Get columnar cache memory budget in bytes
85    pub fn columnar_cache_budget(&self) -> usize {
86        self.columnar_cache.max_memory()
87    }
88
89    /// Set the columnar cache memory budget
90    ///
91    /// Note: This creates a new cache, discarding all cached data.
92    /// Call this before loading data for best results.
93    pub fn set_columnar_cache_budget(&mut self, max_bytes: usize) {
94        self.columnar_cache = Arc::new(ColumnarCache::new(max_bytes));
95    }
96
97    /// Pre-warm the columnar cache for specific tables
98    ///
99    /// This method eagerly converts row data to columnar format and populates
100    /// the cache. Call this after data loading to avoid conversion overhead
101    /// during query execution.
102    ///
103    /// # Arguments
104    /// * `table_names` - Names of tables to pre-warm
105    ///
106    /// # Returns
107    /// * `Ok(count)` - Number of tables successfully pre-warmed
108    /// * `Err(StorageError)` - Conversion failed for a table
109    ///
110    /// # Example
111    /// ```text
112    /// // After loading TPC-H data
113    /// let warmed = db.pre_warm_columnar_cache(&["lineitem", "orders"])?;
114    /// eprintln!("Pre-warmed {} tables", warmed);
115    /// ```
116    ///
117    /// # Performance
118    ///
119    /// This method performs the row-to-columnar conversion once, eliminating
120    /// the ~31% overhead that would otherwise occur on the first query.
121    /// For a 600K row LINEITEM table, this saves ~40ms per query session.
122    pub fn pre_warm_columnar_cache(&self, table_names: &[&str]) -> Result<usize, StorageError> {
123        let mut count = 0;
124        for table_name in table_names {
125            // get_columnar will convert and cache if not already cached
126            if self.get_columnar(table_name)?.is_some() {
127                count += 1;
128            }
129        }
130        Ok(count)
131    }
132
133    /// Pre-warm the columnar cache for all tables in the database
134    ///
135    /// This method eagerly converts all tables to columnar format.
136    /// Useful for benchmark scenarios where all tables will be queried.
137    ///
138    /// # Returns
139    /// * `Ok(count)` - Number of tables successfully pre-warmed
140    /// * `Err(StorageError)` - Conversion failed for a table
141    ///
142    /// # Example
143    /// ```text
144    /// // After loading all benchmark data
145    /// let warmed = db.pre_warm_all_columnar()?;
146    /// eprintln!("Pre-warmed {} tables", warmed);
147    /// ```
148    pub fn pre_warm_all_columnar(&self) -> Result<usize, StorageError> {
149        let table_names: Vec<String> = self.list_tables();
150        let refs: Vec<&str> = table_names.iter().map(|s| s.as_str()).collect();
151        self.pre_warm_columnar_cache(&refs)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use vibesql_catalog::{ColumnSchema, TableSchema};
158    use vibesql_types::{DataType, SqlValue};
159
160    use super::*;
161    use crate::Row;
162
163    fn create_test_table_schema(name: &str) -> TableSchema {
164        TableSchema::new(
165            name.to_string(),
166            vec![
167                ColumnSchema::new("id".to_string(), DataType::Integer, false),
168                ColumnSchema::new(
169                    "name".to_string(),
170                    DataType::Varchar { max_length: Some(255) },
171                    true,
172                ),
173            ],
174        )
175    }
176
177    fn create_test_rows(count: usize) -> Vec<Row> {
178        (0..count)
179            .map(|i| {
180                Row::new(vec![
181                    SqlValue::Integer(i as i64),
182                    SqlValue::Varchar(arcstr::ArcStr::from(format!("name_{}", i))),
183                ])
184            })
185            .collect()
186    }
187
188    #[test]
189    fn test_pre_warm_columnar_cache_with_valid_tables() {
190        let mut db = Database::new();
191
192        // Create test tables
193        db.create_table(create_test_table_schema("table1")).unwrap();
194        db.create_table(create_test_table_schema("table2")).unwrap();
195
196        // Insert some rows
197        for row in create_test_rows(10) {
198            db.insert_row("table1", row).unwrap();
199        }
200        for row in create_test_rows(5) {
201            db.insert_row("table2", row).unwrap();
202        }
203
204        // Pre-warm specific tables
205        let count = db.pre_warm_columnar_cache(&["table1", "table2"]).unwrap();
206        assert_eq!(count, 2, "Should have pre-warmed 2 tables");
207
208        // Verify stats show conversions occurred
209        let stats = db.columnar_cache_stats();
210        assert_eq!(stats.conversions, 2, "Should have converted 2 tables");
211    }
212
213    #[test]
214    fn test_pre_warm_columnar_cache_nonexistent_table() {
215        let db = Database::new();
216
217        // Pre-warm with nonexistent tables
218        let count = db.pre_warm_columnar_cache(&["nonexistent1", "nonexistent2"]).unwrap();
219        assert_eq!(count, 0, "Should return 0 for nonexistent tables");
220
221        // Verify no conversions occurred
222        let stats = db.columnar_cache_stats();
223        assert_eq!(stats.conversions, 0, "Should have 0 conversions for nonexistent tables");
224    }
225
226    #[test]
227    fn test_pre_warm_columnar_cache_mixed_tables() {
228        let mut db = Database::new();
229
230        // Create only one table
231        db.create_table(create_test_table_schema("exists")).unwrap();
232        for row in create_test_rows(5) {
233            db.insert_row("exists", row).unwrap();
234        }
235
236        // Pre-warm with mix of existing and nonexistent tables
237        let count = db.pre_warm_columnar_cache(&["exists", "nonexistent"]).unwrap();
238        assert_eq!(count, 1, "Should have pre-warmed only 1 existing table");
239    }
240
241    #[test]
242    fn test_pre_warm_all_columnar() {
243        let mut db = Database::new();
244
245        // Create multiple test tables
246        db.create_table(create_test_table_schema("table_a")).unwrap();
247        db.create_table(create_test_table_schema("table_b")).unwrap();
248        db.create_table(create_test_table_schema("table_c")).unwrap();
249
250        // Insert some rows
251        for row in create_test_rows(5) {
252            db.insert_row("table_a", row).unwrap();
253        }
254        for row in create_test_rows(3) {
255            db.insert_row("table_b", row).unwrap();
256        }
257        for row in create_test_rows(7) {
258            db.insert_row("table_c", row).unwrap();
259        }
260
261        // Pre-warm all tables
262        let count = db.pre_warm_all_columnar().unwrap();
263        assert_eq!(count, 3, "Should have pre-warmed all 3 tables");
264
265        // Verify stats
266        let stats = db.columnar_cache_stats();
267        assert_eq!(stats.conversions, 3, "Should have converted all 3 tables");
268    }
269
270    #[test]
271    fn test_pre_warm_results_in_cache_hits() {
272        let mut db = Database::new();
273
274        // Create and populate a table
275        db.create_table(create_test_table_schema("cached_table")).unwrap();
276        for row in create_test_rows(10) {
277            db.insert_row("cached_table", row).unwrap();
278        }
279
280        // Pre-warm the cache
281        let count = db.pre_warm_columnar_cache(&["cached_table"]).unwrap();
282        assert_eq!(count, 1);
283
284        // Record stats after pre-warming
285        let stats_before = db.columnar_cache_stats();
286        let hits_before = stats_before.hits;
287
288        // Access the columnar data again - should be a cache hit
289        let _ = db.get_columnar("cached_table").unwrap();
290
291        // Verify cache hit occurred
292        let stats_after = db.columnar_cache_stats();
293        assert_eq!(
294            stats_after.hits,
295            hits_before + 1,
296            "Should have one more cache hit after accessing pre-warmed table"
297        );
298        assert_eq!(
299            stats_after.conversions, stats_before.conversions,
300            "Should not have additional conversions"
301        );
302    }
303
304    #[test]
305    fn test_pre_warm_empty_table_list() {
306        let db = Database::new();
307
308        // Pre-warm with empty list
309        let count = db.pre_warm_columnar_cache(&[]).unwrap();
310        assert_eq!(count, 0, "Should return 0 for empty table list");
311    }
312
313    #[test]
314    fn test_pre_warm_all_empty_database() {
315        let db = Database::new();
316
317        // Pre-warm all on empty database
318        let count = db.pre_warm_all_columnar().unwrap();
319        assert_eq!(count, 0, "Should return 0 for empty database");
320    }
321
322    #[test]
323    fn test_pre_warm_idempotent() {
324        let mut db = Database::new();
325
326        // Create and populate a table
327        db.create_table(create_test_table_schema("test_table")).unwrap();
328        for row in create_test_rows(5) {
329            db.insert_row("test_table", row).unwrap();
330        }
331
332        // Pre-warm twice
333        let count1 = db.pre_warm_columnar_cache(&["test_table"]).unwrap();
334        let stats1 = db.columnar_cache_stats();
335
336        let count2 = db.pre_warm_columnar_cache(&["test_table"]).unwrap();
337        let stats2 = db.columnar_cache_stats();
338
339        // Both should report success
340        assert_eq!(count1, 1);
341        assert_eq!(count2, 1);
342
343        // But only one conversion should have occurred (second should be cache hit)
344        assert_eq!(stats1.conversions, 1);
345        assert_eq!(stats2.conversions, 1, "Second pre-warm should not cause additional conversion");
346        assert_eq!(stats2.hits, stats1.hits + 1, "Second pre-warm should result in cache hit");
347    }
348}