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] = &[
27 include_str!("../migrations/0001_init.sql"),
28 include_str!("../migrations/0002_revision_message.sql"),
29];
30
31pub const SCHEMA_VERSION: u32 = MIGRATIONS.len() as u32;
33
34const BUSY_TIMEOUT: Duration = Duration::from_secs(10);
36
37pub fn trestle_home() -> Option<PathBuf> {
40 match std::env::var_os(TRESTLE_HOME_VAR) {
41 Some(home) if !home.is_empty() => Some(PathBuf::from(home)),
42 _ => dirs::data_dir().map(|d| d.join("trestle")),
43 }
44}
45
46fn now() -> i64 {
47 SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .map(|d| d.as_secs() as i64)
50 .unwrap_or_default()
51}
52
53fn time_of(secs: i64) -> SystemTime {
55 UNIX_EPOCH + Duration::from_secs(secs.max(0) as u64)
56}
57
58fn json<T: serde::Serialize>(value: &T) -> Result<String> {
59 Ok(serde_json::to_string(value)?)
60}
61
62fn from_json<T: serde::de::DeserializeOwned>(text: &str) -> Result<T> {
63 Ok(serde_json::from_str(text)?)
64}
65
66fn check_name(name: &str) -> Result<()> {
67 if is_valid_name(name) {
68 Ok(())
69 } else {
70 Err(Error::InvalidName(name.to_owned()))
71 }
72}
73
74fn is_unique_violation(e: &rusqlite::Error) -> bool {
75 matches!(
76 e,
77 rusqlite::Error::SqliteFailure(f, _)
78 if f.code == rusqlite::ErrorCode::ConstraintViolation
79 )
80}
81
82fn duplicate_or(e: rusqlite::Error, kind: &'static str, name: &str) -> Error {
84 if is_unique_violation(&e) {
85 Error::Duplicate {
86 kind,
87 name: name.to_owned(),
88 }
89 } else {
90 e.into()
91 }
92}
93
94fn refuse_instances(
97 tx: &Transaction<'_>,
98 count: &str,
99 id: &str,
100 kind: &'static str,
101 name: &str,
102) -> Result<()> {
103 let instances: usize = tx.query_row(count, [id], |r| r.get(0))?;
104 if instances == 0 {
105 Ok(())
106 } else {
107 Err(Error::HasInstances {
108 kind,
109 name: name.to_owned(),
110 instances,
111 })
112 }
113}
114
115const WORKSPACE_INSTANCES: &str = "SELECT COUNT(*) FROM instance i \
116 JOIN environment e ON e.id = i.environment_id WHERE e.workspace_id = ?1";
117
118const ENVIRONMENT_INSTANCES: &str = "SELECT COUNT(*) FROM instance WHERE environment_id = ?1";
119
120pub struct Store {
128 conn: Mutex<Connection>,
129 path: PathBuf,
130}
131
132impl std::fmt::Debug for Store {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct("Store").field("path", &self.path).finish()
135 }
136}
137
138pub(crate) fn connect(path: &Path) -> Result<Connection> {
140 let conn = Connection::open(path)?;
141 conn.busy_timeout(BUSY_TIMEOUT)?;
142 conn.pragma_update(None, "journal_mode", "WAL")?;
143 conn.pragma_update(None, "foreign_keys", true)?;
144 conn.pragma_update(None, "synchronous", "NORMAL")?;
145 Ok(conn)
146}
147
148impl Store {
149 pub fn open_default() -> Result<Self> {
151 let home = trestle_home().ok_or_else(|| Error::NotFound {
152 kind: "data directory",
153 id: format!("(set {TRESTLE_HOME_VAR})"),
154 })?;
155 std::fs::create_dir_all(&home).map_err(|e| Error::io(&home, e))?;
156 Self::open(&home.join(DB_FILE))
157 }
158
159 pub fn open(path: &Path) -> Result<Self> {
163 let mut conn = connect(path)?;
164 migrate(&mut conn)?;
165 Ok(Store {
166 conn: Mutex::new(conn),
167 path: path.to_owned(),
168 })
169 }
170
171 pub fn path(&self) -> &Path {
173 &self.path
174 }
175
176 fn lock(&self) -> MutexGuard<'_, Connection> {
177 self.conn.lock().unwrap_or_else(|e| e.into_inner())
178 }
179
180 fn write<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
183 let mut conn = self.lock();
184 let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
185 let out = f(&tx)?;
186 tx.commit()?;
187 Ok(out)
188 }
189
190 fn read<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
192 f(&self.lock())
193 }
194
195 pub fn create_workspace(&self, name: &str, mesh: bool) -> Result<Workspace> {
199 check_name(name)?;
200 let ws = Workspace {
201 id: WorkspaceId::generate(),
202 name: name.to_owned(),
203 mesh,
204 vars: VarSet::new(),
205 sources: BTreeMap::new(),
206 };
207 self.write(|tx| {
208 tx.execute(
209 "INSERT INTO workspace (id, name, mesh, vars, sources, created_at) \
210 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
211 params![
212 ws.id.0,
213 ws.name,
214 ws.mesh,
215 json(&ws.vars)?,
216 json(&ws.sources)?,
217 now()
218 ],
219 )
220 .map_err(|e| duplicate_or(e, "workspace", name))?;
221 Ok(())
222 })?;
223 Ok(ws)
224 }
225
226 pub fn workspaces(&self) -> Result<Vec<Workspace>> {
228 self.read(|conn| {
229 let mut stmt = conn.prepare(&format!("{WS_COLUMNS} ORDER BY name"))?;
230 let rows = stmt.query_map([], workspace_row)?;
231 rows.map(|r| r?).collect()
232 })
233 }
234
235 pub fn workspace(&self, name: &str) -> Result<Option<Workspace>> {
237 self.read(|conn| {
238 conn.query_row(
239 &format!("{WS_COLUMNS} WHERE name = ?1"),
240 [name],
241 workspace_row,
242 )
243 .optional()?
244 .transpose()
245 })
246 }
247
248 pub fn workspace_by_id(&self, id: &WorkspaceId) -> Result<Workspace> {
250 self.read(|conn| {
251 conn.query_row(
252 &format!("{WS_COLUMNS} WHERE id = ?1"),
253 [&id.0],
254 workspace_row,
255 )
256 .optional()?
257 .transpose()?
258 .ok_or_else(|| Error::NotFound {
259 kind: "workspace",
260 id: id.0.clone(),
261 })
262 })
263 }
264
265 fn workspace_head(tx: &Transaction<'_>, id: &WorkspaceId) -> Result<(String, bool)> {
267 tx.query_row(
268 "SELECT name, mesh FROM workspace WHERE id = ?1",
269 [&id.0],
270 |r| Ok((r.get(0)?, r.get(1)?)),
271 )
272 .optional()?
273 .ok_or_else(|| Error::NotFound {
274 kind: "workspace",
275 id: id.0.clone(),
276 })
277 }
278
279 pub fn rename_workspace(&self, id: &WorkspaceId, name: &str) -> Result<()> {
286 check_name(name)?;
287 self.write(|tx| {
288 let (old, _) = Self::workspace_head(tx, id)?;
289 if old == name {
290 return Ok(());
291 }
292 refuse_instances(tx, WORKSPACE_INSTANCES, &id.0, "workspace", &old)?;
293 tx.execute(
294 "UPDATE workspace SET name = ?2 WHERE id = ?1",
295 params![id.0, name],
296 )
297 .map_err(|e| duplicate_or(e, "workspace", name))?;
298 Ok(())
299 })
300 }
301
302 pub fn set_mesh(&self, id: &WorkspaceId, mesh: bool) -> Result<()> {
309 self.write(|tx| {
310 let (name, current) = Self::workspace_head(tx, id)?;
311 if current == mesh {
312 return Ok(());
313 }
314 refuse_instances(tx, WORKSPACE_INSTANCES, &id.0, "workspace", &name)?;
315 tx.execute(
316 "UPDATE workspace SET mesh = ?2 WHERE id = ?1",
317 params![id.0, mesh],
318 )?;
319 Ok(())
320 })
321 }
322
323 pub fn set_workspace_vars(&self, id: &WorkspaceId, vars: &VarSet) -> Result<()> {
325 self.write(|tx| {
326 let n = tx.execute(
327 "UPDATE workspace SET vars = ?2 WHERE id = ?1",
328 params![id.0, json(vars)?],
329 )?;
330 found(n, "workspace", &id.0)
331 })
332 }
333
334 pub fn set_workspace_source(
337 &self,
338 id: &WorkspaceId,
339 name: &str,
340 config: Option<serde_json::Value>,
341 ) -> Result<()> {
342 self.write(|tx| {
343 let text: String = tx
344 .query_row(
345 "SELECT sources FROM workspace WHERE id = ?1",
346 [&id.0],
347 |r| r.get(0),
348 )
349 .optional()?
350 .ok_or_else(|| Error::NotFound {
351 kind: "workspace",
352 id: id.0.clone(),
353 })?;
354 let mut sources: BTreeMap<String, serde_json::Value> = from_json(&text)?;
355 match config {
356 Some(config) => sources.insert(name.to_owned(), config),
357 None => sources.remove(name),
358 };
359 tx.execute(
360 "UPDATE workspace SET sources = ?2 WHERE id = ?1",
361 params![id.0, json(&sources)?],
362 )?;
363 Ok(())
364 })
365 }
366
367 pub fn modify_vars<E: From<Error>>(
370 &self,
371 scope: &VarScope,
372 f: impl FnOnce(&mut VarSet) -> std::result::Result<(), E>,
373 ) -> std::result::Result<VarSet, E> {
374 let (table, column, kind, id) = match scope {
375 VarScope::Workspace(id) => ("workspace", "vars", "workspace", &id.0),
376 VarScope::Environment(id) => ("environment", "vars", "environment", &id.0),
377 VarScope::Draft(id) => ("environment", "draft_vars", "environment", &id.0),
378 };
379 self.modify(
380 &format!("SELECT {column} FROM {table} WHERE id = ?1"),
381 &format!("UPDATE {table} SET {column} = ?2 WHERE id = ?1"),
382 kind,
383 id,
384 |text| {
385 let mut vars: VarSet = from_json(text)?;
386 f(&mut vars)?;
387 Ok((json(&vars)?, vars))
388 },
389 )
390 }
391
392 fn modify<T, E: From<Error>>(
395 &self,
396 select: &str,
397 update: &str,
398 kind: &'static str,
399 id: &str,
400 f: impl FnOnce(&str) -> std::result::Result<(String, T), E>,
401 ) -> std::result::Result<T, E> {
402 let mut conn = self.lock();
403 let tx = conn
404 .transaction_with_behavior(TransactionBehavior::Immediate)
405 .map_err(Error::from)?;
406 let text: String = tx
407 .query_row(select, [id], |r| r.get(0))
408 .optional()
409 .map_err(Error::from)?
410 .ok_or_else(|| Error::NotFound {
411 kind,
412 id: id.to_owned(),
413 })?;
414 let (text, out) = f(&text)?;
415 tx.execute(update, params![id, text]).map_err(Error::from)?;
416 tx.commit().map_err(Error::from)?;
417 Ok(out)
418 }
419
420 pub fn delete_workspace(&self, id: &WorkspaceId) -> Result<()> {
422 self.write(|tx| {
423 let n = tx.execute("DELETE FROM workspace WHERE id = ?1", [&id.0])?;
424 found(n, "workspace", &id.0)
425 })
426 }
427
428 pub fn create_environment(
433 &self,
434 workspace: &WorkspaceId,
435 name: &str,
436 mut draft: Draft,
437 ) -> Result<Environment> {
438 check_name(name)?;
439 draft.manifest.context.env_name = name.to_owned();
440 let env = Environment {
441 id: EnvironmentId::generate(),
442 workspace: workspace.clone(),
443 name: name.to_owned(),
444 draft,
445 vars: VarSet::new(),
446 head: None,
447 };
448 self.write(|tx| {
449 tx.execute(
450 "INSERT INTO environment (id, workspace_id, name, draft, draft_modules, draft_vars, \
451 vars, head, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8)",
452 params![
453 env.id.0,
454 workspace.0,
455 env.name,
456 env.draft.manifest.to_toml()?,
457 json(&env.draft.modules)?,
458 json(&env.draft.vars)?,
459 json(&env.vars)?,
460 now()
461 ],
462 )
463 .map_err(|e| {
464 if matches!(&e, rusqlite::Error::SqliteFailure(f, _)
465 if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY)
466 {
467 Error::NotFound {
468 kind: "workspace",
469 id: workspace.0.clone(),
470 }
471 } else {
472 duplicate_or(e, "environment", name)
473 }
474 })?;
475 Ok(())
476 })?;
477 Ok(env)
478 }
479
480 pub fn environments(&self, workspace: &WorkspaceId) -> Result<Vec<Environment>> {
482 self.read(|conn| {
483 let mut stmt = conn.prepare(&format!(
484 "{ENV_COLUMNS} WHERE workspace_id = ?1 ORDER BY name"
485 ))?;
486 let rows = stmt.query_map([&workspace.0], environment_row)?;
487 rows.map(|r| r?).collect()
488 })
489 }
490
491 pub fn environment(&self, id: &EnvironmentId) -> Result<Environment> {
493 self.read(|conn| {
494 conn.query_row(
495 &format!("{ENV_COLUMNS} WHERE id = ?1"),
496 [&id.0],
497 environment_row,
498 )
499 .optional()?
500 .transpose()?
501 .ok_or_else(|| Error::NotFound {
502 kind: "environment",
503 id: id.0.clone(),
504 })
505 })
506 }
507
508 pub fn rename_environment(&self, id: &EnvironmentId, name: &str) -> Result<()> {
517 check_name(name)?;
518 self.write(|tx| {
519 let env = tx
520 .query_row(
521 &format!("{ENV_COLUMNS} WHERE id = ?1"),
522 [&id.0],
523 environment_row,
524 )
525 .optional()?
526 .transpose()?
527 .ok_or_else(|| Error::NotFound {
528 kind: "environment",
529 id: id.0.clone(),
530 })?;
531 if env.name == name {
532 return Ok(());
533 }
534 refuse_instances(tx, ENVIRONMENT_INSTANCES, &id.0, "environment", &env.name)?;
535 let mut manifest = env.draft.manifest;
536 manifest.context.env_name = name.to_owned();
537 tx.execute(
538 "UPDATE environment SET name = ?2, draft = ?3 WHERE id = ?1",
539 params![id.0, name, manifest.to_toml()?],
540 )
541 .map_err(|e| duplicate_or(e, "environment", name))?;
542 Ok(())
543 })
544 }
545
546 pub fn update_draft(&self, id: &EnvironmentId, draft: &Draft) -> Result<()> {
549 self.modify_draft(id, |d| {
550 *d = draft.clone();
551 Ok::<_, Error>(())
552 })?;
553 Ok(())
554 }
555
556 pub fn modify_draft<E: From<Error>>(
560 &self,
561 id: &EnvironmentId,
562 f: impl FnOnce(&mut Draft) -> std::result::Result<(), E>,
563 ) -> std::result::Result<Draft, E> {
564 let mut conn = self.lock();
565 let tx = conn
566 .transaction_with_behavior(TransactionBehavior::Immediate)
567 .map_err(Error::from)?;
568 let env = tx
569 .query_row(
570 &format!("{ENV_COLUMNS} WHERE id = ?1"),
571 [&id.0],
572 environment_row,
573 )
574 .optional()
575 .map_err(Error::from)?
576 .transpose()?
577 .ok_or_else(|| Error::NotFound {
578 kind: "environment",
579 id: id.0.clone(),
580 })?;
581 let mut draft = env.draft;
582 f(&mut draft)?;
583 draft.manifest.context.env_name = env.name;
584 tx.execute(
585 "UPDATE environment SET draft = ?2, draft_modules = ?3, draft_vars = ?4 WHERE id = ?1",
586 params![
587 id.0,
588 draft.manifest.to_toml().map_err(Error::from)?,
589 json(&draft.modules)?,
590 json(&draft.vars)?
591 ],
592 )
593 .map_err(Error::from)?;
594 tx.commit().map_err(Error::from)?;
595 Ok(draft)
596 }
597
598 pub fn set_environment_vars(&self, id: &EnvironmentId, vars: &VarSet) -> Result<()> {
600 self.write(|tx| {
601 let n = tx.execute(
602 "UPDATE environment SET vars = ?2 WHERE id = ?1",
603 params![id.0, json(vars)?],
604 )?;
605 found(n, "environment", &id.0)
606 })
607 }
608
609 pub fn delete_environment(&self, id: &EnvironmentId) -> Result<()> {
611 self.write(|tx| {
612 let n = tx.execute("DELETE FROM environment WHERE id = ?1", [&id.0])?;
613 found(n, "environment", &id.0)
614 })
615 }
616
617 pub fn commit_draft(&self, id: &EnvironmentId) -> Result<Revision> {
621 self.commit_draft_with(id, None)
622 }
623
624 pub fn commit_draft_with(&self, id: &EnvironmentId, message: Option<&str>) -> Result<Revision> {
627 let message = message
628 .map(str::trim)
629 .filter(|m| !m.is_empty())
630 .map(str::to_owned);
631 self.write(|tx| {
632 let (draft, modules, vars, head): (String, String, String, Option<u32>) = tx
633 .query_row(
634 "SELECT draft, draft_modules, draft_vars, head FROM environment WHERE id = ?1",
635 [&id.0],
636 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
637 )
638 .optional()?
639 .ok_or_else(|| Error::NotFound {
640 kind: "environment",
641 id: id.0.clone(),
642 })?;
643 let number = head.unwrap_or(0) + 1;
644 let modules: BTreeMap<String, String> = from_json(&modules)?;
645 let vars: VarSet = from_json(&vars)?;
646 let digest = digest(&draft, &modules, &vars)?;
647 let created_at = now();
648 tx.execute(
649 "INSERT INTO revision (environment_id, number, manifest, modules, vars, digest, \
650 message, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
651 params![
652 id.0,
653 number,
654 draft,
655 json(&modules)?,
656 json(&vars)?,
657 digest,
658 message,
659 created_at
660 ],
661 )?;
662 tx.execute(
663 "UPDATE environment SET head = ?2 WHERE id = ?1",
664 params![id.0, number],
665 )?;
666 Ok(Revision {
667 environment: id.clone(),
668 number,
669 manifest: EnvManifest::from_toml(&draft)?,
670 manifest_toml: draft,
671 modules,
672 vars,
673 digest,
674 message,
675 created_at: time_of(created_at),
676 })
677 })
678 }
679
680 pub fn revision(&self, env: &EnvironmentId, number: u32) -> Result<Revision> {
682 self.read(|conn| {
683 conn.query_row(
684 &format!("{REV_COLUMNS} WHERE environment_id = ?1 AND number = ?2"),
685 params![env.0, number],
686 revision_row,
687 )
688 .optional()?
689 .transpose()?
690 .ok_or_else(|| Error::NotFound {
691 kind: "revision",
692 id: format!("{env}#{number}"),
693 })
694 })
695 }
696
697 pub fn revisions(&self, env: &EnvironmentId) -> Result<Vec<Revision>> {
699 self.read(|conn| {
700 let mut stmt = conn.prepare(&format!(
701 "{REV_COLUMNS} WHERE environment_id = ?1 ORDER BY number"
702 ))?;
703 let rows = stmt.query_map([&env.0], revision_row)?;
704 rows.map(|r| r?).collect()
705 })
706 }
707
708 pub fn create_instance(&self, env: &EnvironmentId, revision: u32) -> Result<Instance> {
713 let instance = Instance {
714 id: InstanceId::generate(),
715 environment: env.clone(),
716 revision,
717 desired: Desired::Stopped,
718 overrides: VarSet::new(),
719 sources: Vec::new(),
720 slot: None,
721 };
722 self.write(|tx| {
723 tx.execute(
724 "INSERT INTO instance (id, environment_id, revision, desired, overrides, sources, \
725 created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
726 params![
727 instance.id.0,
728 env.0,
729 revision,
730 instance.desired.as_str(),
731 json(&instance.overrides)?,
732 json(&instance.sources)?,
733 now()
734 ],
735 )
736 .map_err(|e| match &e {
737 rusqlite::Error::SqliteFailure(f, _)
738 if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
739 {
740 Error::NotFound {
741 kind: "revision",
742 id: format!("{env}#{revision}"),
743 }
744 }
745 _ => e.into(),
746 })?;
747 Ok(())
748 })?;
749 Ok(instance)
750 }
751
752 pub fn instance(&self, id: &InstanceId) -> Result<Instance> {
754 self.read(|conn| {
755 conn.query_row(
756 &format!("{INST_COLUMNS} WHERE i.id = ?1"),
757 [&id.0],
758 instance_row,
759 )
760 .optional()?
761 .transpose()?
762 .ok_or_else(|| Error::NotFound {
763 kind: "instance",
764 id: id.0.clone(),
765 })
766 })
767 }
768
769 pub fn instances(&self) -> Result<Vec<Instance>> {
771 self.read(|conn| {
772 let mut stmt = conn.prepare(&format!("{INST_COLUMNS} ORDER BY i.created_at, i.id"))?;
773 let rows = stmt.query_map([], instance_row)?;
774 rows.map(|r| r?).collect()
775 })
776 }
777
778 pub fn instances_of(&self, env: &EnvironmentId) -> Result<Vec<Instance>> {
780 self.read(|conn| {
781 let mut stmt = conn.prepare(&format!(
782 "{INST_COLUMNS} WHERE i.environment_id = ?1 ORDER BY i.created_at, i.id"
783 ))?;
784 let rows = stmt.query_map([&env.0], instance_row)?;
785 rows.map(|r| r?).collect()
786 })
787 }
788
789 pub fn set_desired(&self, id: &InstanceId, desired: Desired) -> Result<()> {
791 self.write(|tx| {
792 let n = tx.execute(
793 "UPDATE instance SET desired = ?2 WHERE id = ?1",
794 params![id.0, desired.as_str()],
795 )?;
796 found(n, "instance", &id.0)
797 })
798 }
799
800 pub fn set_overrides(&self, id: &InstanceId, overrides: &VarSet) -> Result<()> {
802 self.write(|tx| {
803 let n = tx.execute(
804 "UPDATE instance SET overrides = ?2 WHERE id = ?1",
805 params![id.0, json(overrides)?],
806 )?;
807 found(n, "instance", &id.0)
808 })
809 }
810
811 pub fn set_sources(&self, id: &InstanceId, sources: &[SourceBinding]) -> Result<()> {
813 self.write(|tx| {
814 let n = tx.execute(
815 "UPDATE instance SET sources = ?2 WHERE id = ?1",
816 params![id.0, json(&sources)?],
817 )?;
818 found(n, "instance", &id.0)
819 })
820 }
821
822 pub fn delete_instance(&self, id: &InstanceId) -> Result<()> {
824 self.write(|tx| {
825 let n = tx.execute("DELETE FROM instance WHERE id = ?1", [&id.0])?;
826 found(n, "instance", &id.0)
827 })
828 }
829
830 pub fn lease_slot(&self, instance: &InstanceId) -> Result<SlotLease> {
835 self.write(|tx| {
836 let held: Option<u16> = tx
837 .query_row(
838 "SELECT slot FROM slot_lease WHERE instance_id = ?1",
839 [&instance.0],
840 |r| r.get(0),
841 )
842 .optional()?;
843 if let Some(slot) = held {
844 return Ok(SlotLease {
845 slot,
846 instance: instance.clone(),
847 });
848 }
849 let taken: Vec<u16> = tx
850 .prepare("SELECT slot FROM slot_lease ORDER BY slot")?
851 .query_map([], |r| r.get(0))?
852 .collect::<rusqlite::Result<_>>()?;
853 let slot = (0..=MAX_SLOT)
854 .find(|s| taken.binary_search(s).is_err())
855 .ok_or(Error::SlotsExhausted)?;
856 tx.execute(
857 "INSERT INTO slot_lease (slot, instance_id, leased_at) VALUES (?1, ?2, ?3)",
858 params![slot, instance.0, now()],
859 )
860 .map_err(|e| match &e {
861 rusqlite::Error::SqliteFailure(f, _)
862 if f.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY =>
863 {
864 Error::NotFound {
865 kind: "instance",
866 id: instance.0.clone(),
867 }
868 }
869 _ => e.into(),
870 })?;
871 Ok(SlotLease {
872 slot,
873 instance: instance.clone(),
874 })
875 })
876 }
877
878 pub fn release_slot(&self, instance: &InstanceId) -> Result<()> {
880 self.write(|tx| {
881 tx.execute(
882 "DELETE FROM slot_lease WHERE instance_id = ?1",
883 [&instance.0],
884 )?;
885 Ok(())
886 })
887 }
888
889 pub fn leases(&self) -> Result<Vec<SlotLease>> {
891 self.read(|conn| {
892 let mut stmt =
893 conn.prepare("SELECT slot, instance_id FROM slot_lease ORDER BY slot")?;
894 let rows = stmt.query_map([], |r| {
895 Ok(SlotLease {
896 slot: r.get(0)?,
897 instance: InstanceId(r.get(1)?),
898 })
899 })?;
900 Ok(rows.collect::<rusqlite::Result<_>>()?)
901 })
902 }
903}
904
905fn found(rows: usize, kind: &'static str, id: &str) -> Result<()> {
906 if rows == 0 {
907 Err(Error::NotFound {
908 kind,
909 id: id.to_owned(),
910 })
911 } else {
912 Ok(())
913 }
914}
915
916fn migrate(conn: &mut Connection) -> Result<()> {
919 let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
920 let found: u32 = tx.query_row("PRAGMA user_version", [], |r| r.get(0))?;
921 if found > SCHEMA_VERSION {
922 return Err(Error::UnsupportedVersion {
923 found,
924 supported: SCHEMA_VERSION,
925 });
926 }
927 for sql in &MIGRATIONS[found as usize..] {
928 tx.execute_batch(sql)?;
929 }
930 tx.pragma_update(None, "user_version", SCHEMA_VERSION)?;
931 tx.commit()?;
932 Ok(())
933}
934
935const WS_COLUMNS: &str = "SELECT id, name, mesh, vars, sources FROM workspace";
936
937fn workspace_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Workspace>> {
938 let (vars, sources): (String, String) = (r.get(3)?, r.get(4)?);
939 let (id, name, mesh) = (r.get(0)?, r.get(1)?, r.get(2)?);
940 Ok((|| {
941 Ok(Workspace {
942 id: WorkspaceId(id),
943 name,
944 mesh,
945 vars: from_json(&vars)?,
946 sources: from_json(&sources)?,
947 })
948 })())
949}
950
951const ENV_COLUMNS: &str =
952 "SELECT id, workspace_id, name, draft, draft_modules, draft_vars, vars, head FROM environment";
953
954fn environment_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Environment>> {
955 let (id, workspace, name): (String, String, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
956 let (draft, modules, draft_vars, vars): (String, String, String, String) =
957 (r.get(3)?, r.get(4)?, r.get(5)?, r.get(6)?);
958 let head: Option<u32> = r.get(7)?;
959 Ok((|| {
960 Ok(Environment {
961 id: EnvironmentId(id),
962 workspace: WorkspaceId(workspace),
963 name,
964 draft: Draft {
965 manifest: EnvManifest::from_toml(&draft)?,
966 modules: from_json(&modules)?,
967 vars: from_json(&draft_vars)?,
968 },
969 vars: from_json(&vars)?,
970 head,
971 })
972 })())
973}
974
975const REV_COLUMNS: &str = "SELECT environment_id, number, manifest, modules, vars, digest, \
976 message, created_at FROM revision";
977
978fn revision_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Revision>> {
979 let (env, number, manifest_toml): (String, u32, String) = (r.get(0)?, r.get(1)?, r.get(2)?);
980 let (modules, vars, digest): (String, String, String) = (r.get(3)?, r.get(4)?, r.get(5)?);
981 let (message, created_at): (Option<String>, i64) = (r.get(6)?, r.get(7)?);
982 Ok((|| {
983 Ok(Revision {
984 environment: EnvironmentId(env),
985 number,
986 manifest: EnvManifest::from_toml(&manifest_toml)?,
987 manifest_toml,
988 modules: from_json(&modules)?,
989 vars: from_json(&vars)?,
990 digest,
991 message,
992 created_at: time_of(created_at),
993 })
994 })())
995}
996
997const INST_COLUMNS: &str = "SELECT i.id, i.environment_id, i.revision, i.desired, i.overrides, \
998 i.sources, l.slot FROM instance i LEFT JOIN slot_lease l ON l.instance_id = i.id";
999
1000fn instance_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Result<Instance>> {
1001 let (id, env, revision, desired): (String, String, u32, String) =
1002 (r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?);
1003 let (overrides, sources, slot): (String, String, Option<u16>) =
1004 (r.get(4)?, r.get(5)?, r.get(6)?);
1005 Ok((|| {
1006 Ok(Instance {
1007 id: InstanceId(id),
1008 environment: EnvironmentId(env),
1009 revision,
1010 desired: Desired::parse(&desired),
1011 overrides: from_json(&overrides)?,
1012 sources: from_json(&sources)?,
1013 slot,
1014 })
1015 })())
1016}