1use 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
18pub const TRESTLE_HOME_VAR: &str = "TRESTLE_HOME";
20
21pub const DB_FILE: &str = "state.db";
23
24const MIGRATIONS: &[&str] = &[include_str!("../migrations/0001_init.sql")];
27
28pub const SCHEMA_VERSION: u32 = MIGRATIONS.len() as u32;
30
31const BUSY_TIMEOUT: Duration = Duration::from_secs(10);
33
34pub 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
74pub 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
92pub(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 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 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 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 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 fn read<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
146 f(&self.lock())
147 }
148
149 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
773fn 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}