Skip to main content

deno_kv/
sqlite.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::rc::Rc;
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::sync::OnceLock;
11
12use async_trait::async_trait;
13use deno_core::OpState;
14use deno_core::unsync::spawn_blocking;
15use deno_error::JsErrorBox;
16use deno_permissions::OpenAccessKind;
17use deno_permissions::PermissionsContainer;
18pub use denokv_sqlite::SqliteBackendError;
19use denokv_sqlite::SqliteConfig;
20use denokv_sqlite::SqliteNotifier;
21use rand::SeedableRng;
22use rusqlite::OpenFlags;
23
24use crate::DatabaseHandler;
25
26static SQLITE_NOTIFIERS_MAP: OnceLock<Mutex<HashMap<PathBuf, SqliteNotifier>>> =
27  OnceLock::new();
28
29pub struct SqliteDbHandler {
30  pub default_storage_dir: Option<PathBuf>,
31  versionstamp_rng_seed: Option<u64>,
32}
33
34impl SqliteDbHandler {
35  pub fn new(
36    default_storage_dir: Option<PathBuf>,
37    versionstamp_rng_seed: Option<u64>,
38  ) -> Self {
39    Self {
40      default_storage_dir,
41      versionstamp_rng_seed,
42    }
43  }
44}
45
46deno_error::js_error_wrapper!(
47  SqliteBackendError,
48  JsSqliteBackendError,
49  "TypeError"
50);
51
52#[derive(Debug)]
53enum Mode {
54  Disk,
55  InMemory,
56}
57
58#[async_trait(?Send)]
59impl DatabaseHandler for SqliteDbHandler {
60  type DB = denokv_sqlite::Sqlite;
61
62  async fn open(
63    &self,
64    state: Rc<RefCell<OpState>>,
65    path: Option<String>,
66  ) -> Result<Self::DB, JsErrorBox> {
67    enum PathOrInMemory {
68      InMemory,
69      Path(PathBuf),
70    }
71
72    #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
73    fn validate_path(
74      state: &RefCell<OpState>,
75      path: Option<String>,
76    ) -> Result<Option<PathOrInMemory>, JsErrorBox> {
77      let Some(path) = path else {
78        return Ok(None);
79      };
80      if path == ":memory:" {
81        return Ok(Some(PathOrInMemory::InMemory));
82      }
83      if path.is_empty() {
84        return Err(JsErrorBox::type_error("Filename cannot be empty"));
85      }
86      if path.starts_with(':') {
87        return Err(JsErrorBox::type_error(
88          "Filename cannot start with ':' unless prefixed with './'",
89        ));
90      }
91      {
92        let state = state.borrow();
93        let permissions = state.borrow::<PermissionsContainer>();
94        let path = permissions
95          .check_open(
96            Cow::Owned(PathBuf::from(path)),
97            OpenAccessKind::ReadWriteNoFollow,
98            Some("Deno.openKv"),
99          )
100          .map_err(JsErrorBox::from_err)?;
101        Ok(Some(PathOrInMemory::Path(path.into_owned_path())))
102      }
103    }
104
105    let path = validate_path(&state, path)?;
106    let default_storage_dir = self.default_storage_dir.clone();
107    type ConnGen =
108      Arc<dyn Fn() -> rusqlite::Result<rusqlite::Connection> + Send + Sync>;
109    let (conn_gen, notifier_key): (ConnGen, _) = spawn_blocking(move || {
110      denokv_sqlite::sqlite_retry_loop(move || {
111        let mode = match std::env::var("DENO_KV_DB_MODE")
112          .unwrap_or_default()
113          .as_str()
114        {
115          "disk" | "" => Mode::Disk,
116          "memory" => Mode::InMemory,
117          _ => {
118            log::warn!("Unknown DENO_KV_DB_MODE value, defaulting to disk");
119            Mode::Disk
120          }
121        };
122
123        if matches!(mode, Mode::InMemory) {
124          return Ok::<_, SqliteBackendError>((
125            Arc::new(rusqlite::Connection::open_in_memory) as ConnGen,
126            None,
127          ));
128        }
129
130        let (conn, notifier_key) = match (path.as_ref(), &default_storage_dir) {
131          (Some(PathOrInMemory::InMemory), _) | (None, None) => (
132            Arc::new(rusqlite::Connection::open_in_memory) as ConnGen,
133            None,
134          ),
135          (Some(PathOrInMemory::Path(path)), _) => {
136            let flags =
137              OpenFlags::default().difference(OpenFlags::SQLITE_OPEN_URI);
138            let resolved_path =
139              deno_path_util::fs::canonicalize_path_maybe_not_exists(
140                // todo(dsherret): probably should use the FileSystem in the op state instead
141                &sys_traits::impls::RealSys,
142                path,
143              )
144              .map_err(JsErrorBox::from_err)?;
145            let path = path.clone();
146            (
147              Arc::new(move || {
148                rusqlite::Connection::open_with_flags(&path, flags)
149              }) as ConnGen,
150              Some(resolved_path),
151            )
152          }
153          (None, Some(path)) => {
154            #[allow(
155              clippy::disallowed_methods,
156              reason = "the storage directory is always on the real fs"
157            )]
158            std::fs::create_dir_all(path).map_err(JsErrorBox::from_err)?;
159            let path = path.join("kv.sqlite3");
160            let path2 = path.clone();
161            (
162              Arc::new(move || rusqlite::Connection::open(&path2)) as ConnGen,
163              Some(path),
164            )
165          }
166        };
167
168        Ok::<_, SqliteBackendError>((conn, notifier_key))
169      })
170    })
171    .await
172    .unwrap()
173    .map_err(JsErrorBox::from_err)?;
174
175    let notifier = if let Some(notifier_key) = notifier_key {
176      SQLITE_NOTIFIERS_MAP
177        .get_or_init(Default::default)
178        .lock()
179        .unwrap()
180        .entry(notifier_key)
181        .or_default()
182        .clone()
183    } else {
184      SqliteNotifier::default()
185    };
186
187    let versionstamp_rng_seed = self.versionstamp_rng_seed;
188
189    let config = SqliteConfig {
190      batch_timeout: None,
191      num_workers: 1,
192    };
193
194    denokv_sqlite::Sqlite::new(
195      move || {
196        let conn =
197          conn_gen().map_err(|e| JsErrorBox::generic(e.to_string()))?;
198        conn
199          .pragma_update(None, "journal_mode", "wal")
200          .map_err(|e| JsErrorBox::generic(e.to_string()))?;
201        Ok((
202          conn,
203          match versionstamp_rng_seed {
204            Some(seed) => Box::new(rand::rngs::StdRng::seed_from_u64(seed)),
205            None => Box::new(rand::rngs::StdRng::from_entropy()),
206          },
207        ))
208      },
209      notifier,
210      config,
211    )
212    .map_err(|e| JsErrorBox::generic(e.to_string()))
213  }
214}