Skip to main content

hyperdb_compile_check/
db.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Shared Hyper instance for compile-time validation.
5//!
6//! One `CompileTimeDb` is shared across all macro invocations in a single
7//! crate compilation (rustc spawns one proc-macro host process per crate).
8//! The instance is lazily initialized on first use via `get_or_init()` and
9//! dropped when the host process exits.
10
11use parking_lot::Mutex;
12
13/// A live connection to an in-process Hyper instance used for SQL dry-runs.
14#[derive(Debug)]
15pub struct CompileTimeDb {
16    _process: hyperdb_api::HyperProcess,
17    pub(crate) conn: hyperdb_api::Connection,
18}
19
20// `HyperProcess` manages the hyperd subprocess; it can produce many independent
21// `Connection`s (see `hyperdb_api::pool` for the production N-connection pool).
22// Here we hold exactly ONE `Connection` — a single TCP session used for all
23// `LIMIT 0` dry-runs. A `Connection` has internal mutable TCP + protocol state
24// and is NOT safe to use from multiple threads simultaneously.
25//
26// The `parking_lot::Mutex` is what makes this safe: it ensures only one
27// proc-macro expansion thread touches the connection at a time. Each `query_as!`
28// site locks, runs one dry-run (~7ms), unlocks. They serialize on the one
29// connection rather than each getting their own (a connection-pool approach
30// would work too but adds startup cost for negligible gain at v1 scale).
31//
32// Neither `HyperProcess` nor `Connection` is `Send`/`Sync` in the public API.
33// We implement both here because `OnceLock<T>` requires `T: Send + Sync`.
34// The `Mutex` upholds the invariant that only one thread ever accesses the
35// fields — making the `Send`/`Sync` impls sound.
36//
37// REVISIT: if `HyperProcess`/`Connection` are made `Send` upstream, remove
38// these impls and let the compiler derive them.
39//
40// # Why `parking_lot::Mutex` instead of `std::sync::Mutex`
41//
42// Proc-macros routinely call `panic!` to emit a `compile_error!`. A
43// `std::sync::Mutex` poisons on the first panic, causing every subsequent
44// macro invocation in the same crate to receive `PoisonError` regardless of
45// whether they have anything to do with the failing site. `parking_lot::Mutex`
46// never poisons — lock acquisition always succeeds after the panicking thread
47// releases the lock, so a bad `query_as!` site doesn't cascade.
48
49// SAFETY: `OnceLock` requires `Send`; safe because the `Mutex` guarantees
50// exclusive access — `CompileTimeDb` is never touched without holding the lock.
51unsafe impl Send for CompileTimeDb {}
52// SAFETY: `OnceLock` requires `Sync`; safe for the same reason as `Send` above.
53unsafe impl Sync for CompileTimeDb {}
54
55/// Global storage: initialized at most once per proc-macro host process.
56///
57/// We use `std::sync::OnceLock` (stable since 1.70) rather than a raw
58/// `static mut` + `Once` pair to avoid the `static_mut_refs` UB concern in
59/// Rust 2024 edition. `OnceLock` provides the same "write-once, read-many"
60/// guarantee without unsafe code in the accessor.
61static DB_STORAGE: std::sync::OnceLock<Mutex<CompileTimeDb>> = std::sync::OnceLock::new();
62
63/// Returns a reference to the global `Mutex<CompileTimeDb>`, initializing it
64/// on the first call.
65///
66/// # Panics
67///
68/// Panics if Hyper fails to start (e.g. `HYPERD_PATH` is invalid or the
69/// binary is absent). The error is surfaced as a `compile_error!` by the
70/// calling macro.
71pub fn get_or_init() -> &'static Mutex<CompileTimeDb> {
72    DB_STORAGE.get_or_init(|| {
73        Mutex::new(CompileTimeDb::new().expect(
74            "hyperdb-compile-check: failed to start embedded Hyper instance; \
75                 check HYPERD_PATH or ensure .hyperd/current/hyperd is present",
76        ))
77    })
78}
79
80impl CompileTimeDb {
81    fn new() -> hyperdb_api::Result<Self> {
82        use hyperdb_api::{Connection, CreateMode, HyperProcess, Parameters};
83
84        // Emit Hyper logs to a temp dir to keep build output clean.
85        let log_dir = tempfile::tempdir().map_err(|e| {
86            hyperdb_api::Error::Config(format!("compile-check: tempdir failed: {e}"))
87        })?;
88        let log_path = log_dir
89            .path()
90            .canonicalize()
91            .unwrap_or_else(|_| log_dir.path().to_path_buf());
92
93        let mut params = Parameters::new();
94        params.set("log_dir", log_path.to_string_lossy().to_string());
95
96        // `None` → auto-discover via HYPERD_PATH env or `.hyperd/current`.
97        let process = HyperProcess::new(None, Some(&params))?;
98
99        // In-memory validation database; each dry-run seeds required tables
100        // on demand (lazy seeding via 42P01 SQLSTATE — see `validate.rs`).
101        let db_path = log_dir.path().join("compile_check.hyper");
102        let conn = Connection::new(&process, &db_path, CreateMode::CreateAndReplace)?;
103
104        // Keep `log_dir` alive as long as the process — drop it with the struct.
105        // We leak the TempDir intentionally: `CompileTimeDb` is `'static` (stored
106        // in a static); the OS will clean up the temp dir on process exit.
107        std::mem::forget(log_dir);
108
109        Ok(Self {
110            _process: process,
111            conn,
112        })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    #[ignore = "requires HYPERD_PATH; run manually"]
122    fn smoke_two_calls_reuse_instance() {
123        let ptr1 = std::ptr::from_ref(get_or_init());
124        let ptr2 = std::ptr::from_ref(get_or_init());
125        assert_eq!(
126            ptr1, ptr2,
127            "get_or_init must return the same static instance"
128        );
129    }
130}