Skip to main content

spiral/
table_session.rs

1//! Repeatable resource resolution in one project.
2
3use std::collections::HashMap;
4use std::fmt::{self, Debug, Formatter};
5use std::sync::Arc;
6
7use parking_lot::Mutex;
8use tokio::sync::OnceCell;
9
10use crate::catalog_types::{ProjectBranchName, VersionId};
11use crate::{BranchHead, Error, Project, ResourceAddress, ResourceRef, Snapshot, Table};
12
13/// A default snapshot and branch heads pinned on first use.
14///
15/// Clones share pins. Refresh creates a separate session, leaving existing
16/// tables, scans, and sessions unchanged.
17#[derive(Clone)]
18pub struct TableSession {
19    project: Project,
20    default_ref: ResourceRef,
21    snapshot: Snapshot,
22    branches: Arc<Mutex<HashMap<ProjectBranchName, Arc<OnceCell<Snapshot>>>>>,
23}
24
25impl Debug for TableSession {
26    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
27        formatter
28            .debug_struct("TableSession")
29            .field("project", self.project.id())
30            .field("default_ref", &self.default_ref)
31            .field("commit", &self.snapshot.commit_id())
32            .finish()
33    }
34}
35
36impl Project {
37    /// Open a table session with a branch or `c`-prefixed commit as its default ref.
38    pub async fn table_session(&self, default_ref: &str) -> Result<TableSession, Error> {
39        TableSession::open(self.clone(), default_ref.parse()?).await
40    }
41}
42
43impl Snapshot {
44    /// Resolve qualified resource addresses with this exact commit as the default.
45    pub fn table_session(&self) -> TableSession {
46        TableSession {
47            project: self.project().clone(),
48            default_ref: ResourceRef::Commit(self.commit_id()),
49            snapshot: self.clone(),
50            branches: Arc::new(Mutex::new(HashMap::new())),
51        }
52    }
53}
54
55impl TableSession {
56    async fn open(project: Project, default_ref: ResourceRef) -> Result<Self, Error> {
57        let snapshot = match &default_ref {
58            ResourceRef::Branch(branch) => {
59                project.branch(branch.as_str()).await?.snapshot().clone()
60            }
61            ResourceRef::Commit(commit) => project.snapshot_at(*commit),
62            ResourceRef::Version(_) => {
63                return Err(Error::invalid(
64                    "a session default must be a branch or project commit, not a table version",
65                ));
66            }
67        };
68        let mut branches = HashMap::new();
69        if let ResourceRef::Branch(branch) = &default_ref {
70            branches.insert(
71                branch.clone(),
72                Arc::new(OnceCell::new_with(Some(snapshot.clone()))),
73            );
74        }
75        Ok(Self {
76            project,
77            default_ref,
78            snapshot,
79            branches: Arc::new(Mutex::new(branches)),
80        })
81    }
82
83    /// Default ref selected when the session was opened.
84    pub fn default_ref(&self) -> &ResourceRef {
85        &self.default_ref
86    }
87
88    /// Immutable default snapshot.
89    pub fn snapshot(&self) -> &Snapshot {
90        &self.snapshot
91    }
92
93    /// The pinned head of the default branch, which commits publish onto; `None` when the
94    /// default ref is an exact commit.
95    pub fn head(&self) -> Option<BranchHead> {
96        BranchHead::of(&self.snapshot)
97    }
98
99    /// Start a separate session at a newer head of the default branch, such as a published
100    /// commit's.
101    ///
102    /// The default branch name is preserved and other branch pins are reset.
103    /// Existing tables and sessions keep their original immutable bindings.
104    pub fn with_head(&self, head: BranchHead) -> Result<Self, Error> {
105        let ResourceRef::Branch(branch) = &self.default_ref else {
106            return Err(Error::invalid("session must select a default branch"));
107        };
108        let snapshot = head.snapshot().clone();
109        if !Arc::ptr_eq(
110            &self.project.client.connection,
111            &snapshot.project.client.connection,
112        ) || self.project.id != snapshot.project.id
113            || self.snapshot.branch_id != Some(head.branch_id())
114        {
115            return Err(Error::invalid("head must be of this session's branch"));
116        }
117        let branches = HashMap::from([(
118            branch.clone(),
119            Arc::new(OnceCell::new_with(Some(snapshot.clone()))),
120        )]);
121        Ok(Self {
122            project: self.project.clone(),
123            default_ref: self.default_ref.clone(),
124            snapshot,
125            branches: Arc::new(Mutex::new(branches)),
126        })
127    }
128
129    /// Open a new session with fresh branch pins and the same default ref.
130    pub async fn refresh(&self) -> Result<Self, Error> {
131        if matches!(self.default_ref, ResourceRef::Branch(_)) {
132            Self::open(self.project.clone(), self.default_ref.clone()).await
133        } else {
134            Ok(self.snapshot.table_session())
135        }
136    }
137
138    /// Resolve `path`, `path@branch`, `path@c<commit>`, or `path@v<version>`.
139    ///
140    /// Exact versions identify their resource through the default snapshot's
141    /// path. Names and refs never cross into scandal.
142    pub async fn table(&self, address: &str) -> Result<Table, Error> {
143        let address: ResourceAddress = address.parse()?;
144        let snapshot = match address.reference() {
145            None | Some(ResourceRef::Version(_)) => self.snapshot.clone(),
146            Some(ResourceRef::Commit(commit)) => self.project.snapshot_at(*commit),
147            Some(ResourceRef::Branch(branch)) => {
148                let pin = {
149                    let mut branches = self.branches.lock();
150                    Arc::clone(
151                        branches
152                            .entry(branch.clone())
153                            .or_insert_with(|| Arc::new(OnceCell::new())),
154                    )
155                };
156                pin.get_or_try_init(|| async {
157                    Ok::<_, Error>(
158                        self.project
159                            .branch(branch.as_str())
160                            .await?
161                            .snapshot()
162                            .clone(),
163                    )
164                })
165                .await?
166                .clone()
167            }
168        };
169        let mut table = snapshot.table_at_path(address.path()).await?;
170        if let Some(ResourceRef::Version(version)) = address.reference() {
171            table = self
172                .project
173                .client
174                .table_by_id(table.identity().resource_id, version.as_uuid());
175        }
176        let version = VersionId::new(table.identity().version_id).map_err(Error::invalid)?;
177        let address = address.at_version(version);
178        if let Some(base) = &mut table.base {
179            base.address = Some(address.clone());
180        }
181        table.address = Some(address);
182        Ok(table)
183    }
184}