Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/oak_db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ url.workspace = true

[dev-dependencies]
annotate-snippets.workspace = true
anyhow.workspace = true
insta.workspace = true
oak_semantic = { workspace = true, features = ["salsa", "testing"] }
oak_tidy.workspace = true
Expand Down
12 changes: 10 additions & 2 deletions crates/oak_db/src/db.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::io;

use aether_path::FilePath;
use camino::Utf8Path;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;

Expand All @@ -12,8 +15,8 @@ use crate::StaleRoot;
use crate::WorkspaceRoots;

/// Concrete-input surface of the salsa database. Each impl
/// ([`crate::OakDatabase`], the test db) supplies the three singleton input
/// handles.
/// ([`crate::OakDatabase`], the test db) supplies singleton input handles
/// and file reads.
///
/// Kept separate from [`Db`] (the query trait) so input accessors and derived
/// queries live on different traits. Mirrors rust-analyzer's `SourceDatabase`
Expand All @@ -33,6 +36,11 @@ pub trait DbInputs: salsa::Database {
/// Files and packages from roots that have been removed. Holding
/// pen for entity reuse on re-add (see [`StaleRoot`]).
fn stale_root(&self) -> StaleRoot;

/// Read through the database's file reader. Note that this method is not
/// tracked and callers _must_ depend on the corresponding revision input
/// for the file before reading.
fn read_to_string(&self, path: &Utf8Path) -> io::Result<String>;
}

/// Salsa database trait used throughout `oak_db`. Tracked queries take `&dyn
Expand Down
4 changes: 1 addition & 3 deletions crates/oak_db/src/file.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::fs;

use aether_path::FilePath;
use oak_semantic::semantic_index::SemanticDiagnostic;
use oak_semantic::semantic_index::SemanticIndex;
Expand Down Expand Up @@ -99,7 +97,7 @@ impl File {
return String::new();
};

match fs::read_to_string(path.as_path().as_std_path()) {
match db.read_to_string(path.as_path()) {
Ok(text) => text,
Err(err) => {
// A file we were asked to analyze but can't read (permissions,
Expand Down
34 changes: 34 additions & 0 deletions crates/oak_db/src/file_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! File reads used by Oak queries. Path resolution remains independent of I/O.

use std::fs;
use std::io;

use camino::Utf8Path;

/// Supplies file contents to queries when no editor or namespace override is set.
///
/// Readers must be shared by database snapshots. Changes to their contents must
/// be accompanied by the corresponding file or package revision bump, just like
/// filesystem changes. The reader itself is fixed for the database's lifetime.
pub(crate) trait FileReader: Send + Sync {
fn read_to_string(&self, path: &Utf8Path) -> io::Result<String>;
}

pub(crate) struct DiskFileReader;

impl FileReader for DiskFileReader {
fn read_to_string(&self, path: &Utf8Path) -> io::Result<String> {
fs::read_to_string(path)
}
}

/// Fixtures supply source and namespace overrides; all other files are absent.
#[cfg(test)]
pub(crate) struct EmptyFileReader;

#[cfg(test)]
impl FileReader for EmptyFileReader {
fn read_to_string(&self, _path: &Utf8Path) -> io::Result<String> {
Err(io::ErrorKind::NotFound.into())
}
}
1 change: 1 addition & 0 deletions crates/oak_db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod file;
mod file_diagnostics;
mod file_exports;
mod file_imports;
mod file_reader;
mod file_resolve;
mod file_revision;
mod file_source_site;
Expand Down
7 changes: 3 additions & 4 deletions crates/oak_db/src/package.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::fs;
use std::io;

use aether_path::FilePath;
Expand Down Expand Up @@ -117,7 +116,7 @@ impl Package {
};

let namespace_path = dir.join("NAMESPACE");
match fs::read_to_string(namespace_path.as_std_path()) {
match db.read_to_string(&namespace_path) {
Ok(text) => Namespace::parse(&text).log_err().unwrap_or_default(),
// A package needn't ship a `NAMESPACE`, so absence is the normal
// case and stays quiet. A file that exists but can't be read is
Expand Down Expand Up @@ -220,7 +219,7 @@ impl Package {
report_untracked_if_zero(db, self.description_revision(db));

let path = self.description_path(db).as_path()?;
match fs::read_to_string(path.as_std_path()) {
match db.read_to_string(path) {
Ok(text) => Description::parse(&text).log_err(),
// A missing `DESCRIPTION` is the normal "gone after a rescan" case
// and stays quiet. A file that exists but can't be read is logged
Expand Down Expand Up @@ -260,7 +259,7 @@ impl Package {
// The `index_revision()` early exit handled workspace packages, so we only handle
// installed packages here. If an `INDEX` is missing, we silently return an empty
// one ({translations} is an example). Otherwise, failure to parse logs an error.
match fs::read_to_string(path.as_std_path()) {
match db.read_to_string(&path) {
Ok(text) => Some(Index::parse(&text)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Some(Index::default()),
Err(err) => {
Expand Down
31 changes: 30 additions & 1 deletion crates/oak_db/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::io;
use std::sync::Arc;
use std::sync::OnceLock;

use camino::Utf8Path;

use crate::file_reader::DiskFileReader;
use crate::file_reader::FileReader;
use crate::Db;
use crate::DbInputs;
use crate::LibraryRoots;
Expand All @@ -13,9 +18,9 @@ use crate::WorkspaceRoots;
/// Holds singleton `WorkspaceRoots` / `LibraryRoots` / `OrphanRoot` /
/// `StaleRoot` inputs and lazy-initialises them on first access.
#[salsa::db]
#[derive(Default)]
pub struct OakDatabase {
storage: salsa::Storage<Self>,
file_reader: Arc<dyn FileReader>,
workspace_roots: Arc<OnceLock<WorkspaceRoots>>,
library_roots: Arc<OnceLock<LibraryRoots>>,
orphan_root: Arc<OnceLock<OrphanRoot>>,
Expand All @@ -29,6 +34,19 @@ impl OakDatabase {
Self::default()
}

/// Construct a database whose queries read from the supplied reader.
pub(crate) fn with_file_reader(reader: impl FileReader + 'static) -> Self {
Self {
storage: salsa::Storage::default(),
file_reader: Arc::new(reader),
workspace_roots: Arc::default(),
library_roots: Arc::default(),
orphan_root: Arc::default(),
stale_root: Arc::default(),
holds: Arc::default(),
}
}

/// A snapshot handle onto the database for a background reader.
///
/// When the main loop needs to write to a `&mut OakDatabase`, it gets
Expand All @@ -41,6 +59,7 @@ impl OakDatabase {
pub fn snapshot(&self) -> Self {
Self {
storage: self.storage.clone(),
file_reader: Arc::clone(&self.file_reader),
workspace_roots: Arc::clone(&self.workspace_roots),
library_roots: Arc::clone(&self.library_roots),
orphan_root: Arc::clone(&self.orphan_root),
Expand All @@ -57,6 +76,12 @@ impl OakDatabase {
}
}

impl Default for OakDatabase {
fn default() -> Self {
Self::with_file_reader(DiskFileReader)
}
}

#[salsa::db]
impl salsa::Database for OakDatabase {}

Expand All @@ -68,6 +93,10 @@ impl std::fmt::Debug for OakDatabase {

#[salsa::db]
impl DbInputs for OakDatabase {
fn read_to_string(&self, path: &Utf8Path) -> io::Result<String> {
self.file_reader.read_to_string(path)
}

fn workspace_roots(&self) -> WorkspaceRoots {
*self
.workspace_roots
Expand Down
1 change: 1 addition & 0 deletions crates/oak_db/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod file_diagnostics;
mod file_exports;
mod file_imports;
mod file_imports_at;
mod file_reader;
mod file_resolve;
mod file_resolve_at;
mod file_root;
Expand Down
3 changes: 2 additions & 1 deletion crates/oak_db/src/tests/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::tests::test_db::file_path;
use crate::tests::test_db::TestDb;
use crate::File;
use crate::FileRevision;
use crate::OakDatabase;

/// File entities are created directly with `File::new` so these tests
/// stay focused on per-query behavior (caching, backdating) without
Expand Down Expand Up @@ -35,7 +36,7 @@ fn test_source_text_rereads_disk_when_revision_bumps() {
fs::write(&path, "v1\n").unwrap();
let url = FilePath::from_path_buf(path.clone()).unwrap();

let mut db = TestDb::new();
let mut db = OakDatabase::new();
let file = File::new(&db, url, FileRevision::zero(), None, None);
assert_eq!(file.source_text(&db), "v1\n");

Expand Down
109 changes: 109 additions & 0 deletions crates/oak_db/src/tests/file_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::fs;

use aether_path::FilePath;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use oak_package_metadata::index::Index;
use oak_package_metadata::namespace::Namespace;
use salsa::Setter;

use crate::file_reader::EmptyFileReader;
use crate::tests::test_db::TestDb;
use crate::Db;
use crate::File;
use crate::FileRevision;
use crate::OakDatabase;
use crate::Package;

#[test]
fn fixture_readers_ignore_existing_host_files() -> anyhow::Result<()> {
let temp = tempfile::tempdir()?;
let dir =
Utf8Path::from_path(temp.path()).ok_or_else(|| anyhow::anyhow!("Non-UTF-8 temp path"))?;
let files = fixture_contents(dir);
for (path, text) in &files {
fs::write(path, text)?;
}

// Establish that all four host files are readable and valid.
assert_query_contents(&mut OakDatabase::new(), dir, true)?;
assert_query_contents(&mut TestDb::new(), dir, false)?;

// The empty reader must also survive cloning into a background snapshot.
let db = OakDatabase::with_file_reader(EmptyFileReader);
let mut snapshot = db.snapshot();
drop(db);
assert_query_contents(&mut snapshot, dir, false)?;
Ok(())
}

#[test]
fn queries_read_in_memory_files() -> anyhow::Result<()> {
let dir = crate::tests::test_db::file_path("pkg");
let dir = dir
.as_path()
.ok_or_else(|| anyhow::anyhow!("Expected filesystem path"))?;
let mut db = TestDb::with_files(fixture_contents(dir));
assert_query_contents(&mut db, dir, true)
}

fn fixture_contents(dir: &Utf8Path) -> [(Utf8PathBuf, String); 4] {
[
(dir.join("a.R"), "x <- 1\n".to_string()),
(dir.join("NAMESPACE"), "export(x)\n".to_string()),
(
dir.join("DESCRIPTION"),
"Package: pkg\nVersion: 1.0.0\n".to_string(),
),
(dir.join("INDEX"), "x A dataset\n".to_string()),
]
}

fn assert_query_contents(db: &mut impl Db, dir: &Utf8Path, present: bool) -> anyhow::Result<()> {
let source_path = FilePath::from_path_buf(dir.join("a.R").into_std_path_buf())
.ok_or_else(|| anyhow::anyhow!("Expected absolute source path"))?;
let description_path = FilePath::from_path_buf(dir.join("DESCRIPTION").into_std_path_buf())
.ok_or_else(|| anyhow::anyhow!("Expected absolute DESCRIPTION path"))?;
let file = File::new(
db,
source_path,
FileRevision::from(1u128),
Some("editor text".to_string()),
None,
);
assert_eq!(file.source_text(db), "editor text");
file.set_source_text_override(db).to(None);
assert_eq!(file.source_text(db), if present { "x <- 1\n" } else { "" });

let overridden_namespace = Namespace::parse("export(editor)")?;
let package = Package::new(
db,
description_path,
"pkg".to_string(),
FileRevision::from(1u128),
FileRevision::from(1u128),
Some(FileRevision::from(1u128)),
Some(overridden_namespace.clone()),
vec![],
vec![],
);
assert_eq!(package.namespace(db), &overridden_namespace);
package.set_namespace_override(db).to(None);
let namespace = if present {
Namespace::parse("export(x)")?
} else {
Namespace::default()
};
assert_eq!(package.namespace(db), &namespace);
assert_eq!(
package.version(db).as_deref(),
if present { Some("1.0.0") } else { None }
);
let index = if present {
Index::parse("x A dataset\n")
} else {
Index::default()
};
assert_eq!(package.index(db), &Some(index));
Ok(())
}
11 changes: 6 additions & 5 deletions crates/oak_db/src/tests/file_root.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use salsa::Setter;

use crate::file_reader::EmptyFileReader;
use crate::tests::test_db::file_path;
use crate::tests::test_db::library_root;
use crate::tests::test_db::workspace_root;
Expand All @@ -11,15 +12,15 @@ use crate::Package;

#[test]
fn test_root_returns_none_for_orphan_file_outside_workspace() {
let db = OakDatabase::new();
let db = OakDatabase::with_file_reader(EmptyFileReader);
let file = File::new(&db, file_path("orphan.R"), FileRevision::zero(), None, None);

assert_eq!(file.root(&db), None);
}

#[test]
fn test_root_finds_containing_workspace_for_orphan_file() {
let mut db = OakDatabase::new();
let mut db = OakDatabase::with_file_reader(EmptyFileReader);
let workspace = workspace_root(&db, "proj");
db.workspace_roots().set_roots(&mut db).to(vec![workspace]);

Expand All @@ -35,7 +36,7 @@ fn test_root_finds_containing_workspace_for_orphan_file() {

#[test]
fn test_root_returns_longest_prefix_for_orphan_file() {
let mut db = OakDatabase::new();
let mut db = OakDatabase::with_file_reader(EmptyFileReader);
let outer = workspace_root(&db, "proj");
let inner = workspace_root(&db, "proj/inner");
db.workspace_roots()
Expand Down Expand Up @@ -63,7 +64,7 @@ fn test_root_returns_longest_prefix_for_orphan_file() {

#[test]
fn test_root_dispatches_through_library_package_when_set() {
let mut db = OakDatabase::new();
let mut db = OakDatabase::with_file_reader(EmptyFileReader);
let pkg_root = library_root(&db, "libs/mypkg");
let pkg = Package::new(
&db,
Expand Down Expand Up @@ -97,7 +98,7 @@ fn test_root_dispatches_through_workspace_package_when_set() {
// Same dispatch as the library case, but the owning root is a
// `Workspace` kind. The URL-prefix fallback is *not* consulted here
// because `package` is set.
let mut db = OakDatabase::new();
let mut db = OakDatabase::with_file_reader(EmptyFileReader);
let pkg_root = workspace_root(&db, "proj");
let pkg = Package::new(
&db,
Expand Down
Loading
Loading