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