Skip to main content

zsh/compsys/
cache.rs

1//! SQLite mirror tables for compsys.
2//!
3//! NOT the completion cache. The authoritative completion / autoload
4//! cache is the rkyv-mmap'd shard set at `~/.zshrs/*.rkyv`
5//! (zero-copy hot path; see `src/extensions/autoload_cache.rs` and
6//! `src/compsys/README.md`). This SQLite file is a read-only mirror
7//! hydrated alongside the shards for `dbview` / SQL inspection only
8//! — the Tab cache hit/miss path never opens this connection.
9//!
10//! Mirror-side optimizations (still in place because `dbview` queries
11//! benefit from them):
12//! - FTS5 vtables sit beside the flat mirror tables for ad-hoc SQL
13//!   prefix search from `dbview`; not consulted by the completion hot
14//!   path
15//! - WAL mode for concurrent reads
16//! - Memory-mapped I/O (mmap)
17//! - No JOINs, no GROUP BY, no subqueries
18//! - Denormalized flat tables with covering indexes
19//! - Prepared statement caching
20
21use rusqlite::{params, Connection, OptionalExtension};
22use std::collections::HashMap;
23use std::path::{Path, PathBuf};
24
25/// SQLite cache for completion system
26pub struct CompsysCache {
27    /// `conn` field.
28    conn: Connection,
29}
30
31/// Returns the default cache path: `$ZSHRS_HOME/compsys.db` (default
32/// `~/.zshrs/compsys.db`). Project policy forbids `~/.cache/zshrs/`
33/// and `~/Library/Caches/zshrs/`.
34pub fn default_cache_path() -> PathBuf {
35    let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
36        PathBuf::from(custom)
37    } else {
38        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
39        PathBuf::from(home).join(".zshrs")
40    };
41    root.join("compsys.db")
42}
43
44impl CompsysCache {
45    /// Access the underlying SQLite connection (for dbview etc.)
46    pub fn conn(&self) -> &Connection {
47        &self.conn
48    }
49
50    /// Count rows in a table.
51    pub fn count_table(&self, table: &str) -> rusqlite::Result<usize> {
52        // Table name is not user input — it comes from our code.
53        let sql = format!("SELECT COUNT(*) FROM {}", table);
54        self.conn
55            .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
56    }
57
58    /// Count rows matching a WHERE clause.
59    pub fn count_table_where(&self, table: &str, condition: &str) -> rusqlite::Result<usize> {
60        let sql = format!("SELECT COUNT(*) FROM {} WHERE {}", table, condition);
61        self.conn
62            .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
63    }
64
65    /// Open or create cache database with maximum performance settings
66    pub fn open(path: impl AsRef<Path>) -> rusqlite::Result<Self> {
67        let conn = Connection::open(path)?;
68        let cache = Self { conn };
69        cache.configure_for_speed()?;
70        cache.init_schema()?;
71        Ok(cache)
72    }
73
74    /// In-memory cache (for testing)
75    pub fn memory() -> rusqlite::Result<Self> {
76        let conn = Connection::open_in_memory()?;
77        let cache = Self { conn };
78        cache.configure_for_speed()?;
79        cache.init_schema()?;
80        Ok(cache)
81    }
82
83    /// Configure SQLite for maximum read performance (called on every open)
84    fn configure_for_speed(&self) -> rusqlite::Result<()> {
85        // WAL mode persists, but cache/mmap need to be set each session
86        self.conn.execute_batch(
87            r#"
88            PRAGMA journal_mode = WAL;
89            PRAGMA synchronous = NORMAL;
90            PRAGMA cache_size = -64000;
91            PRAGMA mmap_size = 268435456;
92            PRAGMA temp_store = MEMORY;
93            "#,
94        )
95    }
96
97    fn init_schema(&self) -> rusqlite::Result<()> {
98        // Migration: drop the legacy `bytecode BLOB` column from `autoloads`
99        // if and only if the table already exists with that schema. Bytecode
100        // now lives in the rkyv shard at ~/.zshrs/autoloads.rkyv (see
101        // `crate::autoload_cache`); SQLite holds only the body/source
102        // metadata. The user's directive: "delete all sqlite columns related
103        // to bytecode, sqlite3 is read only mirror".
104        //
105        // SQLite ≥3.35 supports `ALTER TABLE … DROP COLUMN`. Older runtimes
106        // need the recreate-table dance. We use the recreate path here for
107        // portability and gate it on a table-existence + column-existence
108        // probe so a fresh DB never tries to SELECT from a missing
109        // `autoloads`. Cost: one full table rewrite the first time a
110        // post-migration zshrs opens an old DB; bodies/sources/offsets/sizes
111        // are preserved.
112        let has_legacy_bytecode_col = {
113            let exists: i64 = self.conn.query_row(
114                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='autoloads'",
115                [],
116                |row| row.get(0),
117            )?;
118            if exists == 0 {
119                false
120            } else {
121                let mut stmt = self.conn.prepare("PRAGMA table_info(autoloads)")?;
122                let cols: Vec<String> = stmt
123                    .query_map([], |row| row.get::<_, String>(1))?
124                    .collect::<rusqlite::Result<_>>()?;
125                cols.iter().any(|c| c == "bytecode")
126            }
127        };
128        if has_legacy_bytecode_col {
129            self.conn.execute_batch(
130                r#"
131                BEGIN;
132                CREATE TABLE autoloads_new (
133                    name TEXT PRIMARY KEY,
134                    source TEXT NOT NULL,
135                    offset INTEGER NOT NULL,
136                    size INTEGER NOT NULL,
137                    body TEXT
138                ) WITHOUT ROWID;
139                INSERT OR IGNORE INTO autoloads_new (name, source, offset, size, body)
140                    SELECT name, source, offset, size, body FROM autoloads;
141                DROP TABLE autoloads;
142                ALTER TABLE autoloads_new RENAME TO autoloads;
143                COMMIT;
144                "#,
145            )?;
146        }
147        self.conn.execute_batch(
148            r#"
149            -- Autoloads: flat table, PRIMARY KEY = clustered index
150            -- body stores actual function definition - NO filesystem access on autoload -Xz
151            -- compinit reads from .zwc or plain files ONCE, stores body here.
152            -- bytecode is held separately in the rkyv autoload-cache shard.
153            CREATE TABLE IF NOT EXISTS autoloads (
154                name TEXT PRIMARY KEY,
155                source TEXT NOT NULL,
156                offset INTEGER NOT NULL,
157                size INTEGER NOT NULL,
158                body TEXT
159            ) WITHOUT ROWID;
160
161            -- zstyle: flat lookup by pattern+style
162            CREATE TABLE IF NOT EXISTS zstyles (
163                pattern TEXT NOT NULL,
164                style TEXT NOT NULL,
165                value TEXT NOT NULL,
166                eval INTEGER DEFAULT 0,
167                PRIMARY KEY (pattern, style)
168            ) WITHOUT ROWID;
169
170            -- Completion mappings: direct key lookup
171            CREATE TABLE IF NOT EXISTS comps (
172                command TEXT PRIMARY KEY,
173                function TEXT NOT NULL
174            ) WITHOUT ROWID;
175
176            -- Pattern completions
177            CREATE TABLE IF NOT EXISTS patcomps (
178                pattern TEXT PRIMARY KEY,
179                function TEXT NOT NULL
180            ) WITHOUT ROWID;
181
182            -- Key completions
183            CREATE TABLE IF NOT EXISTS keycomps (
184                key TEXT PRIMARY KEY,
185                function TEXT NOT NULL
186            ) WITHOUT ROWID;
187
188            -- Services
189            CREATE TABLE IF NOT EXISTS services (
190                command TEXT PRIMARY KEY,
191                service TEXT NOT NULL
192            ) WITHOUT ROWID;
193
194            -- Result cache
195            CREATE TABLE IF NOT EXISTS cache (
196                context TEXT PRIMARY KEY,
197                data BLOB NOT NULL,
198                mtime INTEGER NOT NULL
199            ) WITHOUT ROWID;
200
201            -- PATH executables: flat, fast prefix via FTS5
202            CREATE TABLE IF NOT EXISTS executables (
203                name TEXT PRIMARY KEY,
204                path TEXT NOT NULL
205            ) WITHOUT ROWID;
206
207            -- Named directories
208            CREATE TABLE IF NOT EXISTS named_dirs (
209                name TEXT PRIMARY KEY,
210                path TEXT NOT NULL
211            ) WITHOUT ROWID;
212
213            -- Shell functions
214            CREATE TABLE IF NOT EXISTS shell_functions (
215                name TEXT PRIMARY KEY,
216                source TEXT NOT NULL
217            ) WITHOUT ROWID;
218
219            -- Metadata
220            CREATE TABLE IF NOT EXISTS metadata (
221                key TEXT PRIMARY KEY,
222                value TEXT NOT NULL
223            ) WITHOUT ROWID;
224
225            -- FTS5 for lightning-fast prefix search (standalone, not content-synced)
226            CREATE VIRTUAL TABLE IF NOT EXISTS fts_comps USING fts5(
227                command,
228                tokenize='unicode61'
229            );
230
231            CREATE VIRTUAL TABLE IF NOT EXISTS fts_executables USING fts5(
232                name,
233                tokenize='unicode61'
234            );
235
236            CREATE VIRTUAL TABLE IF NOT EXISTS fts_shell_functions USING fts5(
237                name,
238                tokenize='unicode61'
239            );
240
241            -- Covering index for comps prefix search (fallback if FTS unavailable)
242            CREATE INDEX IF NOT EXISTS idx_comps_cmd ON comps(command);
243            CREATE INDEX IF NOT EXISTS idx_comps_func ON comps(function);
244            CREATE INDEX IF NOT EXISTS idx_executables_name ON executables(name);
245            CREATE INDEX IF NOT EXISTS idx_shell_functions_name ON shell_functions(name);
246            CREATE INDEX IF NOT EXISTS idx_named_dirs_name ON named_dirs(name);
247        "#,
248        )?;
249        self.migrate()?;
250        Ok(())
251    }
252
253    /// Schema migrations for existing databases.
254    ///
255    /// The legacy `bytecode BLOB` re-add path was removed when bytecode moved
256    /// to the rkyv shard (~/.zshrs/autoloads.rkyv). The pre-v0.8.16
257    /// `ast` column is still detected here and dropped — its data was the
258    /// same kind of bytecode and is now obsolete.
259    fn migrate(&self) -> rusqlite::Result<()> {
260        let has_ast: bool = self
261            .conn
262            .prepare("SELECT ast FROM autoloads LIMIT 0")
263            .is_ok();
264        if has_ast {
265            // Recreate-table dance to drop the `ast` column.
266            self.conn.execute_batch(
267                r#"
268                BEGIN;
269                CREATE TABLE autoloads_no_ast (
270                    name TEXT PRIMARY KEY,
271                    source TEXT NOT NULL,
272                    offset INTEGER NOT NULL,
273                    size INTEGER NOT NULL,
274                    body TEXT
275                ) WITHOUT ROWID;
276                INSERT OR IGNORE INTO autoloads_no_ast (name, source, offset, size, body)
277                    SELECT name, source, offset, size, body FROM autoloads;
278                DROP TABLE autoloads;
279                ALTER TABLE autoloads_no_ast RENAME TO autoloads;
280                COMMIT;
281                "#,
282            )?;
283        }
284        Ok(())
285    }
286
287    // =========================================================================
288    // Autoloads - function stubs
289    // =========================================================================
290
291    /// Register an autoload stub (without body)
292    pub fn add_autoload(
293        &self,
294        name: &str,
295        source: &str,
296        offset: i64,
297        size: i64,
298    ) -> rusqlite::Result<()> {
299        self.conn.execute(
300            "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)",
301            params![name, source, offset, size],
302        )?;
303        Ok(())
304    }
305
306    /// Register an autoload with full function body (for instant loading)
307    pub fn add_autoload_with_body(
308        &self,
309        name: &str,
310        source: &str,
311        body: &str,
312    ) -> rusqlite::Result<()> {
313        self.conn.execute(
314            "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)",
315            params![name, source, body.len() as i64, body],
316        )?;
317        Ok(())
318    }
319
320    /// Bulk insert autoloads (much faster)
321    pub fn add_autoloads_bulk(
322        &mut self,
323        autoloads: &[(String, String, i64, i64)],
324    ) -> rusqlite::Result<()> {
325        let tx = self.conn.transaction()?;
326        {
327            let mut stmt = tx.prepare(
328                "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)"
329            )?;
330            for (name, source, offset, size) in autoloads {
331                stmt.execute(params![name, source, offset, size])?;
332            }
333        }
334        tx.commit()?;
335        Ok(())
336    }
337
338    /// Bulk insert autoloads with bodies (for compinit to cache function definitions)
339    pub fn add_autoloads_with_bodies_bulk(
340        &mut self,
341        autoloads: &[(String, String, String)], // (name, source, body)
342    ) -> rusqlite::Result<()> {
343        let tx = self.conn.transaction()?;
344        {
345            let mut stmt = tx.prepare(
346                "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)"
347            )?;
348            for (name, source, body) in autoloads {
349                stmt.execute(params![name, source, body.len() as i64, body])?;
350            }
351        }
352        tx.commit()?;
353        Ok(())
354    }
355
356    /// Lookup autoload by name
357    pub fn get_autoload(&self, name: &str) -> rusqlite::Result<Option<AutoloadStub>> {
358        self.conn
359            .query_row(
360                "SELECT source, offset, size, body FROM autoloads WHERE name = ?1",
361                params![name],
362                |row| {
363                    Ok(AutoloadStub {
364                        name: name.to_string(),
365                        source: row.get(0)?,
366                        offset: row.get(1)?,
367                        size: row.get(2)?,
368                        body: row.get(3)?,
369                    })
370                },
371            )
372            .optional()
373    }
374
375    /// Get function body directly (fast path for autoload -Xz)
376    pub fn get_autoload_body(&self, name: &str) -> rusqlite::Result<Option<String>> {
377        self.conn
378            .query_row(
379                "SELECT body FROM autoloads WHERE name = ?1",
380                params![name],
381                |row| row.get(0),
382            )
383            .optional()
384    }
385
386    /// Count autoloads with a non-NULL body. Replaces the legacy
387    /// `count_autoloads_missing_bytecode` — bytecode coverage is now derived
388    /// by subtracting the rkyv shard's `cached_names` set from this count
389    /// (caller-side, see the `autoload_cache` module in the `zsh` / zshrs library crate).
390    pub fn count_autoloads_with_body(&self) -> rusqlite::Result<usize> {
391        self.conn.query_row(
392            "SELECT COUNT(*) FROM autoloads WHERE body IS NOT NULL",
393            [],
394            |row| row.get::<_, i64>(0).map(|n| n as usize),
395        )
396    }
397
398    /// Get a batch of `(name, body)` pairs for autoloads with a non-NULL
399    /// body, excluding any whose name is in `exclude` (e.g. names already
400    /// present in the rkyv autoload-bytecode shard). Used by compinit's
401    /// background backfill: SQLite supplies the source bodies, the caller
402    /// parses+compiles, then writes results to the rkyv shard.
403    ///
404    /// Returns up to `limit` entries; caller iterates until the empty vec
405    /// or counts what was returned.
406    pub fn get_autoload_bodies_excluding(
407        &self,
408        exclude: &std::collections::HashSet<String>,
409        limit: usize,
410    ) -> rusqlite::Result<Vec<(String, String)>> {
411        let mut stmt = self
412            .conn
413            .prepare("SELECT name, body FROM autoloads WHERE body IS NOT NULL ORDER BY name")?;
414        let rows = stmt.query_map([], |row| {
415            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
416        })?;
417        let mut out = Vec::with_capacity(limit.min(256));
418        for row in rows {
419            let (name, body) = row?;
420            if exclude.contains(&name) {
421                continue;
422            }
423            out.push((name, body));
424            if out.len() >= limit {
425                break;
426            }
427        }
428        Ok(out)
429    }
430
431    /// Get function body with ZWC fallback
432    /// 1. If body column has content, return it (fast path)
433    /// 2. If body is NULL but source/offset/size exist, read from ZWC file
434    /// 3. Returns None if function not found or ZWC read fails
435    pub fn get_autoload_body_or_zwc(&self, name: &str) -> Option<String> {
436        let stub = self.get_autoload(name).ok()??;
437
438        // Fast path: body is cached
439        if let Some(body) = stub.body {
440            return Some(body);
441        }
442
443        // Fallback: read from ZWC file
444        if stub.size > 0 && !stub.source.is_empty() {
445            return Self::read_function_from_zwc(&stub.source, stub.offset, stub.size);
446        }
447
448        None
449    }
450
451    /// Read function body from ZWC file at given offset/size
452    fn read_function_from_zwc(zwc_path: &str, offset: i64, size: i64) -> Option<String> {
453        use std::io::{Read, Seek, SeekFrom};
454
455        let mut file = std::fs::File::open(zwc_path).ok()?;
456        file.seek(SeekFrom::Start(offset as u64)).ok()?;
457
458        let mut buf = vec![0u8; size as usize];
459        file.read_exact(&mut buf).ok()?;
460
461        // ZWC stores tokenized strings - need to untokenize
462        // For now, just try to interpret as UTF-8 (works for most cases)
463        // TODO: proper untokenization like zwc.rs does
464        match String::from_utf8(buf) {
465            Ok(s) => Some(s),
466            Err(e) => Some(String::from_utf8_lossy(e.as_bytes()).into_owned()),
467        }
468    }
469
470    /// Count autoloads
471    pub fn autoload_count(&self) -> rusqlite::Result<i64> {
472        self.conn
473            .query_row("SELECT COUNT(*) FROM autoloads", [], |row| row.get(0))
474    }
475
476    /// List all autoload names (for debugging)
477    pub fn list_autoloads(&self, limit: usize) -> rusqlite::Result<Vec<String>> {
478        let mut stmt = self.conn.prepare("SELECT name FROM autoloads LIMIT ?1")?;
479        let rows = stmt.query_map(params![limit as i64], |row| row.get(0))?;
480        rows.collect()
481    }
482
483    /// List all autoload names (no limit)
484    pub fn list_autoload_names(&self) -> rusqlite::Result<Vec<String>> {
485        let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
486        let rows = stmt.query_map([], |row| row.get(0))?;
487        rows.collect()
488    }
489
490    // =========================================================================
491    // zstyle database
492    // =========================================================================
493
494    /// Set a zstyle
495    pub fn set_zstyle(
496        &self,
497        pattern: &str,
498        style: &str,
499        values: &[String],
500        eval: bool,
501    ) -> rusqlite::Result<()> {
502        let value_json = serde_values_to_json(values);
503        self.conn.execute(
504            "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)",
505            params![pattern, style, value_json, eval as i32],
506        )?;
507        Ok(())
508    }
509
510    /// Bulk insert zstyles
511    pub fn set_zstyles_bulk(
512        &mut self,
513        styles: &[(String, String, Vec<String>, bool)],
514    ) -> rusqlite::Result<()> {
515        let tx = self.conn.transaction()?;
516        {
517            let mut stmt = tx.prepare(
518                "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)"
519            )?;
520            for (pattern, style, values, eval) in styles {
521                let value_json = serde_values_to_json(values);
522                stmt.execute(params![pattern, style, value_json, *eval as i32])?;
523            }
524        }
525        tx.commit()?;
526        Ok(())
527    }
528
529    /// Delete a zstyle
530    pub fn delete_zstyle(&self, pattern: &str, style: Option<&str>) -> rusqlite::Result<usize> {
531        if let Some(s) = style {
532            self.conn.execute(
533                "DELETE FROM zstyles WHERE pattern = ?1 AND style = ?2",
534                params![pattern, s],
535            )
536        } else {
537            self.conn
538                .execute("DELETE FROM zstyles WHERE pattern = ?1", params![pattern])
539        }
540    }
541
542    /// Lookup zstyle - returns all matching patterns sorted by specificity
543    pub fn lookup_zstyle(
544        &self,
545        context: &str,
546        style: &str,
547    ) -> rusqlite::Result<Option<ZStyleEntry>> {
548        let mut stmt = self
549            .conn
550            .prepare("SELECT pattern, value, eval FROM zstyles WHERE style = ?1")?;
551
552        let entries: Vec<(String, String, bool)> = stmt
553            .query_map(params![style], |row| {
554                Ok((row.get(0)?, row.get(1)?, row.get::<_, i32>(2)? != 0))
555            })?
556            .filter_map(|r| r.ok())
557            .collect();
558
559        // Find best match by specificity
560        let mut best: Option<(i32, String, bool)> = None;
561        for (pattern, value, eval) in entries {
562            if pattern_matches_context(&pattern, context) {
563                let weight = calculate_pattern_weight(&pattern);
564                if best.is_none() || weight > best.as_ref().unwrap().0 {
565                    best = Some((weight, value, eval));
566                }
567            }
568        }
569
570        Ok(best.map(|(_, value, eval)| ZStyleEntry {
571            values: serde_json_to_values(&value),
572            eval,
573        }))
574    }
575
576    /// List all zstyles (for `zstyle -L`)
577    #[allow(clippy::type_complexity)]
578    pub fn list_zstyles(&self) -> rusqlite::Result<Vec<(String, String, Vec<String>, bool)>> {
579        let mut stmt = self
580            .conn
581            .prepare("SELECT pattern, style, value, eval FROM zstyles ORDER BY pattern, style")?;
582        let rows = stmt.query_map([], |row| {
583            let pattern: String = row.get(0)?;
584            let style: String = row.get(1)?;
585            let value: String = row.get(2)?;
586            let eval: bool = row.get::<_, i32>(3)? != 0;
587            Ok((pattern, style, serde_json_to_values(&value), eval))
588        })?;
589        rows.collect()
590    }
591
592    /// Count zstyles
593    pub fn zstyle_count(&self) -> rusqlite::Result<i64> {
594        self.conn
595            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
596    }
597
598    // =========================================================================
599    // Completion mappings (_comps)
600    // =========================================================================
601
602    /// Register a completion function for a command
603    pub fn set_comp(&self, command: &str, function: &str) -> rusqlite::Result<()> {
604        self.conn.execute(
605            "INSERT OR REPLACE INTO comps (command, function) VALUES (?1, ?2)",
606            params![command, function],
607        )?;
608        Ok(())
609    }
610
611    /// Bulk insert comps + populate FTS5 index
612    pub fn set_comps_bulk(&mut self, comps: &[(String, String)]) -> rusqlite::Result<()> {
613        let tx = self.conn.transaction()?;
614        // Clear and repopulate both tables
615        tx.execute("DELETE FROM comps", [])?;
616        tx.execute("DELETE FROM fts_comps", [])?;
617        {
618            let mut stmt = tx.prepare("INSERT INTO comps (command, function) VALUES (?1, ?2)")?;
619            let mut fts_stmt = tx.prepare("INSERT INTO fts_comps (command) VALUES (?1)")?;
620            for (command, function) in comps {
621                stmt.execute(params![command, function])?;
622                fts_stmt.execute(params![command])?;
623            }
624        }
625        tx.commit()
626    }
627
628    /// Fast prefix search using FTS5 (O(log n) vs O(n) for LIKE)
629    pub fn comps_prefix_fts(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
630        if prefix.is_empty() {
631            return self.comps_kv();
632        }
633        // FTS5 prefix search: "git*" matches git, github, gitk, etc.
634        let pattern = format!("{}*", prefix);
635        let mut stmt = self.conn.prepare(
636            "SELECT c.command, c.function FROM fts_comps f, comps c WHERE f.command MATCH ?1 AND c.command = f.command"
637        )?;
638        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
639        rows.collect()
640    }
641
642    /// Fast prefix search (LIKE with index scan, ORDER BY is free on indexed column)
643    pub fn comps_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
644        if prefix.is_empty() {
645            return self.comps_kv();
646        }
647        let pattern = format!("{}%", prefix);
648        let mut stmt = self.conn.prepare(
649            "SELECT command, function FROM comps WHERE command LIKE ?1 ORDER BY command",
650        )?;
651        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
652        rows.collect()
653    }
654
655    /// Lookup completion function for command
656    pub fn get_comp(&self, command: &str) -> rusqlite::Result<Option<String>> {
657        self.conn
658            .query_row(
659                "SELECT function FROM comps WHERE command = ?1",
660                params![command],
661                |row| row.get(0),
662            )
663            .optional()
664    }
665
666    /// Get all comps as HashMap (for compatibility)
667    pub fn get_all_comps(&self) -> rusqlite::Result<HashMap<String, String>> {
668        let mut stmt = self.conn.prepare("SELECT command, function FROM comps")?;
669        let rows = stmt.query_map([], |row| {
670            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
671        })?;
672        let mut map = HashMap::new();
673        for row in rows {
674            let (k, v) = row?;
675            map.insert(k, v);
676        }
677        Ok(map)
678    }
679
680    /// Count comps
681    pub fn comp_count(&self) -> rusqlite::Result<i64> {
682        self.conn
683            .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
684    }
685
686    /// Delete a completion registration
687    pub fn delete_comp(&self, command: &str) -> rusqlite::Result<usize> {
688        self.conn
689            .execute("DELETE FROM comps WHERE command = ?1", params![command])
690    }
691
692    // =========================================================================
693    // Pattern completions (_patcomps)
694    // =========================================================================
695
696    /// Register a pattern completion
697    pub fn set_patcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
698        self.conn.execute(
699            "INSERT OR REPLACE INTO patcomps (pattern, function) VALUES (?1, ?2)",
700            params![pattern, function],
701        )?;
702        Ok(())
703    }
704
705    /// Find matching pattern completion
706    pub fn find_patcomp(&self, command: &str) -> rusqlite::Result<Option<String>> {
707        let mut stmt = self
708            .conn
709            .prepare("SELECT pattern, function FROM patcomps")?;
710        let rows = stmt.query_map([], |row| {
711            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
712        })?;
713
714        for row in rows {
715            let (pattern, function) = row?;
716            if glob_matches(&pattern, command) {
717                return Ok(Some(function));
718            }
719        }
720        Ok(None)
721    }
722
723    // =========================================================================
724    // Key completions
725    // =========================================================================
726
727    /// Register a key completion (for -K)
728    pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
729        self.conn.execute(
730            "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
731            params![key, function],
732        )?;
733        Ok(())
734    }
735
736    /// Lookup key completion
737    pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
738        self.conn
739            .query_row(
740                "SELECT function FROM keycomps WHERE key = ?1",
741                params![key],
742                |row| row.get(0),
743            )
744            .optional()
745    }
746
747    // =========================================================================
748    // Result cache
749    // =========================================================================
750
751    /// Cache completion results
752    pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
753        self.conn.execute(
754            "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
755            params![context, data, mtime],
756        )?;
757        Ok(())
758    }
759
760    /// Get cached results if not stale
761    pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
762        let now = std::time::SystemTime::now()
763            .duration_since(std::time::UNIX_EPOCH)
764            .unwrap()
765            .as_secs() as i64;
766
767        self.conn
768            .query_row(
769                "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
770                params![context, now - max_age],
771                |row| row.get(0),
772            )
773            .optional()
774    }
775
776    /// Clear old cache entries
777    pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
778        let now = std::time::SystemTime::now()
779            .duration_since(std::time::UNIX_EPOCH)
780            .unwrap()
781            .as_secs() as i64;
782
783        self.conn
784            .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
785    }
786
787    /// Clear all cache
788    pub fn clear_cache(&self) -> rusqlite::Result<()> {
789        self.conn.execute("DELETE FROM cache", [])?;
790        Ok(())
791    }
792
793    // =========================================================================
794    // Maintenance
795    // =========================================================================
796
797    /// Vacuum database
798    pub fn vacuum(&self) -> rusqlite::Result<()> {
799        self.conn.execute("VACUUM", [])?;
800        Ok(())
801    }
802
803    /// Get database stats
804    pub fn stats(&self) -> rusqlite::Result<CacheStats> {
805        Ok(CacheStats {
806            autoloads: self.autoload_count()?,
807            zstyles: self.zstyle_count()?,
808            comps: self.comp_count()?,
809            patcomps: self
810                .conn
811                .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
812            keycomps: self
813                .conn
814                .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
815            services: self
816                .conn
817                .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
818            cache_entries: self
819                .conn
820                .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
821        })
822    }
823}
824
825/// Autoload stub info
826#[derive(Debug, Clone)]
827pub struct AutoloadStub {
828    /// `name` field.
829    pub name: String,
830    /// `source` field.
831    pub source: String,
832    /// `offset` field.
833    pub offset: i64,
834    /// `size` field.
835    pub size: i64,
836    /// Cached function body - if present, no need to read from source file
837    pub body: Option<String>,
838}
839
840/// zstyle entry
841#[derive(Debug, Clone)]
842pub struct ZStyleEntry {
843    /// `values` field.
844    pub values: Vec<String>,
845    /// `eval` field.
846    pub eval: bool,
847}
848
849/// Cache statistics
850#[derive(Debug)]
851pub struct CacheStats {
852    /// `autoloads` field.
853    pub autoloads: i64,
854    /// `zstyles` field.
855    pub zstyles: i64,
856    /// `comps` field.
857    pub comps: i64,
858    /// `patcomps` field.
859    pub patcomps: i64,
860    /// `keycomps` field.
861    pub keycomps: i64,
862    /// `services` field.
863    pub services: i64,
864    /// `cache_entries` field.
865    pub cache_entries: i64,
866}
867
868// Helper: serialize values to JSON
869fn serde_values_to_json(values: &[String]) -> String {
870    let escaped: Vec<String> = values
871        .iter()
872        .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
873        .collect();
874    format!("[{}]", escaped.join(","))
875}
876
877// Helper: deserialize JSON to values
878fn serde_json_to_values(json: &str) -> Vec<String> {
879    let trimmed = json.trim();
880    if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
881        return vec![json.to_string()];
882    }
883
884    let inner = &trimmed[1..trimmed.len() - 1];
885    if inner.is_empty() {
886        return vec![];
887    }
888
889    let mut values = Vec::new();
890    let mut current = String::new();
891    let mut in_string = false;
892    let mut escape = false;
893
894    for c in inner.chars() {
895        if escape {
896            current.push(c);
897            escape = false;
898        } else if c == '\\' {
899            escape = true;
900        } else if c == '"' {
901            in_string = !in_string;
902        } else if c == ',' && !in_string {
903            values.push(current.trim().to_string());
904            current = String::new();
905        } else {
906            current.push(c);
907        }
908    }
909    if !current.is_empty() {
910        values.push(current.trim().to_string());
911    }
912
913    values
914}
915
916// Helper: check if zstyle pattern matches context
917fn pattern_matches_context(pattern: &str, context: &str) -> bool {
918    let pat_parts: Vec<&str> = pattern.split(':').collect();
919    let ctx_parts: Vec<&str> = context.split(':').collect();
920
921    if pat_parts.len() > ctx_parts.len() {
922        return false;
923    }
924
925    for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
926        if *p != "*" && *p != *c {
927            return false;
928        }
929    }
930
931    true
932}
933
934// Helper: calculate pattern weight for specificity
935fn calculate_pattern_weight(pattern: &str) -> i32 {
936    let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
937    let mut weight = parts.len() as i32 * 100;
938
939    for part in &parts {
940        if *part != "*" {
941            weight += 10;
942        }
943    }
944
945    weight
946}
947
948// Helper: glob matching for patcomps
949fn glob_matches(pattern: &str, text: &str) -> bool {
950    let mut pat_chars = pattern.chars().peekable();
951    let mut txt_chars = text.chars().peekable();
952
953    while let Some(p) = pat_chars.next() {
954        match p {
955            '*' => {
956                if pat_chars.peek().is_none() {
957                    return true;
958                }
959                while txt_chars.peek().is_some() {
960                    if glob_matches(
961                        &pat_chars.clone().collect::<String>(),
962                        &txt_chars.clone().collect::<String>(),
963                    ) {
964                        return true;
965                    }
966                    txt_chars.next();
967                }
968                return false;
969            }
970            '?' => {
971                if txt_chars.next().is_none() {
972                    return false;
973                }
974            }
975            c => {
976                if txt_chars.next() != Some(c) {
977                    return false;
978                }
979            }
980        }
981    }
982
983    txt_chars.peek().is_none()
984}
985
986// =========================================================================
987// Shell-visible arrays (_comps, _services, _patcomps, etc.)
988// These back the zsh special arrays that users query with $#_comps etc.
989// =========================================================================
990
991impl CompsysCache {
992    /// Get count of _comps entries (for $#_comps)
993    pub fn comps_count(&self) -> rusqlite::Result<i64> {
994        self.comp_count()
995    }
996
997    /// Get all _comps keys (for ${(k)_comps}) - ORDER BY is free on PRIMARY KEY
998    pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
999        let mut stmt = self
1000            .conn
1001            .prepare("SELECT command FROM comps ORDER BY command")?;
1002        let rows = stmt.query_map([], |row| row.get(0))?;
1003        rows.collect()
1004    }
1005
1006    /// Get all _comps values (for ${(v)_comps})
1007    pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1008        let mut stmt = self
1009            .conn
1010            .prepare("SELECT function FROM comps ORDER BY command")?;
1011        let rows = stmt.query_map([], |row| row.get(0))?;
1012        rows.collect()
1013    }
1014
1015    /// Get _comps as key-value pairs (for ${(kv)_comps})
1016    pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1017        let mut stmt = self
1018            .conn
1019            .prepare("SELECT command, function FROM comps ORDER BY command")?;
1020        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1021        rows.collect()
1022    }
1023
1024    // --- _patcomps ---
1025
1026    /// Get count of _patcomps
1027    pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1028        self.conn
1029            .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1030    }
1031
1032    /// Get all _patcomps keys
1033    pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1034        let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1035        let rows = stmt.query_map([], |row| row.get(0))?;
1036        rows.collect()
1037    }
1038
1039    /// Get all _patcomps as kv
1040    pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1041        let mut stmt = self
1042            .conn
1043            .prepare("SELECT pattern, function FROM patcomps")?;
1044        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1045        rows.collect()
1046    }
1047
1048    // --- _services ---
1049
1050    /// Set a service mapping
1051    pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1052        self.conn.execute(
1053            "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1054            params![command, service],
1055        )?;
1056        Ok(())
1057    }
1058
1059    /// Get service for command
1060    pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1061        self.conn
1062            .query_row(
1063                "SELECT service FROM services WHERE command = ?1",
1064                params![command],
1065                |row| row.get(0),
1066            )
1067            .optional()
1068    }
1069
1070    /// Get count of _services
1071    pub fn services_count(&self) -> rusqlite::Result<i64> {
1072        self.conn
1073            .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1074    }
1075
1076    /// Get all _services keys
1077    pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1078        let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1079        let rows = stmt.query_map([], |row| row.get(0))?;
1080        rows.collect()
1081    }
1082
1083    /// Bulk insert services
1084    pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1085        let tx = self.conn.transaction()?;
1086        {
1087            let mut stmt =
1088                tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1089            for (command, service) in services {
1090                stmt.execute(params![command, service])?;
1091            }
1092        }
1093        tx.commit()?;
1094        Ok(())
1095    }
1096
1097    // --- _compautos (autoloaded completion functions) ---
1098
1099    /// Get count of autoloaded functions
1100    pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1101        self.autoload_count()
1102    }
1103
1104    /// Get all autoload names (for ${(k)_compautos})
1105    pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1106        let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1107        let rows = stmt.query_map([], |row| row.get(0))?;
1108        rows.collect()
1109    }
1110
1111    // =========================================================================
1112    // PATH executables cache
1113    // =========================================================================
1114
1115    /// Check if executables cache is populated
1116    pub fn has_executables(&self) -> rusqlite::Result<bool> {
1117        let count: i64 = self
1118            .conn
1119            .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1120        Ok(count > 0)
1121    }
1122
1123    /// Store executables in bulk + populate FTS5 index
1124    pub fn set_executables_bulk(
1125        &mut self,
1126        executables: &[(String, String)],
1127    ) -> rusqlite::Result<()> {
1128        let tx = self.conn.transaction()?;
1129        tx.execute("DELETE FROM executables", [])?;
1130        tx.execute("DELETE FROM fts_executables", [])?;
1131        {
1132            let mut stmt =
1133                tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1134            let mut fts_stmt =
1135                tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1136            for (name, path) in executables {
1137                stmt.execute(params![name, path])?;
1138                fts_stmt.execute(params![name])?;
1139            }
1140        }
1141        tx.commit()
1142    }
1143
1144    /// Get all executable names (fast lookup set)
1145    pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1146        let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1147        let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1148        rows.collect::<Result<std::collections::HashSet<_>, _>>()
1149    }
1150
1151    /// Check if an executable exists in cache (O(1) lookup)
1152    pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1153        // Use EXISTS for faster check (stops at first match)
1154        let exists: i64 = self.conn.query_row(
1155            "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1156            params![name],
1157            |row| row.get(0),
1158        )?;
1159        Ok(exists == 1)
1160    }
1161
1162    /// Get executable path by name (direct key lookup)
1163    pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1164        self.conn
1165            .query_row(
1166                "SELECT path FROM executables WHERE name = ?1",
1167                params![name],
1168                |row| row.get(0),
1169            )
1170            .optional()
1171    }
1172
1173    /// Fast prefix search using FTS5
1174    pub fn get_executables_prefix_fts(
1175        &self,
1176        prefix: &str,
1177    ) -> rusqlite::Result<Vec<(String, String)>> {
1178        if prefix.is_empty() {
1179            let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1180            let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1181            return rows.collect();
1182        }
1183        let pattern = format!("{}*", prefix);
1184        let mut stmt = self.conn.prepare(
1185            "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1186        )?;
1187        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1188        rows.collect()
1189    }
1190
1191    /// Get executables matching prefix (LIKE with index, ORDER BY free on PRIMARY KEY)
1192    pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1193        if prefix.is_empty() {
1194            let mut stmt = self
1195                .conn
1196                .prepare("SELECT name, path FROM executables ORDER BY name")?;
1197            let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1198            return rows.collect();
1199        }
1200        let pattern = format!("{}%", prefix);
1201        let mut stmt = self
1202            .conn
1203            .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1204        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1205        rows.collect()
1206    }
1207
1208    /// Count executables
1209    pub fn executables_count(&self) -> rusqlite::Result<i64> {
1210        self.conn
1211            .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1212    }
1213
1214    // =========================================================================
1215    // Named directories cache (hash -d)
1216    // =========================================================================
1217
1218    /// Check if named_dirs cache is populated
1219    pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1220        let count: i64 = self
1221            .conn
1222            .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1223        Ok(count > 0)
1224    }
1225
1226    /// Store named directories in bulk (clears existing)
1227    pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1228        let tx = self.conn.transaction()?;
1229        tx.execute("DELETE FROM named_dirs", [])?;
1230        {
1231            let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1232            for (name, path) in dirs {
1233                stmt.execute(params![name, path])?;
1234            }
1235        }
1236        tx.commit()
1237    }
1238
1239    /// Get all named directories (ORDER BY free on PRIMARY KEY)
1240    pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1241        let mut stmt = self
1242            .conn
1243            .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1244        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1245        rows.collect()
1246    }
1247
1248    /// Get named directories matching prefix
1249    pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1250        if prefix.is_empty() {
1251            return self.get_named_dirs();
1252        }
1253        let pattern = format!("{}%", prefix);
1254        let mut stmt = self
1255            .conn
1256            .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1257        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1258        rows.collect()
1259    }
1260
1261    /// Count named directories
1262    pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1263        self.conn
1264            .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1265    }
1266
1267    // =========================================================================
1268    // Shell functions cache (FPATH)
1269    // =========================================================================
1270
1271    /// Check if shell_functions cache is populated
1272    pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1273        let count: i64 =
1274            self.conn
1275                .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1276        Ok(count > 0)
1277    }
1278
1279    /// Store shell functions in bulk + populate FTS5 index
1280    pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1281        let tx = self.conn.transaction()?;
1282        tx.execute("DELETE FROM shell_functions", [])?;
1283        tx.execute("DELETE FROM fts_shell_functions", [])?;
1284        {
1285            let mut stmt =
1286                tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1287            let mut fts_stmt =
1288                tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1289            for (name, source) in funcs {
1290                stmt.execute(params![name, source])?;
1291                fts_stmt.execute(params![name])?;
1292            }
1293        }
1294        tx.commit()
1295    }
1296
1297    /// Get all shell function names (ORDER BY free on PRIMARY KEY)
1298    pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1299        let mut stmt = self
1300            .conn
1301            .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1302        let rows = stmt.query_map([], |row| row.get(0))?;
1303        rows.collect()
1304    }
1305
1306    /// Get shell functions with source paths
1307    pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1308        let mut stmt = self
1309            .conn
1310            .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1311        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1312        rows.collect()
1313    }
1314
1315    /// Fast prefix search using FTS5 (note: FTS5 doesn't preserve order, needs post-sort)
1316    pub fn get_shell_functions_prefix_fts(
1317        &self,
1318        prefix: &str,
1319    ) -> rusqlite::Result<Vec<(String, String)>> {
1320        if prefix.is_empty() {
1321            return self.get_shell_functions();
1322        }
1323        let pattern = format!("{}*", prefix);
1324        let mut stmt = self.conn.prepare(
1325            "SELECT s.name, s.source FROM fts_shell_functions f, shell_functions s WHERE f.name MATCH ?1 AND s.name = f.name ORDER BY s.name"
1326        )?;
1327        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1328        rows.collect()
1329    }
1330
1331    /// Get shell functions matching prefix (LIKE with index, ORDER BY free)
1332    pub fn get_shell_functions_prefix(
1333        &self,
1334        prefix: &str,
1335    ) -> rusqlite::Result<Vec<(String, String)>> {
1336        if prefix.is_empty() {
1337            return self.get_shell_functions();
1338        }
1339        let pattern = format!("{}%", prefix);
1340        let mut stmt = self
1341            .conn
1342            .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1343        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1344        rows.collect()
1345    }
1346
1347    /// Count shell functions
1348    pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1349        self.conn
1350            .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1351    }
1352
1353    // =========================================================================
1354    // Metadata for cache versioning/invalidation
1355    // =========================================================================
1356
1357    /// Set metadata key-value
1358    pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1359        self.conn.execute(
1360            "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1361            params![key, value],
1362        )?;
1363        Ok(())
1364    }
1365
1366    /// Get metadata value
1367    pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1368        self.conn
1369            .query_row(
1370                "SELECT value FROM metadata WHERE key = ?1",
1371                params![key],
1372                |row| row.get(0),
1373            )
1374            .optional()
1375    }
1376
1377    // =========================================================================
1378    // Zstyle helpers
1379    // =========================================================================
1380
1381    /// Check if zstyles cache is populated
1382    pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1383        let count: i64 = self
1384            .conn
1385            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1386        Ok(count > 0)
1387    }
1388
1389    /// Count zstyles
1390    pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1391        self.conn
1392            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1393    }
1394
1395    /// Get all zstyles (for debugging)
1396    pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1397        let mut stmt = self
1398            .conn
1399            .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1400        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1401        rows.collect()
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408
1409    #[test]
1410    fn test_cache_basic() {
1411        let cache = CompsysCache::memory().unwrap();
1412
1413        cache
1414            .add_autoload("_git", "more_src.zwc", 1024, 5000)
1415            .unwrap();
1416        cache
1417            .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1418            .unwrap();
1419
1420        let stub = cache.get_autoload("_git").unwrap().unwrap();
1421        assert_eq!(stub.source, "more_src.zwc");
1422        assert_eq!(stub.offset, 1024);
1423
1424        assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1425    }
1426
1427    #[test]
1428    fn test_zstyle_cache() {
1429        let cache = CompsysCache::memory().unwrap();
1430
1431        cache
1432            .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1433            .unwrap();
1434        cache
1435            .set_zstyle(
1436                ":completion:*:descriptions",
1437                "format",
1438                &["%d".to_string()],
1439                false,
1440            )
1441            .unwrap();
1442
1443        let entry = cache
1444            .lookup_zstyle(":completion:foo", "menu")
1445            .unwrap()
1446            .unwrap();
1447        assert_eq!(entry.values, vec!["select"]);
1448
1449        let entry = cache
1450            .lookup_zstyle(":completion:foo:descriptions", "format")
1451            .unwrap()
1452            .unwrap();
1453        assert_eq!(entry.values, vec!["%d"]);
1454    }
1455
1456    #[test]
1457    fn test_zstyle_specificity() {
1458        let cache = CompsysCache::memory().unwrap();
1459
1460        cache
1461            .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1462            .unwrap();
1463        cache
1464            .set_zstyle(
1465                ":completion:*:*:*:default",
1466                "menu",
1467                &["yes".to_string()],
1468                false,
1469            )
1470            .unwrap();
1471
1472        let entry = cache
1473            .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1474            .unwrap()
1475            .unwrap();
1476        assert_eq!(entry.values, vec!["yes"]);
1477    }
1478
1479    #[test]
1480    fn test_comps_cache() {
1481        let mut cache = CompsysCache::memory().unwrap();
1482
1483        let comps = vec![
1484            ("git".to_string(), "_git".to_string()),
1485            ("docker".to_string(), "_docker".to_string()),
1486            ("cargo".to_string(), "_cargo".to_string()),
1487        ];
1488        cache.set_comps_bulk(&comps).unwrap();
1489
1490        assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1491        assert_eq!(
1492            cache.get_comp("docker").unwrap(),
1493            Some("_docker".to_string())
1494        );
1495        assert!(cache.get_comp("nonexistent").unwrap().is_none());
1496
1497        assert_eq!(cache.comp_count().unwrap(), 3);
1498    }
1499
1500    #[test]
1501    fn test_bulk_autoloads() {
1502        let mut cache = CompsysCache::memory().unwrap();
1503
1504        let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1505            .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1506            .collect();
1507
1508        cache.add_autoloads_bulk(&autoloads).unwrap();
1509        assert_eq!(cache.autoload_count().unwrap(), 1000);
1510
1511        let stub = cache.get_autoload("_func500").unwrap().unwrap();
1512        assert_eq!(stub.offset, 50000);
1513        assert!(stub.body.is_none()); // No body when bulk inserted without
1514    }
1515
1516    #[test]
1517    fn test_autoload_with_body() {
1518        let cache = CompsysCache::memory().unwrap();
1519
1520        let body = r#"
1521local -a opts
1522opts=(--help --version --verbose)
1523_arguments $opts
1524"#;
1525        cache
1526            .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1527            .unwrap();
1528
1529        let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1530        assert_eq!(stub.body.as_deref(), Some(body));
1531        assert_eq!(stub.size, body.len() as i64);
1532
1533        // Fast path: get body directly
1534        let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1535        assert_eq!(direct_body.as_deref(), Some(body));
1536    }
1537
1538    #[test]
1539    fn test_bulk_autoloads_with_bodies() {
1540        let mut cache = CompsysCache::memory().unwrap();
1541
1542        let autoloads: Vec<(String, String, String)> = (0..100)
1543            .map(|i| {
1544                (
1545                    format!("_func{}", i),
1546                    format!("/path/to/_func{}", i),
1547                    format!("# Function {}\necho hello", i),
1548                )
1549            })
1550            .collect();
1551
1552        cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1553        assert_eq!(cache.autoload_count().unwrap(), 100);
1554
1555        let stub = cache.get_autoload("_func50").unwrap().unwrap();
1556        assert!(stub.body.is_some());
1557        assert!(stub.body.unwrap().contains("Function 50"));
1558    }
1559
1560    #[test]
1561    fn test_get_autoload_body_or_zwc_with_body() {
1562        let cache = CompsysCache::memory().unwrap();
1563
1564        let body = "echo from sqlite";
1565        cache
1566            .add_autoload_with_body("_cached", "/some/path", body)
1567            .unwrap();
1568
1569        // Should return body from SQLite (fast path)
1570        let result = cache.get_autoload_body_or_zwc("_cached");
1571        assert_eq!(result, Some(body.to_string()));
1572    }
1573
1574    #[test]
1575    fn test_get_autoload_body_or_zwc_no_body() {
1576        let cache = CompsysCache::memory().unwrap();
1577
1578        // Add autoload without body (just ZWC reference)
1579        cache
1580            .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1581            .unwrap();
1582
1583        // Should return None since ZWC file doesn't exist
1584        let result = cache.get_autoload_body_or_zwc("_nocache");
1585        assert!(result.is_none());
1586    }
1587
1588    #[test]
1589    fn test_get_autoload_body_or_zwc_not_found() {
1590        let cache = CompsysCache::memory().unwrap();
1591
1592        // Function doesn't exist at all
1593        let result = cache.get_autoload_body_or_zwc("_nonexistent");
1594        assert!(result.is_none());
1595    }
1596
1597    #[test]
1598    fn test_patcomp() {
1599        let cache = CompsysCache::memory().unwrap();
1600
1601        cache.set_patcomp("git-*", "_git").unwrap();
1602        cache.set_patcomp("docker-*", "_docker").unwrap();
1603
1604        assert_eq!(
1605            cache.find_patcomp("git-commit").unwrap(),
1606            Some("_git".to_string())
1607        );
1608        assert_eq!(
1609            cache.find_patcomp("docker-compose").unwrap(),
1610            Some("_docker".to_string())
1611        );
1612        assert!(cache.find_patcomp("cargo").unwrap().is_none());
1613    }
1614
1615    #[test]
1616    fn test_glob_matches() {
1617        assert!(glob_matches("git-*", "git-commit"));
1618        assert!(glob_matches("*-compose", "docker-compose"));
1619        // `*.rs` cannot match `zle_main` (no `.rs` extension); the
1620        // glob is anchored at both ends, and `.rs` is literal.
1621        assert!(!glob_matches("*.rs", "zle_main"));
1622        assert!(!glob_matches("git-*", "docker-compose"));
1623        assert!(glob_matches("???", "abc"));
1624        assert!(!glob_matches("???", "abcd"));
1625    }
1626
1627    #[test]
1628    fn test_json_serde() {
1629        let values = vec!["hello".to_string(), "world".to_string()];
1630        let json = serde_values_to_json(&values);
1631        let back = serde_json_to_values(&json);
1632        assert_eq!(back, values);
1633
1634        let values = vec!["with \"quotes\"".to_string()];
1635        let json = serde_values_to_json(&values);
1636        let back = serde_json_to_values(&json);
1637        assert_eq!(back, vec!["with \"quotes\""]);
1638    }
1639
1640    #[test]
1641    fn test_stats() {
1642        let mut cache = CompsysCache::memory().unwrap();
1643
1644        cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1645        cache
1646            .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1647            .unwrap();
1648        cache.set_comp("git", "_git").unwrap();
1649
1650        let stats = cache.stats().unwrap();
1651        assert_eq!(stats.autoloads, 1);
1652        assert_eq!(stats.zstyles, 1);
1653        assert_eq!(stats.comps, 1);
1654    }
1655
1656    #[test]
1657    fn test_large_scale() {
1658        let mut cache = CompsysCache::memory().unwrap();
1659
1660        // Simulate 500k autoloads
1661        let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1662            .map(|i| {
1663                (
1664                    format!("_func{}", i),
1665                    format!("src{}.zwc", i % 10),
1666                    i * 50,
1667                    50,
1668                )
1669            })
1670            .collect();
1671
1672        cache.add_autoloads_bulk(&autoloads).unwrap();
1673
1674        // Fast lookup
1675        let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1676        assert_eq!(stub.offset, 9999 * 50);
1677
1678        assert_eq!(cache.autoload_count().unwrap(), 10000);
1679    }
1680
1681    #[test]
1682    fn test_executables_cache() {
1683        let mut cache = CompsysCache::memory().unwrap();
1684
1685        let executables = vec![
1686            ("ls".to_string(), "/bin/ls".to_string()),
1687            ("cat".to_string(), "/bin/cat".to_string()),
1688            ("git".to_string(), "/usr/bin/git".to_string()),
1689        ];
1690        cache.set_executables_bulk(&executables).unwrap();
1691
1692        assert!(cache.has_executables().unwrap());
1693        assert!(cache.has_executable("ls").unwrap());
1694        assert!(cache.has_executable("git").unwrap());
1695        assert!(!cache.has_executable("nonexistent").unwrap());
1696
1697        assert_eq!(
1698            cache.get_executable_path("ls").unwrap(),
1699            Some("/bin/ls".to_string())
1700        );
1701        assert_eq!(cache.executables_count().unwrap(), 3);
1702    }
1703
1704    #[test]
1705    fn test_executables_prefix_search() {
1706        let mut cache = CompsysCache::memory().unwrap();
1707
1708        let executables = vec![
1709            ("git".to_string(), "/usr/bin/git".to_string()),
1710            ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1711            ("grep".to_string(), "/bin/grep".to_string()),
1712            ("gzip".to_string(), "/bin/gzip".to_string()),
1713        ];
1714        cache.set_executables_bulk(&executables).unwrap();
1715
1716        // FTS prefix search returns (name, path) tuples
1717        let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1718        assert_eq!(git_cmds.len(), 2);
1719        assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1720        assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1721
1722        let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1723        assert_eq!(g_cmds.len(), 4);
1724    }
1725
1726    #[test]
1727    fn test_named_dirs_cache() {
1728        let mut cache = CompsysCache::memory().unwrap();
1729
1730        let dirs = vec![
1731            ("proj".to_string(), "/home/user/projects".to_string()),
1732            ("docs".to_string(), "/home/user/documents".to_string()),
1733        ];
1734        cache.set_named_dirs_bulk(&dirs).unwrap();
1735
1736        assert!(cache.has_named_dirs().unwrap());
1737
1738        let all = cache.get_named_dirs().unwrap();
1739        assert_eq!(all.len(), 2);
1740
1741        let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1742        assert_eq!(p_dirs.len(), 1);
1743        assert_eq!(p_dirs[0].0, "proj");
1744    }
1745
1746    #[test]
1747    fn test_shell_functions_cache() {
1748        let mut cache = CompsysCache::memory().unwrap();
1749
1750        let functions = vec![
1751            ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1752            (
1753                "zpwrClearList".to_string(),
1754                "/home/user/.zpwr/autoload".to_string(),
1755            ),
1756            (
1757                "zpwrTop".to_string(),
1758                "/home/user/.zpwr/autoload".to_string(),
1759            ),
1760        ];
1761        cache.set_shell_functions_bulk(&functions).unwrap();
1762
1763        assert!(cache.has_shell_functions().unwrap());
1764        assert_eq!(cache.shell_functions_count().unwrap(), 3);
1765
1766        let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1767        assert_eq!(zpwr.len(), 2);
1768        // Results are tuples (name, source)
1769        assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1770        assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1771    }
1772
1773    #[test]
1774    fn test_metadata() {
1775        let cache = CompsysCache::memory().unwrap();
1776
1777        cache.set_metadata("version", "1.0.0").unwrap();
1778        cache.set_metadata("build_time", "2026-04-22").unwrap();
1779
1780        assert_eq!(
1781            cache.get_metadata("version").unwrap(),
1782            Some("1.0.0".to_string())
1783        );
1784        assert_eq!(
1785            cache.get_metadata("build_time").unwrap(),
1786            Some("2026-04-22".to_string())
1787        );
1788        assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1789    }
1790
1791    #[test]
1792    fn test_comps_keys() {
1793        let mut cache = CompsysCache::memory().unwrap();
1794
1795        let comps = vec![
1796            ("git".to_string(), "_git".to_string()),
1797            ("docker".to_string(), "_docker".to_string()),
1798        ];
1799        cache.set_comps_bulk(&comps).unwrap();
1800
1801        let keys = cache.comps_keys().unwrap();
1802        assert_eq!(keys.len(), 2);
1803        assert!(keys.contains(&"docker".to_string()));
1804        assert!(keys.contains(&"git".to_string()));
1805    }
1806
1807    #[test]
1808    fn test_comps_prefix() {
1809        let mut cache = CompsysCache::memory().unwrap();
1810
1811        let comps = vec![
1812            ("git".to_string(), "_git".to_string()),
1813            ("gitk".to_string(), "_gitk".to_string()),
1814            ("docker".to_string(), "_docker".to_string()),
1815        ];
1816        cache.set_comps_bulk(&comps).unwrap();
1817
1818        let git_comps = cache.comps_prefix("git").unwrap();
1819        assert_eq!(git_comps.len(), 2);
1820    }
1821
1822    #[test]
1823    fn test_zstyles_bulk() {
1824        let mut cache = CompsysCache::memory().unwrap();
1825
1826        let styles = vec![
1827            (
1828                ":completion:*".to_string(),
1829                "menu".to_string(),
1830                vec!["select".to_string()],
1831                false,
1832            ),
1833            (
1834                ":completion:*".to_string(),
1835                "verbose".to_string(),
1836                vec!["yes".to_string()],
1837                false,
1838            ),
1839            (
1840                ":completion:*:descriptions".to_string(),
1841                "format".to_string(),
1842                vec!["%d".to_string()],
1843                false,
1844            ),
1845        ];
1846        cache.set_zstyles_bulk(&styles).unwrap();
1847
1848        assert!(cache.has_zstyles().unwrap());
1849        assert_eq!(cache.zstyles_count().unwrap(), 3);
1850    }
1851
1852    #[test]
1853    fn test_services() {
1854        let cache = CompsysCache::memory().unwrap();
1855
1856        cache.set_service("git", "scm").unwrap();
1857        cache.set_service("hg", "scm").unwrap();
1858
1859        assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
1860        assert_eq!(cache.get_service("unknown").unwrap(), None);
1861    }
1862
1863    #[test]
1864    fn test_cache_overwrite() {
1865        let cache = CompsysCache::memory().unwrap();
1866
1867        cache.set_comp("git", "_git_old").unwrap();
1868        assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
1869
1870        cache.set_comp("git", "_git_new").unwrap();
1871        assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
1872    }
1873
1874    #[test]
1875    fn test_executable_names() {
1876        let mut cache = CompsysCache::memory().unwrap();
1877
1878        let executables = vec![
1879            ("alpha".to_string(), "/bin/alpha".to_string()),
1880            ("beta".to_string(), "/bin/beta".to_string()),
1881            ("gamma".to_string(), "/bin/gamma".to_string()),
1882        ];
1883        cache.set_executables_bulk(&executables).unwrap();
1884
1885        let names = cache.get_executable_names().unwrap();
1886        assert_eq!(names.len(), 3);
1887        // Returns a HashSet, so check contains
1888        assert!(names.contains("alpha"));
1889        assert!(names.contains("beta"));
1890        assert!(names.contains("gamma"));
1891    }
1892}