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