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, VarSet,
15    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        };
160        self.write(|tx| {
161            tx.execute(
162                "INSERT INTO workspace (id, name, mesh, vars, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
163                params![ws.id.0, ws.name, ws.mesh, json(&ws.vars)?, now()],
164            )
165            .map_err(|e| {
166                if is_unique_violation(&e) {
167                    Error::Duplicate {
168                        kind: "workspace",
169                        name: name.to_owned(),
170                    }
171                } else {
172                    e.into()
173                }
174            })?;
175            Ok(())
176        })?;
177        Ok(ws)
178    }
179
180    /// Every workspace, by name.
181    pub fn workspaces(&self) -> Result<Vec<Workspace>> {
182        self.read(|conn| {
183            let mut stmt =
184                conn.prepare("SELECT id, name, mesh, vars FROM workspace ORDER BY name")?;
185            let rows = stmt.query_map([], workspace_row)?;
186            rows.map(|r| r?).collect()
187        })
188    }
189
190    /// The workspace called `name`.
191    pub fn workspace(&self, name: &str) -> Result<Option<Workspace>> {
192        self.read(|conn| {
193            conn.query_row(
194                "SELECT id, name, mesh, vars FROM workspace WHERE name = ?1",
195                [name],
196                workspace_row,
197            )
198            .optional()?
199            .transpose()
200        })
201    }
202
203    /// The workspace `id`.
204    pub fn workspace_by_id(&self, id: &WorkspaceId) -> Result<Workspace> {
205        self.read(|conn| {
206            conn.query_row(
207                "SELECT id, name, mesh, vars FROM workspace WHERE id = ?1",
208                [&id.0],
209                workspace_row,
210            )
211            .optional()?
212            .transpose()?
213            .ok_or_else(|| Error::NotFound {
214                kind: "workspace",
215                id: id.0.clone(),
216            })
217        })
218    }
219
220    /// Replace the workspace's vars.
221    pub fn set_workspace_vars(&self, id: &WorkspaceId, vars: &VarSet) -> Result<()> {
222        self.write(|tx| {
223            let n = tx.execute(
224                "UPDATE workspace SET vars = ?2 WHERE id = ?1",
225                params![id.0, json(vars)?],
226            )?;
227            found(n, "workspace", &id.0)
228        })
229    }
230
231    /// Delete the workspace with all its environments, revisions, instances and leases.
232    pub fn delete_workspace(&self, id: &WorkspaceId) -> Result<()> {
233        self.write(|tx| {
234            let n = tx.execute("DELETE FROM workspace WHERE id = ?1", [&id.0])?;
235            found(n, "workspace", &id.0)
236        })
237    }
238
239    // --- Environments ------------------------------------------------------------------------
240
241    /// Create the environment `name` in `workspace`, with `draft` as its draft and no
242    /// revision yet. The draft's `env_name` is set to `name`.
243    pub fn create_environment(
244        &self,
245        workspace: &WorkspaceId,
246        name: &str,
247        mut draft: Draft,
248    ) -> Result<Environment> {
249        check_name(name)?;
250        draft.manifest.context.env_name = name.to_owned();
251        let env = Environment {
252            id: EnvironmentId::generate(),
253            workspace: workspace.clone(),
254            name: name.to_owned(),
255            draft,
256            vars: VarSet::new(),
257            head: None,
258        };
259        self.write(|tx| {
260            tx.execute(
261                "INSERT INTO environment (id, workspace_id, name, draft, draft_modules, draft_vars, \
262                 vars, head, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8)",
263                params![
264                    env.id.0,
265                    workspace.0,
266                    env.name,
267                    env.draft.manifest.to_toml()?,
268                    json(&env.draft.modules)?,
269                    json(&env.draft.vars)?,
270                    json(&env.vars)?,
271                    now()
272                ],
273            )
274            .map_err(|e| {
275                if is_unique_violation(&e) {
276                    Error::Duplicate {
277                        kind: "environment",
278                        name: name.to_owned(),
279                    }
280                } else if matches!(&e, rusqlite::Error::SqliteFailure(f, _)
281                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY)
282                {
283                    Error::NotFound {
284                        kind: "workspace",
285                        id: workspace.0.clone(),
286                    }
287                } else {
288                    e.into()
289                }
290            })?;
291            Ok(())
292        })?;
293        Ok(env)
294    }
295
296    /// The environments of `workspace`, by name.
297    pub fn environments(&self, workspace: &WorkspaceId) -> Result<Vec<Environment>> {
298        self.read(|conn| {
299            let mut stmt = conn.prepare(&format!(
300                "{ENV_COLUMNS} WHERE workspace_id = ?1 ORDER BY name"
301            ))?;
302            let rows = stmt.query_map([&workspace.0], environment_row)?;
303            rows.map(|r| r?).collect()
304        })
305    }
306
307    /// The environment `id`.
308    pub fn environment(&self, id: &EnvironmentId) -> Result<Environment> {
309        self.read(|conn| {
310            conn.query_row(
311                &format!("{ENV_COLUMNS} WHERE id = ?1"),
312                [&id.0],
313                environment_row,
314            )
315            .optional()?
316            .transpose()?
317            .ok_or_else(|| Error::NotFound {
318                kind: "environment",
319                id: id.0.clone(),
320            })
321        })
322    }
323
324    /// Replace the environment's draft. Its committed revisions are untouched. The draft's
325    /// `env_name` is kept as the environment's name.
326    pub fn update_draft(&self, id: &EnvironmentId, draft: &Draft) -> Result<()> {
327        let env = self.environment(id)?;
328        let mut manifest = draft.manifest.clone();
329        manifest.context.env_name = env.name;
330        self.write(|tx| {
331            let n = tx.execute(
332                "UPDATE environment SET draft = ?2, draft_modules = ?3, draft_vars = ?4 WHERE id = ?1",
333                params![
334                    id.0,
335                    manifest.to_toml()?,
336                    json(&draft.modules)?,
337                    json(&draft.vars)?
338                ],
339            )?;
340            found(n, "environment", &id.0)
341        })
342    }
343
344    /// Replace the vars every revision of the environment sees.
345    pub fn set_environment_vars(&self, id: &EnvironmentId, vars: &VarSet) -> Result<()> {
346        self.write(|tx| {
347            let n = tx.execute(
348                "UPDATE environment SET vars = ?2 WHERE id = ?1",
349                params![id.0, json(vars)?],
350            )?;
351            found(n, "environment", &id.0)
352        })
353    }
354
355    /// Commit the environment's draft as its next revision (numbered one past its head) and
356    /// make it the head.
357    pub fn commit_draft(&self, id: &EnvironmentId) -> Result<Revision> {
358        self.write(|tx| {
359            let (draft, modules, vars, head): (String, String, String, Option<u32>) = tx
360                .query_row(
361                    "SELECT draft, draft_modules, draft_vars, head FROM environment WHERE id = ?1",
362                    [&id.0],
363                    |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
364                )
365                .optional()?
366                .ok_or_else(|| Error::NotFound {
367                    kind: "environment",
368                    id: id.0.clone(),
369                })?;
370            let number = head.unwrap_or(0) + 1;
371            let modules: BTreeMap<String, String> = from_json(&modules)?;
372            let vars: VarSet = from_json(&vars)?;
373            let digest = digest(&draft, &modules, &vars)?;
374            tx.execute(
375                "INSERT INTO revision (environment_id, number, manifest, modules, vars, digest, \
376                 created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
377                params![
378                    id.0,
379                    number,
380                    draft,
381                    json(&modules)?,
382                    json(&vars)?,
383                    digest,
384                    now()
385                ],
386            )?;
387            tx.execute(
388                "UPDATE environment SET head = ?2 WHERE id = ?1",
389                params![id.0, number],
390            )?;
391            Ok(Revision {
392                environment: id.clone(),
393                number,
394                manifest: EnvManifest::from_toml(&draft)?,
395                manifest_toml: draft,
396                modules,
397                vars,
398                digest,
399            })
400        })
401    }
402
403    /// Revision `number` of the environment.
404    pub fn revision(&self, env: &EnvironmentId, number: u32) -> Result<Revision> {
405        self.read(|conn| {
406            conn.query_row(
407                &format!("{REV_COLUMNS} WHERE environment_id = ?1 AND number = ?2"),
408                params![env.0, number],
409                revision_row,
410            )
411            .optional()?
412            .transpose()?
413            .ok_or_else(|| Error::NotFound {
414                kind: "revision",
415                id: format!("{env}#{number}"),
416            })
417        })
418    }
419
420    /// Every revision of the environment, oldest first.
421    pub fn revisions(&self, env: &EnvironmentId) -> Result<Vec<Revision>> {
422        self.read(|conn| {
423            let mut stmt = conn.prepare(&format!(
424                "{REV_COLUMNS} WHERE environment_id = ?1 ORDER BY number"
425            ))?;
426            let rows = stmt.query_map([&env.0], revision_row)?;
427            rows.map(|r| r?).collect()
428        })
429    }
430
431    // --- Instances ---------------------------------------------------------------------------
432
433    /// Create an instance of revision `revision` of the environment, stopped and without a
434    /// slot.
435    pub fn create_instance(&self, env: &EnvironmentId, revision: u32) -> Result<Instance> {
436        let instance = Instance {
437            id: InstanceId::generate(),
438            environment: env.clone(),
439            revision,
440            desired: Desired::Stopped,
441            overrides: VarSet::new(),
442            sources: Vec::new(),
443            slot: None,
444        };
445        self.write(|tx| {
446            tx.execute(
447                "INSERT INTO instance (id, environment_id, revision, desired, overrides, sources, \
448                 created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
449                params![
450                    instance.id.0,
451                    env.0,
452                    revision,
453                    instance.desired.as_str(),
454                    json(&instance.overrides)?,
455                    json(&instance.sources)?,
456                    now()
457                ],
458            )
459            .map_err(|e| match &e {
460                rusqlite::Error::SqliteFailure(f, _)
461                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
462                {
463                    Error::NotFound {
464                        kind: "revision",
465                        id: format!("{env}#{revision}"),
466                    }
467                }
468                _ => e.into(),
469            })?;
470            Ok(())
471        })?;
472        Ok(instance)
473    }
474
475    /// The instance `id`.
476    pub fn instance(&self, id: &InstanceId) -> Result<Instance> {
477        self.read(|conn| {
478            conn.query_row(
479                &format!("{INST_COLUMNS} WHERE i.id = ?1"),
480                [&id.0],
481                instance_row,
482            )
483            .optional()?
484            .transpose()?
485            .ok_or_else(|| Error::NotFound {
486                kind: "instance",
487                id: id.0.clone(),
488            })
489        })
490    }
491
492    /// Every instance on the host, oldest first.
493    pub fn instances(&self) -> Result<Vec<Instance>> {
494        self.read(|conn| {
495            let mut stmt = conn.prepare(&format!("{INST_COLUMNS} ORDER BY i.created_at, i.id"))?;
496            let rows = stmt.query_map([], instance_row)?;
497            rows.map(|r| r?).collect()
498        })
499    }
500
501    /// Set whether the instance should be running.
502    pub fn set_desired(&self, id: &InstanceId, desired: Desired) -> Result<()> {
503        self.write(|tx| {
504            let n = tx.execute(
505                "UPDATE instance SET desired = ?2 WHERE id = ?1",
506                params![id.0, desired.as_str()],
507            )?;
508            found(n, "instance", &id.0)
509        })
510    }
511
512    /// Replace the instance's overrides.
513    pub fn set_overrides(&self, id: &InstanceId, overrides: &VarSet) -> Result<()> {
514        self.write(|tx| {
515            let n = tx.execute(
516                "UPDATE instance SET overrides = ?2 WHERE id = ?1",
517                params![id.0, json(overrides)?],
518            )?;
519            found(n, "instance", &id.0)
520        })
521    }
522
523    /// Replace the overlay sources the instance takes.
524    pub fn set_sources(&self, id: &InstanceId, sources: &[String]) -> Result<()> {
525        self.write(|tx| {
526            let n = tx.execute(
527                "UPDATE instance SET sources = ?2 WHERE id = ?1",
528                params![id.0, json(&sources)?],
529            )?;
530            found(n, "instance", &id.0)
531        })
532    }
533
534    /// Delete the instance, releasing its slot.
535    pub fn delete_instance(&self, id: &InstanceId) -> Result<()> {
536        self.write(|tx| {
537            let n = tx.execute("DELETE FROM instance WHERE id = ?1", [&id.0])?;
538            found(n, "instance", &id.0)
539        })
540    }
541
542    // --- Slot leases -------------------------------------------------------------------------
543
544    /// Lease the instance the lowest slot no instance on the host holds, or return the one it
545    /// already holds. Atomic across processes.
546    pub fn lease_slot(&self, instance: &InstanceId) -> Result<SlotLease> {
547        self.write(|tx| {
548            let held: Option<u16> = tx
549                .query_row(
550                    "SELECT slot FROM slot_lease WHERE instance_id = ?1",
551                    [&instance.0],
552                    |r| r.get(0),
553                )
554                .optional()?;
555            if let Some(slot) = held {
556                return Ok(SlotLease {
557                    slot,
558                    instance: instance.clone(),
559                });
560            }
561            let taken: Vec<u16> = tx
562                .prepare("SELECT slot FROM slot_lease ORDER BY slot")?
563                .query_map([], |r| r.get(0))?
564                .collect::<rusqlite::Result<_>>()?;
565            let slot = (0..=MAX_SLOT)
566                .find(|s| taken.binary_search(s).is_err())
567                .ok_or(Error::SlotsExhausted)?;
568            tx.execute(
569                "INSERT INTO slot_lease (slot, instance_id, leased_at) VALUES (?1, ?2, ?3)",
570                params![slot, instance.0, now()],
571            )
572            .map_err(|e| match &e {
573                rusqlite::Error::SqliteFailure(f, _)
574                    if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
575                {
576                    Error::NotFound {
577                        kind: "instance",
578                        id: instance.0.clone(),
579                    }
580                }
581                _ => e.into(),
582            })?;
583            Ok(SlotLease {
584                slot,
585                instance: instance.clone(),
586            })
587        })
588    }
589
590    /// Release the instance's slot, if it holds one.
591    pub fn release_slot(&self, instance: &InstanceId) -> Result<()> {
592        self.write(|tx| {
593            tx.execute(
594                "DELETE FROM slot_lease WHERE instance_id = ?1",
595                [&instance.0],
596            )?;
597            Ok(())
598        })
599    }
600
601    /// Every lease on the host, by slot.
602    pub fn leases(&self) -> Result<Vec<SlotLease>> {
603        self.read(|conn| {
604            let mut stmt =
605                conn.prepare("SELECT slot, instance_id FROM slot_lease ORDER BY slot")?;
606            let rows = stmt.query_map([], |r| {
607                Ok(SlotLease {
608                    slot: r.get(0)?,
609                    instance: InstanceId(r.get(1)?),
610                })
611            })?;
612            Ok(rows.collect::<rusqlite::Result<_>>()?)
613        })
614    }
615}
616
617fn found(rows: usize, kind: &'static str, id: &str) -> Result<()> {
618    if rows == 0 {
619        Err(Error::NotFound {
620            kind,
621            id: id.to_owned(),
622        })
623    } else {
624        Ok(())
625    }
626}
627
628/// Bring `conn`'s schema up to [`SCHEMA_VERSION`], in one write transaction so two processes
629/// opening a new store at once don't both migrate it.
630fn migrate(conn: &mut Connection) -> Result<()> {
631    let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
632    let found: u32 = tx.query_row("PRAGMA user_version", [], |r| r.get(0))?;
633    if found > SCHEMA_VERSION {
634        return Err(Error::UnsupportedVersion {
635            found,
636            supported: SCHEMA_VERSION,
637        });
638    }
639    for sql in &MIGRATIONS[found as usize..] {
640        tx.execute_batch(sql)?;
641    }
642    tx.pragma_update(None, "user_version", SCHEMA_VERSION)?;
643    tx.commit()?;
644    Ok(())
645}
646
647fn workspace_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Workspace>> {
648    let vars: String = r.get(3)?;
649    let (id, name, mesh) = (r.get(0)?, r.get(1)?, r.get(2)?);
650    Ok(from_json(&vars).map(|vars| Workspace {
651        id: WorkspaceId(id),
652        name,
653        mesh,
654        vars,
655    }))
656}
657
658const ENV_COLUMNS: &str =
659    "SELECT id, workspace_id, name, draft, draft_modules, draft_vars, vars, head FROM environment";
660
661fn environment_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Environment>> {
662    let (id, workspace, name): (String, String, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
663    let (draft, modules, draft_vars, vars): (String, String, String, String) =
664        (r.get(3)?, r.get(4)?, r.get(5)?, r.get(6)?);
665    let head: Option<u32> = r.get(7)?;
666    Ok((|| {
667        Ok(Environment {
668            id: EnvironmentId(id),
669            workspace: WorkspaceId(workspace),
670            name,
671            draft: Draft {
672                manifest: EnvManifest::from_toml(&draft)?,
673                modules: from_json(&modules)?,
674                vars: from_json(&draft_vars)?,
675            },
676            vars: from_json(&vars)?,
677            head,
678        })
679    })())
680}
681
682const REV_COLUMNS: &str =
683    "SELECT environment_id, number, manifest, modules, vars, digest FROM revision";
684
685fn revision_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Revision>> {
686    let (env, number, manifest_toml): (String, u32, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
687    let (modules, vars, digest): (String, String, String) = (r.get(3)?, r.get(4)?, r.get(5)?);
688    Ok((|| {
689        Ok(Revision {
690            environment: EnvironmentId(env),
691            number,
692            manifest: EnvManifest::from_toml(&manifest_toml)?,
693            manifest_toml,
694            modules: from_json(&modules)?,
695            vars: from_json(&vars)?,
696            digest,
697        })
698    })())
699}
700
701const INST_COLUMNS: &str = "SELECT i.id, i.environment_id, i.revision, i.desired, i.overrides, \
702     i.sources, l.slot FROM instance i LEFT JOIN slot_lease l ON l.instance_id = i.id";
703
704fn instance_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Instance>> {
705    let (id, env, revision, desired): (String, String, u32, String) =
706        (r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?);
707    let (overrides, sources, slot): (String, String, Option<u16>) =
708        (r.get(4)?, r.get(5)?, r.get(6)?);
709    Ok((|| {
710        Ok(Instance {
711            id: InstanceId(id),
712            environment: EnvironmentId(env),
713            revision,
714            desired: Desired::parse(&desired),
715            overrides: from_json(&overrides)?,
716            sources: from_json(&sources)?,
717            slot,
718        })
719    })())
720}