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 - need to untokenize
613        // For now, just try to interpret as UTF-8 (works for most cases)
614        // TODO: proper untokenization like zwc.rs does
615        match String::from_utf8(buf) {
616            Ok(s) => Some(s),
617            Err(e) => Some(String::from_utf8_lossy(e.as_bytes()).into_owned()),
618        }
619    }
620
621    /// Count autoloads
622    pub fn autoload_count(&self) -> rusqlite::Result<i64> {
623        self.conn
624            .query_row("SELECT COUNT(*) FROM autoloads", [], |row| row.get(0))
625    }
626
627    /// List all autoload names (for debugging)
628    pub fn list_autoloads(&self, limit: usize) -> rusqlite::Result<Vec<String>> {
629        let mut stmt = self.conn.prepare("SELECT name FROM autoloads LIMIT ?1")?;
630        let rows = stmt.query_map(params![limit as i64], |row| row.get(0))?;
631        rows.collect()
632    }
633
634    /// List all autoload names (no limit)
635    pub fn list_autoload_names(&self) -> rusqlite::Result<Vec<String>> {
636        let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
637        let rows = stmt.query_map([], |row| row.get(0))?;
638        rows.collect()
639    }
640
641    // =========================================================================
642    // zstyle database
643    // =========================================================================
644
645    /// Set a zstyle
646    pub fn set_zstyle(
647        &self,
648        pattern: &str,
649        style: &str,
650        values: &[String],
651        eval: bool,
652    ) -> rusqlite::Result<()> {
653        let value_json = serde_values_to_json(values);
654        self.conn.execute(
655            "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)",
656            params![pattern, style, value_json, eval as i32],
657        )?;
658        Ok(())
659    }
660
661    /// Bulk insert zstyles
662    pub fn set_zstyles_bulk(
663        &mut self,
664        styles: &[(String, String, Vec<String>, bool)],
665    ) -> rusqlite::Result<()> {
666        let tx = self.conn.transaction()?;
667        {
668            let mut stmt = tx.prepare(
669                "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)"
670            )?;
671            for (pattern, style, values, eval) in styles {
672                let value_json = serde_values_to_json(values);
673                stmt.execute(params![pattern, style, value_json, *eval as i32])?;
674            }
675        }
676        tx.commit()?;
677        Ok(())
678    }
679
680    /// Delete a zstyle
681    pub fn delete_zstyle(&self, pattern: &str, style: Option<&str>) -> rusqlite::Result<usize> {
682        if let Some(s) = style {
683            self.conn.execute(
684                "DELETE FROM zstyles WHERE pattern = ?1 AND style = ?2",
685                params![pattern, s],
686            )
687        } else {
688            self.conn
689                .execute("DELETE FROM zstyles WHERE pattern = ?1", params![pattern])
690        }
691    }
692
693    /// Lookup zstyle - returns all matching patterns sorted by specificity
694    pub fn lookup_zstyle(
695        &self,
696        context: &str,
697        style: &str,
698    ) -> rusqlite::Result<Option<ZStyleEntry>> {
699        let mut stmt = self
700            .conn
701            .prepare("SELECT pattern, value, eval FROM zstyles WHERE style = ?1")?;
702
703        let entries: Vec<(String, String, bool)> = stmt
704            .query_map(params![style], |row| {
705                Ok((row.get(0)?, row.get(1)?, row.get::<_, i32>(2)? != 0))
706            })?
707            .filter_map(|r| r.ok())
708            .collect();
709
710        // Find best match by specificity
711        let mut best: Option<(i32, String, bool)> = None;
712        for (pattern, value, eval) in entries {
713            if pattern_matches_context(&pattern, context) {
714                let weight = calculate_pattern_weight(&pattern);
715                if best.is_none() || weight > best.as_ref().unwrap().0 {
716                    best = Some((weight, value, eval));
717                }
718            }
719        }
720
721        Ok(best.map(|(_, value, eval)| ZStyleEntry {
722            values: serde_json_to_values(&value),
723            eval,
724        }))
725    }
726
727    /// List all zstyles (for `zstyle -L`)
728    #[allow(clippy::type_complexity)]
729    pub fn list_zstyles(&self) -> rusqlite::Result<Vec<(String, String, Vec<String>, bool)>> {
730        let mut stmt = self
731            .conn
732            .prepare("SELECT pattern, style, value, eval FROM zstyles ORDER BY pattern, style")?;
733        let rows = stmt.query_map([], |row| {
734            let pattern: String = row.get(0)?;
735            let style: String = row.get(1)?;
736            let value: String = row.get(2)?;
737            let eval: bool = row.get::<_, i32>(3)? != 0;
738            Ok((pattern, style, serde_json_to_values(&value), eval))
739        })?;
740        rows.collect()
741    }
742
743    /// Count zstyles
744    pub fn zstyle_count(&self) -> rusqlite::Result<i64> {
745        self.conn
746            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
747    }
748
749    // =========================================================================
750    // Completion mappings (_comps)
751    // =========================================================================
752
753    /// Register a completion function for a command
754    pub fn set_comp(&self, command: &str, function: &str) -> rusqlite::Result<()> {
755        self.conn.execute(
756            "INSERT OR REPLACE INTO comps (command, function) VALUES (?1, ?2)",
757            params![command, function],
758        )?;
759        Ok(())
760    }
761
762    /// Bulk insert comps + populate FTS5 index
763    pub fn set_comps_bulk(&mut self, comps: &[(String, String)]) -> rusqlite::Result<()> {
764        let tx = self.conn.transaction()?;
765        // Clear and repopulate both tables
766        tx.execute("DELETE FROM comps", [])?;
767        tx.execute("DELETE FROM fts_comps", [])?;
768        {
769            let mut stmt = tx.prepare("INSERT INTO comps (command, function) VALUES (?1, ?2)")?;
770            let mut fts_stmt = tx.prepare("INSERT INTO fts_comps (command) VALUES (?1)")?;
771            for (command, function) in comps {
772                stmt.execute(params![command, function])?;
773                fts_stmt.execute(params![command])?;
774            }
775        }
776        tx.commit()
777    }
778
779    /// Fast prefix search using FTS5 (O(log n) vs O(n) for LIKE)
780    pub fn comps_prefix_fts(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
781        if prefix.is_empty() {
782            return self.comps_kv();
783        }
784        // FTS5 prefix search: "git*" matches git, github, gitk, etc.
785        let pattern = format!("{}*", prefix);
786        let mut stmt = self.conn.prepare(
787            "SELECT c.command, c.function FROM fts_comps f, comps c WHERE f.command MATCH ?1 AND c.command = f.command"
788        )?;
789        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
790        rows.collect()
791    }
792
793    /// Fast prefix search (LIKE with index scan, ORDER BY is free on indexed column)
794    pub fn comps_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
795        if prefix.is_empty() {
796            return self.comps_kv();
797        }
798        let pattern = format!("{}%", prefix);
799        let mut stmt = self.conn.prepare(
800            "SELECT command, function FROM comps WHERE command LIKE ?1 ORDER BY command",
801        )?;
802        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
803        rows.collect()
804    }
805
806    /// Lookup completion function for command
807    pub fn get_comp(&self, command: &str) -> rusqlite::Result<Option<String>> {
808        self.conn
809            .query_row(
810                "SELECT function FROM comps WHERE command = ?1",
811                params![command],
812                |row| row.get(0),
813            )
814            .optional()
815    }
816
817    /// Get all comps as HashMap (for compatibility)
818    pub fn get_all_comps(&self) -> rusqlite::Result<HashMap<String, String>> {
819        let mut stmt = self.conn.prepare("SELECT command, function FROM comps")?;
820        let rows = stmt.query_map([], |row| {
821            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
822        })?;
823        let mut map = HashMap::new();
824        for row in rows {
825            let (k, v) = row?;
826            map.insert(k, v);
827        }
828        Ok(map)
829    }
830
831    /// Count comps
832    pub fn comp_count(&self) -> rusqlite::Result<i64> {
833        self.conn
834            .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
835    }
836
837    /// Delete a completion registration
838    pub fn delete_comp(&self, command: &str) -> rusqlite::Result<usize> {
839        self.conn
840            .execute("DELETE FROM comps WHERE command = ?1", params![command])
841    }
842
843    // =========================================================================
844    // Pattern completions (_patcomps)
845    // =========================================================================
846
847    /// Register a pattern completion
848    pub fn set_patcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
849        self.conn.execute(
850            "INSERT OR REPLACE INTO patcomps (pattern, function) VALUES (?1, ?2)",
851            params![pattern, function],
852        )?;
853        Ok(())
854    }
855
856    /// Find matching pattern completion
857    pub fn find_patcomp(&self, command: &str) -> rusqlite::Result<Option<String>> {
858        let mut stmt = self
859            .conn
860            .prepare("SELECT pattern, function FROM patcomps")?;
861        let rows = stmt.query_map([], |row| {
862            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
863        })?;
864
865        for row in rows {
866            let (pattern, function) = row?;
867            if glob_matches(&pattern, command) {
868                return Ok(Some(function));
869            }
870        }
871        Ok(None)
872    }
873
874    // =========================================================================
875    // Post-pattern completions (_postpatcomps)
876    // =========================================================================
877
878    /// Register a post-pattern completion (`#compdef -P pat`).
879    pub fn set_postpatcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
880        self.conn.execute(
881            "INSERT OR REPLACE INTO postpatcomps (pattern, function) VALUES (?1, ?2)",
882            params![pattern, function],
883        )?;
884        Ok(())
885    }
886
887    /// All `_postpatcomps` entries as (pattern, function) pairs.
888    pub fn postpatcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
889        let mut stmt = self
890            .conn
891            .prepare("SELECT pattern, function FROM postpatcomps")?;
892        let rows = stmt.query_map([], |row| {
893            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
894        })?;
895        rows.collect()
896    }
897
898    /// All `_services` entries as (command, service) pairs.
899    pub fn services_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
900        let mut stmt = self.conn.prepare("SELECT command, service FROM services")?;
901        let rows = stmt.query_map([], |row| {
902            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
903        })?;
904        rows.collect()
905    }
906
907    /// Count of `_postpatcomps` entries.
908    pub fn postpatcomps_count(&self) -> rusqlite::Result<i64> {
909        self.conn
910            .query_row("SELECT COUNT(*) FROM postpatcomps", [], |row| row.get(0))
911    }
912
913    // =========================================================================
914    // Key completions
915    // =========================================================================
916
917    /// Register a key completion (for -K)
918    pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
919        self.conn.execute(
920            "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
921            params![key, function],
922        )?;
923        Ok(())
924    }
925
926    /// Lookup key completion
927    pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
928        self.conn
929            .query_row(
930                "SELECT function FROM keycomps WHERE key = ?1",
931                params![key],
932                |row| row.get(0),
933            )
934            .optional()
935    }
936
937    // =========================================================================
938    // Result cache
939    // =========================================================================
940
941    /// Cache completion results
942    pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
943        self.conn.execute(
944            "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
945            params![context, data, mtime],
946        )?;
947        Ok(())
948    }
949
950    /// Get cached results if not stale
951    pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
952        let now = std::time::SystemTime::now()
953            .duration_since(std::time::UNIX_EPOCH)
954            .unwrap()
955            .as_secs() as i64;
956
957        self.conn
958            .query_row(
959                "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
960                params![context, now - max_age],
961                |row| row.get(0),
962            )
963            .optional()
964    }
965
966    /// Clear old cache entries
967    pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
968        let now = std::time::SystemTime::now()
969            .duration_since(std::time::UNIX_EPOCH)
970            .unwrap()
971            .as_secs() as i64;
972
973        self.conn
974            .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
975    }
976
977    /// Clear all cache
978    pub fn clear_cache(&self) -> rusqlite::Result<()> {
979        self.conn.execute("DELETE FROM cache", [])?;
980        Ok(())
981    }
982
983    // =========================================================================
984    // Maintenance
985    // =========================================================================
986
987    /// Vacuum database
988    pub fn vacuum(&self) -> rusqlite::Result<()> {
989        self.conn.execute("VACUUM", [])?;
990        Ok(())
991    }
992
993    /// Get database stats
994    pub fn stats(&self) -> rusqlite::Result<CacheStats> {
995        Ok(CacheStats {
996            autoloads: self.autoload_count()?,
997            zstyles: self.zstyle_count()?,
998            comps: self.comp_count()?,
999            patcomps: self
1000                .conn
1001                .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
1002            keycomps: self
1003                .conn
1004                .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
1005            services: self
1006                .conn
1007                .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
1008            cache_entries: self
1009                .conn
1010                .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
1011        })
1012    }
1013}
1014
1015/// Autoload stub info
1016#[derive(Debug, Clone)]
1017pub struct AutoloadStub {
1018    /// `name` field.
1019    pub name: String,
1020    /// `source` field.
1021    pub source: String,
1022    /// `offset` field.
1023    pub offset: i64,
1024    /// `size` field.
1025    pub size: i64,
1026    /// Cached function body - if present, no need to read from source file
1027    pub body: Option<String>,
1028}
1029
1030/// zstyle entry
1031#[derive(Debug, Clone)]
1032pub struct ZStyleEntry {
1033    /// `values` field.
1034    pub values: Vec<String>,
1035    /// `eval` field.
1036    pub eval: bool,
1037}
1038
1039/// Cache statistics
1040#[derive(Debug)]
1041pub struct CacheStats {
1042    /// `autoloads` field.
1043    pub autoloads: i64,
1044    /// `zstyles` field.
1045    pub zstyles: i64,
1046    /// `comps` field.
1047    pub comps: i64,
1048    /// `patcomps` field.
1049    pub patcomps: i64,
1050    /// `keycomps` field.
1051    pub keycomps: i64,
1052    /// `services` field.
1053    pub services: i64,
1054    /// `cache_entries` field.
1055    pub cache_entries: i64,
1056}
1057
1058// Helper: serialize values to JSON
1059fn serde_values_to_json(values: &[String]) -> String {
1060    let escaped: Vec<String> = values
1061        .iter()
1062        .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
1063        .collect();
1064    format!("[{}]", escaped.join(","))
1065}
1066
1067// Helper: deserialize JSON to values
1068fn serde_json_to_values(json: &str) -> Vec<String> {
1069    let trimmed = json.trim();
1070    if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
1071        return vec![json.to_string()];
1072    }
1073
1074    let inner = &trimmed[1..trimmed.len() - 1];
1075    if inner.is_empty() {
1076        return vec![];
1077    }
1078
1079    let mut values = Vec::new();
1080    let mut current = String::new();
1081    let mut in_string = false;
1082    let mut escape = false;
1083
1084    for c in inner.chars() {
1085        if escape {
1086            current.push(c);
1087            escape = false;
1088        } else if c == '\\' {
1089            escape = true;
1090        } else if c == '"' {
1091            in_string = !in_string;
1092        } else if c == ',' && !in_string {
1093            values.push(current.trim().to_string());
1094            current = String::new();
1095        } else {
1096            current.push(c);
1097        }
1098    }
1099    if !current.is_empty() {
1100        values.push(current.trim().to_string());
1101    }
1102
1103    values
1104}
1105
1106// Helper: check if zstyle pattern matches context
1107fn pattern_matches_context(pattern: &str, context: &str) -> bool {
1108    let pat_parts: Vec<&str> = pattern.split(':').collect();
1109    let ctx_parts: Vec<&str> = context.split(':').collect();
1110
1111    if pat_parts.len() > ctx_parts.len() {
1112        return false;
1113    }
1114
1115    for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
1116        if *p != "*" && *p != *c {
1117            return false;
1118        }
1119    }
1120
1121    true
1122}
1123
1124// Helper: calculate pattern weight for specificity
1125fn calculate_pattern_weight(pattern: &str) -> i32 {
1126    let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
1127    let mut weight = parts.len() as i32 * 100;
1128
1129    for part in &parts {
1130        if *part != "*" {
1131            weight += 10;
1132        }
1133    }
1134
1135    weight
1136}
1137
1138// Helper: glob matching for patcomps
1139fn glob_matches(pattern: &str, text: &str) -> bool {
1140    let mut pat_chars = pattern.chars().peekable();
1141    let mut txt_chars = text.chars().peekable();
1142
1143    while let Some(p) = pat_chars.next() {
1144        match p {
1145            '*' => {
1146                if pat_chars.peek().is_none() {
1147                    return true;
1148                }
1149                while txt_chars.peek().is_some() {
1150                    if glob_matches(
1151                        &pat_chars.clone().collect::<String>(),
1152                        &txt_chars.clone().collect::<String>(),
1153                    ) {
1154                        return true;
1155                    }
1156                    txt_chars.next();
1157                }
1158                return false;
1159            }
1160            '?' => {
1161                if txt_chars.next().is_none() {
1162                    return false;
1163                }
1164            }
1165            c => {
1166                if txt_chars.next() != Some(c) {
1167                    return false;
1168                }
1169            }
1170        }
1171    }
1172
1173    txt_chars.peek().is_none()
1174}
1175
1176// =========================================================================
1177// Shell-visible arrays (_comps, _services, _patcomps, etc.)
1178// These back the zsh special arrays that users query with $#_comps etc.
1179// =========================================================================
1180
1181impl CompsysCache {
1182    /// Get count of _comps entries (for $#_comps)
1183    pub fn comps_count(&self) -> rusqlite::Result<i64> {
1184        self.comp_count()
1185    }
1186
1187    /// Get all _comps keys (for ${(k)_comps}) - ORDER BY is free on PRIMARY KEY
1188    pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
1189        let mut stmt = self
1190            .conn
1191            .prepare("SELECT command FROM comps ORDER BY command")?;
1192        let rows = stmt.query_map([], |row| row.get(0))?;
1193        rows.collect()
1194    }
1195
1196    /// Get all _comps values (for ${(v)_comps})
1197    pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1198        let mut stmt = self
1199            .conn
1200            .prepare("SELECT function FROM comps ORDER BY command")?;
1201        let rows = stmt.query_map([], |row| row.get(0))?;
1202        rows.collect()
1203    }
1204
1205    /// Get _comps as key-value pairs (for ${(kv)_comps})
1206    pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1207        let mut stmt = self
1208            .conn
1209            .prepare("SELECT command, function FROM comps ORDER BY command")?;
1210        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1211        rows.collect()
1212    }
1213
1214    // --- _patcomps ---
1215
1216    /// Get count of _patcomps
1217    pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1218        self.conn
1219            .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1220    }
1221
1222    /// Get all _patcomps keys
1223    pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1224        let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1225        let rows = stmt.query_map([], |row| row.get(0))?;
1226        rows.collect()
1227    }
1228
1229    /// Get all _patcomps as kv
1230    pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1231        let mut stmt = self
1232            .conn
1233            .prepare("SELECT pattern, function FROM patcomps")?;
1234        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1235        rows.collect()
1236    }
1237
1238    // --- _services ---
1239
1240    /// Set a service mapping
1241    pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1242        self.conn.execute(
1243            "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1244            params![command, service],
1245        )?;
1246        Ok(())
1247    }
1248
1249    /// Get service for command
1250    pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1251        self.conn
1252            .query_row(
1253                "SELECT service FROM services WHERE command = ?1",
1254                params![command],
1255                |row| row.get(0),
1256            )
1257            .optional()
1258    }
1259
1260    /// Get count of _services
1261    pub fn services_count(&self) -> rusqlite::Result<i64> {
1262        self.conn
1263            .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1264    }
1265
1266    /// Get all _services keys
1267    pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1268        let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1269        let rows = stmt.query_map([], |row| row.get(0))?;
1270        rows.collect()
1271    }
1272
1273    /// Bulk insert services
1274    pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1275        let tx = self.conn.transaction()?;
1276        {
1277            let mut stmt =
1278                tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1279            for (command, service) in services {
1280                stmt.execute(params![command, service])?;
1281            }
1282        }
1283        tx.commit()?;
1284        Ok(())
1285    }
1286
1287    // --- _compautos (autoloaded completion functions) ---
1288
1289    /// Get count of autoloaded functions
1290    pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1291        self.autoload_count()
1292    }
1293
1294    /// Get all autoload names (for ${(k)_compautos})
1295    pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1296        let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1297        let rows = stmt.query_map([], |row| row.get(0))?;
1298        rows.collect()
1299    }
1300
1301    // =========================================================================
1302    // PATH executables cache
1303    // =========================================================================
1304
1305    /// Check if executables cache is populated
1306    pub fn has_executables(&self) -> rusqlite::Result<bool> {
1307        let count: i64 = self
1308            .conn
1309            .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1310        Ok(count > 0)
1311    }
1312
1313    /// Store executables in bulk + populate FTS5 index
1314    pub fn set_executables_bulk(
1315        &mut self,
1316        executables: &[(String, String)],
1317    ) -> rusqlite::Result<()> {
1318        let tx = self.conn.transaction()?;
1319        tx.execute("DELETE FROM executables", [])?;
1320        tx.execute("DELETE FROM fts_executables", [])?;
1321        {
1322            let mut stmt =
1323                tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1324            let mut fts_stmt =
1325                tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1326            for (name, path) in executables {
1327                stmt.execute(params![name, path])?;
1328                fts_stmt.execute(params![name])?;
1329            }
1330        }
1331        tx.commit()
1332    }
1333
1334    /// Get all executable names (fast lookup set)
1335    pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1336        let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1337        let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1338        rows.collect::<Result<std::collections::HashSet<_>, _>>()
1339    }
1340
1341    /// Check if an executable exists in cache (O(1) lookup)
1342    pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1343        // Use EXISTS for faster check (stops at first match)
1344        let exists: i64 = self.conn.query_row(
1345            "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1346            params![name],
1347            |row| row.get(0),
1348        )?;
1349        Ok(exists == 1)
1350    }
1351
1352    /// Get executable path by name (direct key lookup)
1353    pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1354        self.conn
1355            .query_row(
1356                "SELECT path FROM executables WHERE name = ?1",
1357                params![name],
1358                |row| row.get(0),
1359            )
1360            .optional()
1361    }
1362
1363    /// Fast prefix search using FTS5
1364    pub fn get_executables_prefix_fts(
1365        &self,
1366        prefix: &str,
1367    ) -> rusqlite::Result<Vec<(String, String)>> {
1368        if prefix.is_empty() {
1369            let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1370            let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1371            return rows.collect();
1372        }
1373        let pattern = format!("{}*", prefix);
1374        let mut stmt = self.conn.prepare(
1375            "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1376        )?;
1377        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1378        rows.collect()
1379    }
1380
1381    /// Get executables matching prefix (LIKE with index, ORDER BY free on PRIMARY KEY)
1382    pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1383        if prefix.is_empty() {
1384            let mut stmt = self
1385                .conn
1386                .prepare("SELECT name, path FROM executables ORDER BY name")?;
1387            let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1388            return rows.collect();
1389        }
1390        let pattern = format!("{}%", prefix);
1391        let mut stmt = self
1392            .conn
1393            .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1394        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1395        rows.collect()
1396    }
1397
1398    /// Count executables
1399    pub fn executables_count(&self) -> rusqlite::Result<i64> {
1400        self.conn
1401            .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1402    }
1403
1404    // =========================================================================
1405    // Named directories cache (hash -d)
1406    // =========================================================================
1407
1408    /// Check if named_dirs cache is populated
1409    pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1410        let count: i64 = self
1411            .conn
1412            .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1413        Ok(count > 0)
1414    }
1415
1416    /// Store named directories in bulk (clears existing)
1417    pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1418        let tx = self.conn.transaction()?;
1419        tx.execute("DELETE FROM named_dirs", [])?;
1420        {
1421            let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1422            for (name, path) in dirs {
1423                stmt.execute(params![name, path])?;
1424            }
1425        }
1426        tx.commit()
1427    }
1428
1429    /// Get all named directories (ORDER BY free on PRIMARY KEY)
1430    pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1431        let mut stmt = self
1432            .conn
1433            .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1434        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1435        rows.collect()
1436    }
1437
1438    /// Get named directories matching prefix
1439    pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1440        if prefix.is_empty() {
1441            return self.get_named_dirs();
1442        }
1443        let pattern = format!("{}%", prefix);
1444        let mut stmt = self
1445            .conn
1446            .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1447        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1448        rows.collect()
1449    }
1450
1451    /// Count named directories
1452    pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1453        self.conn
1454            .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1455    }
1456
1457    // =========================================================================
1458    // Shell functions cache (FPATH)
1459    // =========================================================================
1460
1461    /// Check if shell_functions cache is populated
1462    pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1463        let count: i64 =
1464            self.conn
1465                .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1466        Ok(count > 0)
1467    }
1468
1469    /// Store shell functions in bulk + populate FTS5 index
1470    pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1471        let tx = self.conn.transaction()?;
1472        tx.execute("DELETE FROM shell_functions", [])?;
1473        tx.execute("DELETE FROM fts_shell_functions", [])?;
1474        {
1475            let mut stmt =
1476                tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1477            let mut fts_stmt =
1478                tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1479            for (name, source) in funcs {
1480                stmt.execute(params![name, source])?;
1481                fts_stmt.execute(params![name])?;
1482            }
1483        }
1484        tx.commit()
1485    }
1486
1487    /// Get all shell function names (ORDER BY free on PRIMARY KEY)
1488    pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1489        let mut stmt = self
1490            .conn
1491            .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1492        let rows = stmt.query_map([], |row| row.get(0))?;
1493        rows.collect()
1494    }
1495
1496    /// Get shell functions with source paths
1497    pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1498        let mut stmt = self
1499            .conn
1500            .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1501        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1502        rows.collect()
1503    }
1504
1505    /// Fast prefix search using FTS5 (note: FTS5 doesn't preserve order, needs post-sort)
1506    pub fn get_shell_functions_prefix_fts(
1507        &self,
1508        prefix: &str,
1509    ) -> rusqlite::Result<Vec<(String, String)>> {
1510        if prefix.is_empty() {
1511            return self.get_shell_functions();
1512        }
1513        let pattern = format!("{}*", prefix);
1514        let mut stmt = self.conn.prepare(
1515            "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"
1516        )?;
1517        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1518        rows.collect()
1519    }
1520
1521    /// Get shell functions matching prefix (LIKE with index, ORDER BY free)
1522    pub fn get_shell_functions_prefix(
1523        &self,
1524        prefix: &str,
1525    ) -> rusqlite::Result<Vec<(String, String)>> {
1526        if prefix.is_empty() {
1527            return self.get_shell_functions();
1528        }
1529        let pattern = format!("{}%", prefix);
1530        let mut stmt = self
1531            .conn
1532            .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1533        let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1534        rows.collect()
1535    }
1536
1537    /// Count shell functions
1538    pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1539        self.conn
1540            .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1541    }
1542
1543    // =========================================================================
1544    // Metadata for cache versioning/invalidation
1545    // =========================================================================
1546
1547    /// Set metadata key-value
1548    pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1549        self.conn.execute(
1550            "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1551            params![key, value],
1552        )?;
1553        Ok(())
1554    }
1555
1556    /// Get metadata value
1557    pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1558        self.conn
1559            .query_row(
1560                "SELECT value FROM metadata WHERE key = ?1",
1561                params![key],
1562                |row| row.get(0),
1563            )
1564            .optional()
1565    }
1566
1567    // =========================================================================
1568    // Zstyle helpers
1569    // =========================================================================
1570
1571    /// Check if zstyles cache is populated
1572    pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1573        let count: i64 = self
1574            .conn
1575            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1576        Ok(count > 0)
1577    }
1578
1579    /// Count zstyles
1580    pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1581        self.conn
1582            .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1583    }
1584
1585    /// Get all zstyles (for debugging)
1586    pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1587        let mut stmt = self
1588            .conn
1589            .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1590        let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1591        rows.collect()
1592    }
1593}
1594
1595#[cfg(test)]
1596mod tests {
1597    use super::*;
1598
1599    #[test]
1600    fn test_cache_basic() {
1601        let cache = CompsysCache::memory().unwrap();
1602
1603        cache
1604            .add_autoload("_git", "more_src.zwc", 1024, 5000)
1605            .unwrap();
1606        cache
1607            .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1608            .unwrap();
1609
1610        let stub = cache.get_autoload("_git").unwrap().unwrap();
1611        assert_eq!(stub.source, "more_src.zwc");
1612        assert_eq!(stub.offset, 1024);
1613
1614        assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1615    }
1616
1617    #[test]
1618    fn test_zstyle_cache() {
1619        let cache = CompsysCache::memory().unwrap();
1620
1621        cache
1622            .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1623            .unwrap();
1624        cache
1625            .set_zstyle(
1626                ":completion:*:descriptions",
1627                "format",
1628                &["%d".to_string()],
1629                false,
1630            )
1631            .unwrap();
1632
1633        let entry = cache
1634            .lookup_zstyle(":completion:foo", "menu")
1635            .unwrap()
1636            .unwrap();
1637        assert_eq!(entry.values, vec!["select"]);
1638
1639        let entry = cache
1640            .lookup_zstyle(":completion:foo:descriptions", "format")
1641            .unwrap()
1642            .unwrap();
1643        assert_eq!(entry.values, vec!["%d"]);
1644    }
1645
1646    #[test]
1647    fn test_zstyle_specificity() {
1648        let cache = CompsysCache::memory().unwrap();
1649
1650        cache
1651            .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1652            .unwrap();
1653        cache
1654            .set_zstyle(
1655                ":completion:*:*:*:default",
1656                "menu",
1657                &["yes".to_string()],
1658                false,
1659            )
1660            .unwrap();
1661
1662        let entry = cache
1663            .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1664            .unwrap()
1665            .unwrap();
1666        assert_eq!(entry.values, vec!["yes"]);
1667    }
1668
1669    #[test]
1670    fn test_comps_cache() {
1671        let mut cache = CompsysCache::memory().unwrap();
1672
1673        let comps = vec![
1674            ("git".to_string(), "_git".to_string()),
1675            ("docker".to_string(), "_docker".to_string()),
1676            ("cargo".to_string(), "_cargo".to_string()),
1677        ];
1678        cache.set_comps_bulk(&comps).unwrap();
1679
1680        assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1681        assert_eq!(
1682            cache.get_comp("docker").unwrap(),
1683            Some("_docker".to_string())
1684        );
1685        assert!(cache.get_comp("nonexistent").unwrap().is_none());
1686
1687        assert_eq!(cache.comp_count().unwrap(), 3);
1688    }
1689
1690    #[test]
1691    fn test_bulk_autoloads() {
1692        let mut cache = CompsysCache::memory().unwrap();
1693
1694        let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1695            .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1696            .collect();
1697
1698        cache.add_autoloads_bulk(&autoloads).unwrap();
1699        assert_eq!(cache.autoload_count().unwrap(), 1000);
1700
1701        let stub = cache.get_autoload("_func500").unwrap().unwrap();
1702        assert_eq!(stub.offset, 50000);
1703        assert!(stub.body.is_none()); // No body when bulk inserted without
1704    }
1705
1706    #[test]
1707    fn test_autoload_with_body() {
1708        let cache = CompsysCache::memory().unwrap();
1709
1710        let body = r#"
1711local -a opts
1712opts=(--help --version --verbose)
1713_arguments $opts
1714"#;
1715        cache
1716            .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1717            .unwrap();
1718
1719        let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1720        assert_eq!(stub.body.as_deref(), Some(body));
1721        assert_eq!(stub.size, body.len() as i64);
1722
1723        // Fast path: get body directly
1724        let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1725        assert_eq!(direct_body.as_deref(), Some(body));
1726    }
1727
1728    #[test]
1729    fn test_bulk_autoloads_with_bodies() {
1730        let mut cache = CompsysCache::memory().unwrap();
1731
1732        let autoloads: Vec<(String, String, String)> = (0..100)
1733            .map(|i| {
1734                (
1735                    format!("_func{}", i),
1736                    format!("/path/to/_func{}", i),
1737                    format!("# Function {}\necho hello", i),
1738                )
1739            })
1740            .collect();
1741
1742        cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1743        assert_eq!(cache.autoload_count().unwrap(), 100);
1744
1745        let stub = cache.get_autoload("_func50").unwrap().unwrap();
1746        assert!(stub.body.is_some());
1747        assert!(stub.body.unwrap().contains("Function 50"));
1748    }
1749
1750    #[test]
1751    fn test_get_autoload_body_or_zwc_with_body() {
1752        let cache = CompsysCache::memory().unwrap();
1753
1754        let body = "echo from sqlite";
1755        cache
1756            .add_autoload_with_body("_cached", "/some/path", body)
1757            .unwrap();
1758
1759        // Should return body from SQLite (fast path)
1760        let result = cache.get_autoload_body_or_zwc("_cached");
1761        assert_eq!(result, Some(body.to_string()));
1762    }
1763
1764    #[test]
1765    fn test_get_autoload_body_or_zwc_no_body() {
1766        let cache = CompsysCache::memory().unwrap();
1767
1768        // Add autoload without body (just ZWC reference)
1769        cache
1770            .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1771            .unwrap();
1772
1773        // Should return None since ZWC file doesn't exist
1774        let result = cache.get_autoload_body_or_zwc("_nocache");
1775        assert!(result.is_none());
1776    }
1777
1778    #[test]
1779    fn test_get_autoload_body_or_zwc_not_found() {
1780        let cache = CompsysCache::memory().unwrap();
1781
1782        // Function doesn't exist at all
1783        let result = cache.get_autoload_body_or_zwc("_nonexistent");
1784        assert!(result.is_none());
1785    }
1786
1787    #[test]
1788    fn test_patcomp() {
1789        let cache = CompsysCache::memory().unwrap();
1790
1791        cache.set_patcomp("git-*", "_git").unwrap();
1792        cache.set_patcomp("docker-*", "_docker").unwrap();
1793
1794        assert_eq!(
1795            cache.find_patcomp("git-commit").unwrap(),
1796            Some("_git".to_string())
1797        );
1798        assert_eq!(
1799            cache.find_patcomp("docker-compose").unwrap(),
1800            Some("_docker".to_string())
1801        );
1802        assert!(cache.find_patcomp("cargo").unwrap().is_none());
1803    }
1804
1805    #[test]
1806    fn test_glob_matches() {
1807        assert!(glob_matches("git-*", "git-commit"));
1808        assert!(glob_matches("*-compose", "docker-compose"));
1809        // `*.rs` cannot match `zle_main` (no `.rs` extension); the
1810        // glob is anchored at both ends, and `.rs` is literal.
1811        assert!(!glob_matches("*.rs", "zle_main"));
1812        assert!(!glob_matches("git-*", "docker-compose"));
1813        assert!(glob_matches("???", "abc"));
1814        assert!(!glob_matches("???", "abcd"));
1815    }
1816
1817    #[test]
1818    fn test_json_serde() {
1819        let values = vec!["hello".to_string(), "world".to_string()];
1820        let json = serde_values_to_json(&values);
1821        let back = serde_json_to_values(&json);
1822        assert_eq!(back, values);
1823
1824        let values = vec!["with \"quotes\"".to_string()];
1825        let json = serde_values_to_json(&values);
1826        let back = serde_json_to_values(&json);
1827        assert_eq!(back, vec!["with \"quotes\""]);
1828    }
1829
1830    #[test]
1831    fn test_stats() {
1832        let mut cache = CompsysCache::memory().unwrap();
1833
1834        cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1835        cache
1836            .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1837            .unwrap();
1838        cache.set_comp("git", "_git").unwrap();
1839
1840        let stats = cache.stats().unwrap();
1841        assert_eq!(stats.autoloads, 1);
1842        assert_eq!(stats.zstyles, 1);
1843        assert_eq!(stats.comps, 1);
1844    }
1845
1846    #[test]
1847    fn test_large_scale() {
1848        let mut cache = CompsysCache::memory().unwrap();
1849
1850        // Simulate 500k autoloads
1851        let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1852            .map(|i| {
1853                (
1854                    format!("_func{}", i),
1855                    format!("src{}.zwc", i % 10),
1856                    i * 50,
1857                    50,
1858                )
1859            })
1860            .collect();
1861
1862        cache.add_autoloads_bulk(&autoloads).unwrap();
1863
1864        // Fast lookup
1865        let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1866        assert_eq!(stub.offset, 9999 * 50);
1867
1868        assert_eq!(cache.autoload_count().unwrap(), 10000);
1869    }
1870
1871    #[test]
1872    fn test_executables_cache() {
1873        let mut cache = CompsysCache::memory().unwrap();
1874
1875        let executables = vec![
1876            ("ls".to_string(), "/bin/ls".to_string()),
1877            ("cat".to_string(), "/bin/cat".to_string()),
1878            ("git".to_string(), "/usr/bin/git".to_string()),
1879        ];
1880        cache.set_executables_bulk(&executables).unwrap();
1881
1882        assert!(cache.has_executables().unwrap());
1883        assert!(cache.has_executable("ls").unwrap());
1884        assert!(cache.has_executable("git").unwrap());
1885        assert!(!cache.has_executable("nonexistent").unwrap());
1886
1887        assert_eq!(
1888            cache.get_executable_path("ls").unwrap(),
1889            Some("/bin/ls".to_string())
1890        );
1891        assert_eq!(cache.executables_count().unwrap(), 3);
1892    }
1893
1894    #[test]
1895    fn test_executables_prefix_search() {
1896        let mut cache = CompsysCache::memory().unwrap();
1897
1898        let executables = vec![
1899            ("git".to_string(), "/usr/bin/git".to_string()),
1900            ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1901            ("grep".to_string(), "/bin/grep".to_string()),
1902            ("gzip".to_string(), "/bin/gzip".to_string()),
1903        ];
1904        cache.set_executables_bulk(&executables).unwrap();
1905
1906        // FTS prefix search returns (name, path) tuples
1907        let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1908        assert_eq!(git_cmds.len(), 2);
1909        assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1910        assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1911
1912        let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1913        assert_eq!(g_cmds.len(), 4);
1914    }
1915
1916    #[test]
1917    fn test_named_dirs_cache() {
1918        let mut cache = CompsysCache::memory().unwrap();
1919
1920        let dirs = vec![
1921            ("proj".to_string(), "/home/user/projects".to_string()),
1922            ("docs".to_string(), "/home/user/documents".to_string()),
1923        ];
1924        cache.set_named_dirs_bulk(&dirs).unwrap();
1925
1926        assert!(cache.has_named_dirs().unwrap());
1927
1928        let all = cache.get_named_dirs().unwrap();
1929        assert_eq!(all.len(), 2);
1930
1931        let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1932        assert_eq!(p_dirs.len(), 1);
1933        assert_eq!(p_dirs[0].0, "proj");
1934    }
1935
1936    #[test]
1937    fn test_shell_functions_cache() {
1938        let mut cache = CompsysCache::memory().unwrap();
1939
1940        let functions = vec![
1941            ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1942            (
1943                "zpwrClearList".to_string(),
1944                "/home/user/.zpwr/autoload".to_string(),
1945            ),
1946            (
1947                "zpwrTop".to_string(),
1948                "/home/user/.zpwr/autoload".to_string(),
1949            ),
1950        ];
1951        cache.set_shell_functions_bulk(&functions).unwrap();
1952
1953        assert!(cache.has_shell_functions().unwrap());
1954        assert_eq!(cache.shell_functions_count().unwrap(), 3);
1955
1956        let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1957        assert_eq!(zpwr.len(), 2);
1958        // Results are tuples (name, source)
1959        assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1960        assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1961    }
1962
1963    #[test]
1964    fn test_metadata() {
1965        let cache = CompsysCache::memory().unwrap();
1966
1967        cache.set_metadata("version", "1.0.0").unwrap();
1968        cache.set_metadata("build_time", "2026-04-22").unwrap();
1969
1970        assert_eq!(
1971            cache.get_metadata("version").unwrap(),
1972            Some("1.0.0".to_string())
1973        );
1974        assert_eq!(
1975            cache.get_metadata("build_time").unwrap(),
1976            Some("2026-04-22".to_string())
1977        );
1978        assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1979    }
1980
1981    #[test]
1982    fn test_comps_keys() {
1983        let mut cache = CompsysCache::memory().unwrap();
1984
1985        let comps = vec![
1986            ("git".to_string(), "_git".to_string()),
1987            ("docker".to_string(), "_docker".to_string()),
1988        ];
1989        cache.set_comps_bulk(&comps).unwrap();
1990
1991        let keys = cache.comps_keys().unwrap();
1992        assert_eq!(keys.len(), 2);
1993        assert!(keys.contains(&"docker".to_string()));
1994        assert!(keys.contains(&"git".to_string()));
1995    }
1996
1997    #[test]
1998    fn test_comps_prefix() {
1999        let mut cache = CompsysCache::memory().unwrap();
2000
2001        let comps = vec![
2002            ("git".to_string(), "_git".to_string()),
2003            ("gitk".to_string(), "_gitk".to_string()),
2004            ("docker".to_string(), "_docker".to_string()),
2005        ];
2006        cache.set_comps_bulk(&comps).unwrap();
2007
2008        let git_comps = cache.comps_prefix("git").unwrap();
2009        assert_eq!(git_comps.len(), 2);
2010    }
2011
2012    #[test]
2013    fn test_zstyles_bulk() {
2014        let mut cache = CompsysCache::memory().unwrap();
2015
2016        let styles = vec![
2017            (
2018                ":completion:*".to_string(),
2019                "menu".to_string(),
2020                vec!["select".to_string()],
2021                false,
2022            ),
2023            (
2024                ":completion:*".to_string(),
2025                "verbose".to_string(),
2026                vec!["yes".to_string()],
2027                false,
2028            ),
2029            (
2030                ":completion:*:descriptions".to_string(),
2031                "format".to_string(),
2032                vec!["%d".to_string()],
2033                false,
2034            ),
2035        ];
2036        cache.set_zstyles_bulk(&styles).unwrap();
2037
2038        assert!(cache.has_zstyles().unwrap());
2039        assert_eq!(cache.zstyles_count().unwrap(), 3);
2040    }
2041
2042    #[test]
2043    fn test_services() {
2044        let cache = CompsysCache::memory().unwrap();
2045
2046        cache.set_service("git", "scm").unwrap();
2047        cache.set_service("hg", "scm").unwrap();
2048
2049        assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
2050        assert_eq!(cache.get_service("unknown").unwrap(), None);
2051    }
2052
2053    #[test]
2054    fn test_cache_overwrite() {
2055        let cache = CompsysCache::memory().unwrap();
2056
2057        cache.set_comp("git", "_git_old").unwrap();
2058        assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
2059
2060        cache.set_comp("git", "_git_new").unwrap();
2061        assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
2062    }
2063
2064    #[test]
2065    fn test_executable_names() {
2066        let mut cache = CompsysCache::memory().unwrap();
2067
2068        let executables = vec![
2069            ("alpha".to_string(), "/bin/alpha".to_string()),
2070            ("beta".to_string(), "/bin/beta".to_string()),
2071            ("gamma".to_string(), "/bin/gamma".to_string()),
2072        ];
2073        cache.set_executables_bulk(&executables).unwrap();
2074
2075        let names = cache.get_executable_names().unwrap();
2076        assert_eq!(names.len(), 3);
2077        // Returns a HashSet, so check contains
2078        assert!(names.contains("alpha"));
2079        assert!(names.contains("beta"));
2080        assert!(names.contains("gamma"));
2081    }
2082
2083    #[test]
2084    fn postpatcomps_do_not_land_in_patcomps() {
2085        // Regression: `build_cache_from_fpath` used to write every
2086        // `#compdef -P` pattern into the `patcomps` table ("For now, we'll
2087        // merge them into patcomps"), and `load_from_cache` never read a
2088        // post-pattern back at all. On the `compinit -C` path that left
2089        // `$_postpatcomps` EMPTY and `$_patcomps` holding all 25 upstream
2090        // post-patterns, so `_dispatch` ran `-P` completers in its PRE
2091        // pass — before the `$_comps` lookup and without the
2092        // `_compskip=default` that the post pass sets. `PATH=/usr/bin:<TAB>`
2093        // therefore ran `_dir_list` AND the `-default-` fallback, listing
2094        // every file instead of only directories.
2095        let cache = CompsysCache::memory().unwrap();
2096        cache
2097            .set_patcomp("*/(init|rc[0-9S]#).d/*", "_init_d")
2098            .unwrap();
2099        cache
2100            .set_postpatcomp("-value-,*PATH,-default-", "_dir_list")
2101            .unwrap();
2102
2103        let pat = cache.patcomps_kv().unwrap();
2104        assert_eq!(
2105            pat,
2106            vec![("*/(init|rc[0-9S]#).d/*".to_string(), "_init_d".to_string())]
2107        );
2108
2109        let post = cache.postpatcomps_kv().unwrap();
2110        assert_eq!(
2111            post,
2112            vec![(
2113                "-value-,*PATH,-default-".to_string(),
2114                "_dir_list".to_string()
2115            )]
2116        );
2117        assert_eq!(cache.postpatcomps_count().unwrap(), 1);
2118        assert_eq!(cache.patcomps_count().unwrap(), 1);
2119    }
2120
2121    #[test]
2122    fn generation_one_completion_tables_are_rebuilt() {
2123        // A cache written before the split cannot say which of its
2124        // `patcomps` rows were really `-P` post-patterns, so the whole
2125        // completion mapping set is dropped and `compinit` re-scans.
2126        // Emptying `comps` is what makes `compinit::cache_is_valid` false.
2127        let mut cache = CompsysCache::memory().unwrap();
2128        cache
2129            .set_comps_bulk(&[("git".to_string(), "_git".to_string())])
2130            .unwrap();
2131        cache
2132            .set_patcomp("-value-,*PATH,-default-", "_dir_list")
2133            .unwrap();
2134        // Pretend this DB predates the split.
2135        cache
2136            .conn
2137            .execute("DELETE FROM metadata WHERE key = 'completion_schema'", [])
2138            .unwrap();
2139
2140        cache.migrate_completion_tables().unwrap();
2141
2142        assert_eq!(
2143            cache.comp_count().unwrap(),
2144            0,
2145            "stale comps must be dropped"
2146        );
2147        assert_eq!(cache.patcomps_count().unwrap(), 0);
2148        assert_eq!(
2149            cache.get_metadata("completion_schema").unwrap().as_deref(),
2150            Some(CompsysCache::COMPLETION_SCHEMA_GENERATION)
2151        );
2152    }
2153}