Skip to main content

olai_stack_state/
store.rs

1//! [`Store`]: the SQLite database the CLI and the desktop app share.
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Mutex, MutexGuard};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use olai_stack_topology::workspace::is_valid_name;
9use olai_stack_topology::{EnvManifest, MAX_SLOT};
10use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior, params};
11
12use crate::error::{Error, Result};
13use crate::model::{
14    Desired, Draft, Environment, EnvironmentId, Instance, InstanceId, Revision, SlotLease,
15    SourceBinding, VarScope, VarSet, Workspace, WorkspaceId, digest,
16};
17
18/// The environment variable naming the trestle state directory.
19pub const TRESTLE_HOME_VAR: &str = "TRESTLE_HOME";
20
21/// The database file inside the state directory.
22pub const DB_FILE: &str = "state.db";
23
24/// The migrations, in order: migration `n` (1-based) takes the schema from version `n - 1` to
25/// `n`.
26const MIGRATIONS: &[&str] = &[include_str!("../migrations/0001_init.sql")];
27
28/// The schema version this crate writes and reads.
29pub const SCHEMA_VERSION: u32 = MIGRATIONS.len() as u32;
30
31/// How long a writer waits for another process's write to finish.
32const BUSY_TIMEOUT: Duration = Duration::from_secs(10);
33
34/// The trestle state directory: `$TRESTLE_HOME` when set, else `trestle` in the platform's data
35/// directory (e.g. `~/Library/Application Support/trestle`, `~/.local/share/trestle`).
36pub fn trestle_home() -> Option<PathBuf> {
37    match std::env::var_os(TRESTLE_HOME_VAR) {
38        Some(home) if !home.is_empty() => Some(PathBuf::from(home)),
39        _ => dirs::data_dir().map(|d| d.join("trestle")),
40    }
41}
42
43fn now() -> i64 {
44    SystemTime::now()
45        .duration_since(UNIX_EPOCH)
46        .map(|d| d.as_secs() as i64)
47        .unwrap_or_default()
48}
49
50fn json<T: serde::Serialize>(value: &T) -> Result<String> {
51    Ok(serde_json::to_string(value)?)
52}
53
54fn from_json<T: serde::de::DeserializeOwned>(text: &str) -> Result<T> {
55    Ok(serde_json::from_str(text)?)
56}
57
58fn check_name(name: &str) -> Result<()> {
59    if is_valid_name(name) {
60        Ok(())
61    } else {
62        Err(Error::InvalidName(name.to_owned()))
63    }
64}
65
66fn is_unique_violation(e: &rusqlite::Error) -> bool {
67    matches!(
68        e,
69        rusqlite::Error::SqliteFailure(f, _)
70            if f.code == rusqlite::ErrorCode::ConstraintViolation
71    )
72}
73
74/// The trestle state: workspaces, their environments (each a draft and numbered, immutable
75/// revisions), instances and the host-wide slot leases, in one SQLite database (WAL mode, so
76/// readers never block the writer).
77///
78/// Several processes open the same file: the CLI and the desktop app see each other's writes.
79/// A write waits for another process's to finish. A [`Watcher`](crate::Watcher) notices commits
80/// made through other connections.
81pub struct Store {
82    conn: Mutex<Connection>,
83    path: PathBuf,
84}
85
86impl std::fmt::Debug for Store {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("Store").field("path", &self.path).finish()
89    }
90}
91
92/// Open a connection to `path` with the pragmas every connection needs.
93pub(crate) fn connect(path: &Path) -> Result<Connection> {
94    let conn = Connection::open(path)?;
95    conn.busy_timeout(BUSY_TIMEOUT)?;
96    conn.pragma_update(None, "journal_mode", "WAL")?;
97    conn.pragma_update(None, "foreign_keys", true)?;
98    conn.pragma_update(None, "synchronous", "NORMAL")?;
99    Ok(conn)
100}
101
102impl Store {
103    /// Open (creating it if needed) the store in the [state directory](trestle_home).
104    pub fn open_default() -> Result<Self> {
105        let home = trestle_home().ok_or_else(|| Error::NotFound {
106            kind: "data directory",
107            id: format!("(set {TRESTLE_HOME_VAR})"),
108        })?;
109        std::fs::create_dir_all(&home).map_err(|e| Error::io(&home, e))?;
110        Self::open(&home.join(DB_FILE))
111    }
112
113    /// Open (creating it if needed) the store at `path`, and bring its schema up to
114    /// [`SCHEMA_VERSION`]. Errors with [`Error::UnsupportedVersion`] for a newer schema rather
115    /// than read it half-way.
116    pub fn open(path: &Path) -> Result<Self> {
117        let mut conn = connect(path)?;
118        migrate(&mut conn)?;
119        Ok(Store {
120            conn: Mutex::new(conn),
121            path: path.to_owned(),
122        })
123    }
124
125    /// The database file.
126    pub fn path(&self) -> &Path {
127        &self.path
128    }
129
130    fn lock(&self) -> MutexGuard<'_, Connection> {
131        self.conn.lock().unwrap_or_else(|e| e.into_inner())
132    }
133
134    /// Run `f` in a write transaction, taken up front so two processes never deadlock on
135    /// lock upgrades.
136    fn write<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
137        let mut conn = self.lock();
138        let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
139        let out = f(&tx)?;
140        tx.commit()?;
141        Ok(out)
142    }
143
144    /// Run `f` against a read snapshot.
145    fn read<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
146        f(&self.lock())
147    }
148
149    // --- Workspaces --------------------------------------------------------------------------
150
151    /// Create a workspace called `name`.
152    pub fn create_workspace(&self, name: &str, mesh: bool) -> Result<Workspace> {
153        check_name(name)?;
154        let ws = Workspace {
155            id: WorkspaceId::generate(),
156            name: name.to_owned(),
157            mesh,
158            vars: VarSet::new(),
159            sources: BTreeMap::new(),
160        };
161        self.write(|tx| {
162            tx.execute(
163                "INSERT INTO workspace (id, name, mesh, vars, sources, created_at) \
164                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
165                params![
166                    ws.id.0,
167                    ws.name,
168                    ws.mesh,
169                    json(&ws.vars)?,
170                    json(&ws.sources)?,
171                    now()
172                ],
173            )
174            .map_err(|e| {
175                if is_unique_violation(&e) {
176                    Error::Duplicate {
177                        kind: "workspace",
178                        name: name.to_owned(),
179                    }
180                } else {
181                    e.into()
182                }
183            })?;
184            Ok(())
185        })?;
186        Ok(ws)
187    }
188
189    /// Every workspace, by name.
190    pub fn workspaces(&self) -> Result<Vec<Workspace>> {
191        self.read(|conn| {
192            let mut stmt = conn.prepare(&format!("{WS_COLUMNS} ORDER BY name"))?;
193            let rows = stmt.query_map([], workspace_row)?;
194            rows.map(|r| r?).collect()
195        })
196    }
197
198    /// The workspace called `name`.
199    pub fn workspace(&self, name: &str) -> Result<Option<Workspace>> {
200        self.read(|conn| {
201            conn.query_row(
202                &format!("{WS_COLUMNS} WHERE name = ?1"),
203                [name],
204                workspace_row,
205            )
206            .optional()?
207            .transpose()
208        })
209    }
210
211    /// The workspace `id`.
212    pub fn workspace_by_id(&self, id: &WorkspaceId) -> Result<Workspace> {
213        self.read(|conn| {
214            conn.query_row(
215                &format!("{WS_COLUMNS} WHERE id = ?1"),
216                [&id.0],
217                workspace_row,
218            )
219            .optional()?
220            .transpose()?
221            .ok_or_else(|| Error::NotFound {
222                kind: "workspace",
223                id: id.0.clone(),
224            })
225        })
226    }
227
228    /// Replace the workspace's vars.
229    pub fn set_workspace_vars(&self, id: &WorkspaceId, vars: &VarSet) -> Result<()> {
230        self.write(|tx| {
231            let n = tx.execute(
232                "UPDATE workspace SET vars = ?2 WHERE id = ?1",
233                params![id.0, json(vars)?],
234            )?;
235            found(n, "workspace", &id.0)
236        })
237    }
238
239    /// Set the config every instance of the workspace binds the source `name` with, or clear it
240    /// (`None`).
241    pub fn set_workspace_source(
242        &self,
243        id: &WorkspaceId,
244        name: &str,
245        config: Option<serde_json::Value>,
246    ) -> Result<()> {
247        self.write(|tx| {
248            let text: String = tx
249                .query_row(
250                    "SELECT sources FROM workspace WHERE id = ?1",
251                    [&id.0],
252                    |r| r.get(0),
253                )
254                .optional()?
255                .ok_or_else(|| Error::NotFound {
256                    kind: "workspace",
257                    id: id.0.clone(),
258                })?;
259            let mut sources: BTreeMap<String, serde_json::Value> = from_json(&text)?;
260            match config {
261                Some(config) => sources.insert(name.to_owned(), config),
262                None => sources.remove(name),
263            };
264            tx.execute(
265                "UPDATE workspace SET sources = ?2 WHERE id = ?1",
266                params![id.0, json(&sources)?],
267            )?;
268            Ok(())
269        })
270    }
271
272    /// Change the vars `scope` names with `f`, in one write transaction, so a concurrent writer's
273    /// change to other vars of the same set is kept. Returns the vars as written.
274    pub fn modify_vars<E: From<Error>>(
275        &self,
276        scope: &VarScope,
277        f: impl FnOnce(&mut VarSet) -> std::result::Result<(), E>,
278    ) -> std::result::Result<VarSet, E> {
279        let (table, column, kind, id) = match scope {
280            VarScope::Workspace(id) => ("workspace", "vars", "workspace", &id.0),
281            VarScope::Environment(id) => ("environment", "vars", "environment", &id.0),
282            VarScope::Draft(id) => ("environment", "draft_vars", "environment", &id.0),
283        };
284        self.modify(
285            &format!("SELECT {column} FROM {table} WHERE id = ?1"),
286            &format!("UPDATE {table} SET {column} = ?2 WHERE id = ?1"),
287            kind,
288            id,
289            |text| {
290                let mut vars: VarSet = from_json(text)?;
291                f(&mut vars)?;
292                Ok((json(&vars)?, vars))
293            },
294        )
295    }
296
297    /// Read the text `select` returns for `id`, let `f` turn it into a new text and a result,
298    /// and write the text with `update`: all in one write transaction.
299    fn modify<T, E: From<Error>>(
300        &self,
301        select: &str,
302        update: &str,
303        kind: &'static str,
304        id: &str,
305        f: impl FnOnce(&str) -> std::result::Result<(String, T), E>,
306    ) -> std::result::Result<T, E> {
307        let mut conn = self.lock();
308        let tx = conn
309            .transaction_with_behavior(TransactionBehavior::Immediate)
310            .map_err(Error::from)?;
311        let text: String = tx
312            .query_row(select, [id], |r| r.get(0))
313            .optional()
314            .map_err(Error::from)?
315            .ok_or_else(|| Error::NotFound {
316                kind,
317                id: id.to_owned(),
318            })?;
319        let (text, out) = f(&text)?;
320        tx.execute(update, params![id, text]).map_err(Error::from)?;
321        tx.commit().map_err(Error::from)?;
322        Ok(out)
323    }
324
325    /// Delete the workspace with all its environments, revisions, instances and leases.
326    pub fn delete_workspace(&self, id: &WorkspaceId) -> Result<()> {
327        self.write(|tx| {
328            let n = tx.execute("DELETE FROM workspace WHERE id = ?1", [&id.0])?;
329            found(n, "workspace", &id.0)
330        })
331    }
332
333    // --- Environments ------------------------------------------------------------------------
334
335    /// Create the environment `name` in `workspace`, with `draft` as its draft and no
336    /// revision yet. The draft's `env_name` is set to `name`.
337    pub fn create_environment(
338        &self,
339        workspace: &WorkspaceId,
340        name: &str,
341        mut draft: Draft,
342    ) -> Result<Environment> {
343        check_name(name)?;
344        draft.manifest.context.env_name = name.to_owned();
345        let env = Environment {
346            id: EnvironmentId::generate(),
347            workspace: workspace.clone(),
348            name: name.to_owned(),
349            draft,
350            vars: VarSet::new(),
351            head: None,
352        };
353        self.write(|tx| {
354            tx.execute(
355                "INSERT INTO environment (id, workspace_id, name, draft, draft_modules, draft_vars, \
356                 vars, head, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8)",
357                params![
358                    env.id.0,
359                    workspace.0,
360                    env.name,
361                    env.draft.manifest.to_toml()?,
362                    json(&env.draft.modules)?,
363                    json(&env.draft.vars)?,
364                    json(&env.vars)?,
365                    now()
366                ],
367            )
368            .map_err(|e| {
369                if is_unique_violation(&e) {
370                    Error::Duplicate {
371                        kind: "environment",
372                        name: name.to_owned(),
373                    }
374                } else if matches!(&e, rusqlite::Error::SqliteFailure(f, _)
375                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY)
376                {
377                    Error::NotFound {
378                        kind: "workspace",
379                        id: workspace.0.clone(),
380                    }
381                } else {
382                    e.into()
383                }
384            })?;
385            Ok(())
386        })?;
387        Ok(env)
388    }
389
390    /// The environments of `workspace`, by name.
391    pub fn environments(&self, workspace: &WorkspaceId) -> Result<Vec<Environment>> {
392        self.read(|conn| {
393            let mut stmt = conn.prepare(&format!(
394                "{ENV_COLUMNS} WHERE workspace_id = ?1 ORDER BY name"
395            ))?;
396            let rows = stmt.query_map([&workspace.0], environment_row)?;
397            rows.map(|r| r?).collect()
398        })
399    }
400
401    /// The environment `id`.
402    pub fn environment(&self, id: &EnvironmentId) -> Result<Environment> {
403        self.read(|conn| {
404            conn.query_row(
405                &format!("{ENV_COLUMNS} WHERE id = ?1"),
406                [&id.0],
407                environment_row,
408            )
409            .optional()?
410            .transpose()?
411            .ok_or_else(|| Error::NotFound {
412                kind: "environment",
413                id: id.0.clone(),
414            })
415        })
416    }
417
418    /// Replace the environment's draft. Its committed revisions are untouched. The draft's
419    /// `env_name` is kept as the environment's name.
420    pub fn update_draft(&self, id: &EnvironmentId, draft: &Draft) -> Result<()> {
421        self.modify_draft(id, |d| {
422            *d = draft.clone();
423            Ok::<_, Error>(())
424        })?;
425        Ok(())
426    }
427
428    /// Change the environment's draft with `f`, in one write transaction, so two writers (the
429    /// CLI and the app, say) never lose each other's edits. The draft's `env_name` is kept as
430    /// the environment's name. Returns the draft as written.
431    pub fn modify_draft<E: From<Error>>(
432        &self,
433        id: &EnvironmentId,
434        f: impl FnOnce(&mut Draft) -> std::result::Result<(), E>,
435    ) -> std::result::Result<Draft, E> {
436        let mut conn = self.lock();
437        let tx = conn
438            .transaction_with_behavior(TransactionBehavior::Immediate)
439            .map_err(Error::from)?;
440        let env = tx
441            .query_row(
442                &format!("{ENV_COLUMNS} WHERE id = ?1"),
443                [&id.0],
444                environment_row,
445            )
446            .optional()
447            .map_err(Error::from)?
448            .transpose()?
449            .ok_or_else(|| Error::NotFound {
450                kind: "environment",
451                id: id.0.clone(),
452            })?;
453        let mut draft = env.draft;
454        f(&mut draft)?;
455        draft.manifest.context.env_name = env.name;
456        tx.execute(
457            "UPDATE environment SET draft = ?2, draft_modules = ?3, draft_vars = ?4 WHERE id = ?1",
458            params![
459                id.0,
460                draft.manifest.to_toml().map_err(Error::from)?,
461                json(&draft.modules)?,
462                json(&draft.vars)?
463            ],
464        )
465        .map_err(Error::from)?;
466        tx.commit().map_err(Error::from)?;
467        Ok(draft)
468    }
469
470    /// Replace the vars every revision of the environment sees.
471    pub fn set_environment_vars(&self, id: &EnvironmentId, vars: &VarSet) -> Result<()> {
472        self.write(|tx| {
473            let n = tx.execute(
474                "UPDATE environment SET vars = ?2 WHERE id = ?1",
475                params![id.0, json(vars)?],
476            )?;
477            found(n, "environment", &id.0)
478        })
479    }
480
481    /// Delete the environment with its revisions, instances and their leases.
482    pub fn delete_environment(&self, id: &EnvironmentId) -> Result<()> {
483        self.write(|tx| {
484            let n = tx.execute("DELETE FROM environment WHERE id = ?1", [&id.0])?;
485            found(n, "environment", &id.0)
486        })
487    }
488
489    /// Commit the environment's draft as its next revision (numbered one past its head) and
490    /// make it the head.
491    pub fn commit_draft(&self, id: &EnvironmentId) -> Result<Revision> {
492        self.write(|tx| {
493            let (draft, modules, vars, head): (String, String, String, Option<u32>) = tx
494                .query_row(
495                    "SELECT draft, draft_modules, draft_vars, head FROM environment WHERE id = ?1",
496                    [&id.0],
497                    |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
498                )
499                .optional()?
500                .ok_or_else(|| Error::NotFound {
501                    kind: "environment",
502                    id: id.0.clone(),
503                })?;
504            let number = head.unwrap_or(0) + 1;
505            let modules: BTreeMap<String, String> = from_json(&modules)?;
506            let vars: VarSet = from_json(&vars)?;
507            let digest = digest(&draft, &modules, &vars)?;
508            tx.execute(
509                "INSERT INTO revision (environment_id, number, manifest, modules, vars, digest, \
510                 created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
511                params![
512                    id.0,
513                    number,
514                    draft,
515                    json(&modules)?,
516                    json(&vars)?,
517                    digest,
518                    now()
519                ],
520            )?;
521            tx.execute(
522                "UPDATE environment SET head = ?2 WHERE id = ?1",
523                params![id.0, number],
524            )?;
525            Ok(Revision {
526                environment: id.clone(),
527                number,
528                manifest: EnvManifest::from_toml(&draft)?,
529                manifest_toml: draft,
530                modules,
531                vars,
532                digest,
533            })
534        })
535    }
536
537    /// Revision `number` of the environment.
538    pub fn revision(&self, env: &EnvironmentId, number: u32) -> Result<Revision> {
539        self.read(|conn| {
540            conn.query_row(
541                &format!("{REV_COLUMNS} WHERE environment_id = ?1 AND number = ?2"),
542                params![env.0, number],
543                revision_row,
544            )
545            .optional()?
546            .transpose()?
547            .ok_or_else(|| Error::NotFound {
548                kind: "revision",
549                id: format!("{env}#{number}"),
550            })
551        })
552    }
553
554    /// Every revision of the environment, oldest first.
555    pub fn revisions(&self, env: &EnvironmentId) -> Result<Vec<Revision>> {
556        self.read(|conn| {
557            let mut stmt = conn.prepare(&format!(
558                "{REV_COLUMNS} WHERE environment_id = ?1 ORDER BY number"
559            ))?;
560            let rows = stmt.query_map([&env.0], revision_row)?;
561            rows.map(|r| r?).collect()
562        })
563    }
564
565    // --- Instances ---------------------------------------------------------------------------
566
567    /// Create an instance of revision `revision` of the environment, stopped and without a
568    /// slot.
569    pub fn create_instance(&self, env: &EnvironmentId, revision: u32) -> Result<Instance> {
570        let instance = Instance {
571            id: InstanceId::generate(),
572            environment: env.clone(),
573            revision,
574            desired: Desired::Stopped,
575            overrides: VarSet::new(),
576            sources: Vec::new(),
577            slot: None,
578        };
579        self.write(|tx| {
580            tx.execute(
581                "INSERT INTO instance (id, environment_id, revision, desired, overrides, sources, \
582                 created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
583                params![
584                    instance.id.0,
585                    env.0,
586                    revision,
587                    instance.desired.as_str(),
588                    json(&instance.overrides)?,
589                    json(&instance.sources)?,
590                    now()
591                ],
592            )
593            .map_err(|e| match &e {
594                rusqlite::Error::SqliteFailure(f, _)
595                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
596                {
597                    Error::NotFound {
598                        kind: "revision",
599                        id: format!("{env}#{revision}"),
600                    }
601                }
602                _ => e.into(),
603            })?;
604            Ok(())
605        })?;
606        Ok(instance)
607    }
608
609    /// The instance `id`.
610    pub fn instance(&self, id: &InstanceId) -> Result<Instance> {
611        self.read(|conn| {
612            conn.query_row(
613                &format!("{INST_COLUMNS} WHERE i.id = ?1"),
614                [&id.0],
615                instance_row,
616            )
617            .optional()?
618            .transpose()?
619            .ok_or_else(|| Error::NotFound {
620                kind: "instance",
621                id: id.0.clone(),
622            })
623        })
624    }
625
626    /// Every instance on the host, oldest first.
627    pub fn instances(&self) -> Result<Vec<Instance>> {
628        self.read(|conn| {
629            let mut stmt = conn.prepare(&format!("{INST_COLUMNS} ORDER BY i.created_at, i.id"))?;
630            let rows = stmt.query_map([], instance_row)?;
631            rows.map(|r| r?).collect()
632        })
633    }
634
635    /// The instances of the environment, oldest first.
636    pub fn instances_of(&self, env: &EnvironmentId) -> Result<Vec<Instance>> {
637        self.read(|conn| {
638            let mut stmt = conn.prepare(&format!(
639                "{INST_COLUMNS} WHERE i.environment_id = ?1 ORDER BY i.created_at, i.id"
640            ))?;
641            let rows = stmt.query_map([&env.0], instance_row)?;
642            rows.map(|r| r?).collect()
643        })
644    }
645
646    /// Set whether the instance should be running.
647    pub fn set_desired(&self, id: &InstanceId, desired: Desired) -> Result<()> {
648        self.write(|tx| {
649            let n = tx.execute(
650                "UPDATE instance SET desired = ?2 WHERE id = ?1",
651                params![id.0, desired.as_str()],
652            )?;
653            found(n, "instance", &id.0)
654        })
655    }
656
657    /// Replace the instance's overrides.
658    pub fn set_overrides(&self, id: &InstanceId, overrides: &VarSet) -> Result<()> {
659        self.write(|tx| {
660            let n = tx.execute(
661                "UPDATE instance SET overrides = ?2 WHERE id = ?1",
662                params![id.0, json(overrides)?],
663            )?;
664            found(n, "instance", &id.0)
665        })
666    }
667
668    /// Replace the overlay sources the instance takes.
669    pub fn set_sources(&self, id: &InstanceId, sources: &[SourceBinding]) -> Result<()> {
670        self.write(|tx| {
671            let n = tx.execute(
672                "UPDATE instance SET sources = ?2 WHERE id = ?1",
673                params![id.0, json(&sources)?],
674            )?;
675            found(n, "instance", &id.0)
676        })
677    }
678
679    /// Delete the instance, releasing its slot.
680    pub fn delete_instance(&self, id: &InstanceId) -> Result<()> {
681        self.write(|tx| {
682            let n = tx.execute("DELETE FROM instance WHERE id = ?1", [&id.0])?;
683            found(n, "instance", &id.0)
684        })
685    }
686
687    // --- Slot leases -------------------------------------------------------------------------
688
689    /// Lease the instance the lowest slot no instance on the host holds, or return the one it
690    /// already holds. Atomic across processes.
691    pub fn lease_slot(&self, instance: &InstanceId) -> Result<SlotLease> {
692        self.write(|tx| {
693            let held: Option<u16> = tx
694                .query_row(
695                    "SELECT slot FROM slot_lease WHERE instance_id = ?1",
696                    [&instance.0],
697                    |r| r.get(0),
698                )
699                .optional()?;
700            if let Some(slot) = held {
701                return Ok(SlotLease {
702                    slot,
703                    instance: instance.clone(),
704                });
705            }
706            let taken: Vec<u16> = tx
707                .prepare("SELECT slot FROM slot_lease ORDER BY slot")?
708                .query_map([], |r| r.get(0))?
709                .collect::<rusqlite::Result<_>>()?;
710            let slot = (0..=MAX_SLOT)
711                .find(|s| taken.binary_search(s).is_err())
712                .ok_or(Error::SlotsExhausted)?;
713            tx.execute(
714                "INSERT INTO slot_lease (slot, instance_id, leased_at) VALUES (?1, ?2, ?3)",
715                params![slot, instance.0, now()],
716            )
717            .map_err(|e| match &e {
718                rusqlite::Error::SqliteFailure(f, _)
719                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
720                {
721                    Error::NotFound {
722                        kind: "instance",
723                        id: instance.0.clone(),
724                    }
725                }
726                _ => e.into(),
727            })?;
728            Ok(SlotLease {
729                slot,
730                instance: instance.clone(),
731            })
732        })
733    }
734
735    /// Release the instance's slot, if it holds one.
736    pub fn release_slot(&self, instance: &InstanceId) -> Result<()> {
737        self.write(|tx| {
738            tx.execute(
739                "DELETE FROM slot_lease WHERE instance_id = ?1",
740                [&instance.0],
741            )?;
742            Ok(())
743        })
744    }
745
746    /// Every lease on the host, by slot.
747    pub fn leases(&self) -> Result<Vec<SlotLease>> {
748        self.read(|conn| {
749            let mut stmt =
750                conn.prepare("SELECT slot, instance_id FROM slot_lease ORDER BY slot")?;
751            let rows = stmt.query_map([], |r| {
752                Ok(SlotLease {
753                    slot: r.get(0)?,
754                    instance: InstanceId(r.get(1)?),
755                })
756            })?;
757            Ok(rows.collect::<rusqlite::Result<_>>()?)
758        })
759    }
760}
761
762fn found(rows: usize, kind: &'static str, id: &str) -> Result<()> {
763    if rows == 0 {
764        Err(Error::NotFound {
765            kind,
766            id: id.to_owned(),
767        })
768    } else {
769        Ok(())
770    }
771}
772
773/// Bring `conn`'s schema up to [`SCHEMA_VERSION`], in one write transaction so two processes
774/// opening a new store at once don't both migrate it.
775fn migrate(conn: &mut Connection) -> Result<()> {
776    let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
777    let found: u32 = tx.query_row("PRAGMA user_version", [], |r| r.get(0))?;
778    if found > SCHEMA_VERSION {
779        return Err(Error::UnsupportedVersion {
780            found,
781            supported: SCHEMA_VERSION,
782        });
783    }
784    for sql in &MIGRATIONS[found as usize..] {
785        tx.execute_batch(sql)?;
786    }
787    tx.pragma_update(None, "user_version", SCHEMA_VERSION)?;
788    tx.commit()?;
789    Ok(())
790}
791
792const WS_COLUMNS: &str = "SELECT id, name, mesh, vars, sources FROM workspace";
793
794fn workspace_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Workspace>> {
795    let (vars, sources): (String, String) = (r.get(3)?, r.get(4)?);
796    let (id, name, mesh) = (r.get(0)?, r.get(1)?, r.get(2)?);
797    Ok((|| {
798        Ok(Workspace {
799            id: WorkspaceId(id),
800            name,
801            mesh,
802            vars: from_json(&vars)?,
803            sources: from_json(&sources)?,
804        })
805    })())
806}
807
808const ENV_COLUMNS: &str =
809    "SELECT id, workspace_id, name, draft, draft_modules, draft_vars, vars, head FROM environment";
810
811fn environment_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Environment>> {
812    let (id, workspace, name): (String, String, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
813    let (draft, modules, draft_vars, vars): (String, String, String, String) =
814        (r.get(3)?, r.get(4)?, r.get(5)?, r.get(6)?);
815    let head: Option<u32> = r.get(7)?;
816    Ok((|| {
817        Ok(Environment {
818            id: EnvironmentId(id),
819            workspace: WorkspaceId(workspace),
820            name,
821            draft: Draft {
822                manifest: EnvManifest::from_toml(&draft)?,
823                modules: from_json(&modules)?,
824                vars: from_json(&draft_vars)?,
825            },
826            vars: from_json(&vars)?,
827            head,
828        })
829    })())
830}
831
832const REV_COLUMNS: &str =
833    "SELECT environment_id, number, manifest, modules, vars, digest FROM revision";
834
835fn revision_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Revision>> {
836    let (env, number, manifest_toml): (String, u32, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
837    let (modules, vars, digest): (String, String, String) = (r.get(3)?, r.get(4)?, r.get(5)?);
838    Ok((|| {
839        Ok(Revision {
840            environment: EnvironmentId(env),
841            number,
842            manifest: EnvManifest::from_toml(&manifest_toml)?,
843            manifest_toml,
844            modules: from_json(&modules)?,
845            vars: from_json(&vars)?,
846            digest,
847        })
848    })())
849}
850
851const INST_COLUMNS: &str = "SELECT i.id, i.environment_id, i.revision, i.desired, i.overrides, \
852     i.sources, l.slot FROM instance i LEFT JOIN slot_lease l ON l.instance_id = i.id";
853
854fn instance_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Instance>> {
855    let (id, env, revision, desired): (String, String, u32, String) =
856        (r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?);
857    let (overrides, sources, slot): (String, String, Option<u16>) =
858        (r.get(4)?, r.get(5)?, r.get(6)?);
859    Ok((|| {
860        Ok(Instance {
861            id: InstanceId(id),
862            environment: EnvironmentId(env),
863            revision,
864            desired: Desired::parse(&desired),
865            overrides: from_json(&overrides)?,
866            sources: from_json(&sources)?,
867            slot,
868        })
869    })())
870}