diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cefc7119eb..3eaa27f0dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `reduce()`, `version()`, and `unique()` have moved. If you're using a `CollectionView`, the implementation should now be a combination of `View` and `CollectionViewSchema`. +- `CollectionName`, `SchemaName`, and `Name` all no longer generate errors if + using invalid characters. When BonsaiDb needs to use these names in a context + that must be able to be parsed, the names are encoded automatically into a + safe format. This change also means that `View::view_name()`, + `Collection::collection_name()`, and `Schema::schema_name()` have been updated + to not return error types. ### Fixed diff --git a/README.md b/README.md index 0eba2f812c0..de876589620 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ struct Shape { } impl Collection for Shape { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "shapes") } @@ -46,7 +46,7 @@ impl View for ShapesByNumberOfSides { type Key = u32; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-number-of-sides") } } diff --git a/benchmarks/benches/collections/bonsai.rs b/benchmarks/benches/collections/bonsai.rs index 8479140ce91..4ac7116d6d7 100644 --- a/benchmarks/benches/collections/bonsai.rs +++ b/benchmarks/benches/collections/bonsai.rs @@ -1,7 +1,7 @@ use bonsaidb::{ core::{ connection::Connection, - schema::{Collection, CollectionName, DefaultSerialization, InvalidNameError, Schematic}, + schema::{Collection, CollectionName, DefaultSerialization, Schematic}, test_util::TestDirectory, Error, }, @@ -16,7 +16,7 @@ use ubyte::ToByteUnit; use crate::collections::ResizableDocument; impl Collection for ResizableDocument { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "resizable-docs") } diff --git a/benchmarks/benches/commerce/bonsai.rs b/benchmarks/benches/commerce/bonsai.rs index 75beb2c53e3..735bf0a1b36 100644 --- a/benchmarks/benches/commerce/bonsai.rs +++ b/benchmarks/benches/commerce/bonsai.rs @@ -9,7 +9,7 @@ use bonsaidb::{ schema::{ view::map::Mappings, Collection, CollectionDocument, CollectionName, CollectionViewSchema, DefaultSerialization, DefaultViewSerialization, InsertError, - InvalidNameError, Name, NamedCollection, ReduceResult, Schema, SchemaName, Schematic, + Name, NamedCollection, ReduceResult, Schema, SchemaName, Schematic, SerializedCollection, View, ViewMapResult, ViewMappedValue, }, transaction::{self, Transaction}, @@ -389,7 +389,7 @@ impl Operator for BonsaiOperator { } impl Schema for Commerce { - fn schema_name() -> Result { + fn schema_name() -> SchemaName { SchemaName::new("benchmarks", "commerce") } @@ -405,7 +405,7 @@ impl Schema for Commerce { } impl Collection for Product { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "products") } @@ -438,7 +438,7 @@ impl View for ProductsByCategoryId { type Key = u32; type Value = u32; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-category") } } @@ -465,7 +465,7 @@ impl NamedCollection for Product { } impl Collection for ProductReview { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "reviews") } @@ -485,7 +485,7 @@ impl View for ProductReviewsByProduct { type Key = u32; type Value = ProductRatings; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-product") } } @@ -541,7 +541,7 @@ impl ProductRatings { } impl Collection for Category { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "categories") } @@ -553,7 +553,7 @@ impl Collection for Category { impl DefaultSerialization for Category {} impl Collection for Customer { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "customers") } @@ -565,7 +565,7 @@ impl Collection for Customer { impl DefaultSerialization for Customer {} impl Collection for Order { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "orders") } @@ -577,7 +577,7 @@ impl Collection for Order { impl DefaultSerialization for Order {} impl Collection for Cart { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("benchmarks", "carts") } diff --git a/book/book-examples/tests/view-example-enum.rs b/book/book-examples/tests/view-example-enum.rs index c3ab40d5a84..003ff2c7b89 100644 --- a/book/book-examples/tests/view-example-enum.rs +++ b/book/book-examples/tests/view-example-enum.rs @@ -4,8 +4,8 @@ use bonsaidb::{ document::Document, schema::{ view::{map::ViewMappedValue, EnumKey}, - Collection, CollectionName, DefaultSerialization, DefaultViewSerialization, - InvalidNameError, Name, ReduceResult, View, ViewMapResult, ViewSchema, + Collection, CollectionName, DefaultSerialization, DefaultViewSerialization, Name, + ReduceResult, View, ViewMapResult, ViewSchema, }, Error, }, @@ -38,7 +38,7 @@ pub struct BlogPost { // ANCHOR_END: struct impl Collection for BlogPost { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("view-example", "blog-post") } @@ -58,7 +58,7 @@ impl View for BlogPostsByCategory { type Key = Option; type Value = u32; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-category") } } diff --git a/book/book-examples/tests/view-example-string.rs b/book/book-examples/tests/view-example-string.rs index f1f2c2e41fd..fab923fc1f6 100644 --- a/book/book-examples/tests/view-example-string.rs +++ b/book/book-examples/tests/view-example-string.rs @@ -4,8 +4,7 @@ use bonsaidb::{ document::Document, schema::{ view::map::ViewMappedValue, Collection, CollectionName, DefaultSerialization, - DefaultViewSerialization, InvalidNameError, Name, ReduceResult, View, ViewMapResult, - ViewSchema, + DefaultViewSerialization, Name, ReduceResult, View, ViewMapResult, ViewSchema, }, Error, }, @@ -26,7 +25,7 @@ pub struct BlogPost { // ANCHOR_END: struct impl Collection for BlogPost { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("view-example", "blog-post") } @@ -46,7 +45,7 @@ impl View for BlogPostsByCategory { type Key = Option; type Value = u32; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-category") } } diff --git a/crates/bonsaidb-client/src/client/remote_database.rs b/crates/bonsaidb-client/src/client/remote_database.rs index c7bd701c5fb..4eebcccba8b 100644 --- a/crates/bonsaidb-client/src/client/remote_database.rs +++ b/crates/bonsaidb-client/src/client/remote_database.rs @@ -63,7 +63,7 @@ impl Connection for RemoteDatabase { .send_request(Request::Database { database: self.name.to_string(), request: DatabaseRequest::Get { - collection: C::collection_name()?, + collection: C::collection_name(), id, }, }) @@ -89,7 +89,7 @@ impl Connection for RemoteDatabase { .send_request(Request::Database { database: self.name.to_string(), request: DatabaseRequest::GetMultiple { - collection: C::collection_name()?, + collection: C::collection_name(), ids: ids.to_vec(), }, }) @@ -114,7 +114,7 @@ impl Connection for RemoteDatabase { .send_request(Request::Database { database: self.name.to_string(), request: DatabaseRequest::List { - collection: C::collection_name()?, + collection: C::collection_name(), ids: ids.into(), order, limit, @@ -149,7 +149,7 @@ impl Connection for RemoteDatabase { .schema .view::() .ok_or(bonsaidb_core::Error::CollectionNotFound)? - .view_name()?, + .view_name(), key: key.map(|key| key.serialized()).transpose()?, order, limit, @@ -190,7 +190,7 @@ impl Connection for RemoteDatabase { .schema .view::() .ok_or(bonsaidb_core::Error::CollectionNotFound)? - .view_name()?, + .view_name(), key: key.map(|key| key.serialized()).transpose()?, order, limit, @@ -229,7 +229,7 @@ impl Connection for RemoteDatabase { .schema .view::() .ok_or(bonsaidb_core::Error::CollectionNotFound)? - .view_name()?, + .view_name(), key: key.map(|key| key.serialized()).transpose()?, access_policy, grouped: false, @@ -265,7 +265,7 @@ impl Connection for RemoteDatabase { .schema .view::() .ok_or(bonsaidb_core::Error::CollectionNotFound)? - .view_name()?, + .view_name(), key: key.map(|key| key.serialized()).transpose()?, access_policy, grouped: true, @@ -310,7 +310,7 @@ impl Connection for RemoteDatabase { .schema .view::() .ok_or(bonsaidb_core::Error::CollectionNotFound)? - .view_name()?, + .view_name(), key: key.map(|key| key.serialized()).transpose()?, access_policy, }, @@ -391,7 +391,7 @@ impl Connection for RemoteDatabase { .send_request(Request::Database { database: self.name.to_string(), request: DatabaseRequest::CompactCollection { - name: C::collection_name()?, + name: C::collection_name(), }, }) .await? diff --git a/crates/bonsaidb-core/src/admin/database.rs b/crates/bonsaidb-core/src/admin/database.rs index a5ddf40dd33..b65224e2f18 100644 --- a/crates/bonsaidb-core/src/admin/database.rs +++ b/crates/bonsaidb-core/src/admin/database.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::{ define_basic_unique_mapped_view, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - NamedCollection, SchemaName, Schematic, + Collection, CollectionDocument, CollectionName, DefaultSerialization, NamedCollection, + SchemaName, Schematic, }, Error, }; @@ -19,7 +19,7 @@ pub struct Database { } impl Collection for Database { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("bonsaidb", "databases") } diff --git a/crates/bonsaidb-core/src/admin/group.rs b/crates/bonsaidb-core/src/admin/group.rs index 5d11ad63158..1a334b5c537 100644 --- a/crates/bonsaidb-core/src/admin/group.rs +++ b/crates/bonsaidb-core/src/admin/group.rs @@ -5,8 +5,8 @@ use crate::{ define_basic_unique_mapped_view, permissions::Statement, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - NamedCollection, Schematic, + Collection, CollectionDocument, CollectionName, DefaultSerialization, NamedCollection, + Schematic, }, Error, }; @@ -38,7 +38,7 @@ impl PermissionGroup { #[async_trait] impl Collection for PermissionGroup { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "permission-group") } diff --git a/crates/bonsaidb-core/src/admin/mod.rs b/crates/bonsaidb-core/src/admin/mod.rs index b0b5df8690e..f7805a4f1cf 100644 --- a/crates/bonsaidb-core/src/admin/mod.rs +++ b/crates/bonsaidb-core/src/admin/mod.rs @@ -1,5 +1,5 @@ use crate::{ - schema::{InvalidNameError, Schema, SchemaName, Schematic}, + schema::{Schema, SchemaName, Schematic}, Error, }; @@ -27,7 +27,7 @@ pub use self::{group::PermissionGroup, role::Role, user::User}; pub struct Admin; impl Schema for Admin { - fn schema_name() -> Result { + fn schema_name() -> SchemaName { SchemaName::new("khonsulabs", "bonsaidb-admin") } diff --git a/crates/bonsaidb-core/src/admin/password_config.rs b/crates/bonsaidb-core/src/admin/password_config.rs index 77064ce7ed2..59fbedc6c80 100644 --- a/crates/bonsaidb-core/src/admin/password_config.rs +++ b/crates/bonsaidb-core/src/admin/password_config.rs @@ -9,8 +9,7 @@ use crate::{ password_config, schema::{ view::{DefaultViewSerialization, ViewSchema}, - Collection, CollectionName, DefaultSerialization, InvalidNameError, Name, View, - ViewMapResult, + Collection, CollectionName, DefaultSerialization, Name, View, ViewMapResult, }, }; @@ -66,7 +65,7 @@ impl Deref for PasswordConfig { } impl Collection for PasswordConfig { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "password-config") } @@ -86,7 +85,7 @@ impl View for Singleton { type Key = (); type Value = (); - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("singleton") } } diff --git a/crates/bonsaidb-core/src/admin/role.rs b/crates/bonsaidb-core/src/admin/role.rs index faceaa8427e..df110fad79f 100644 --- a/crates/bonsaidb-core/src/admin/role.rs +++ b/crates/bonsaidb-core/src/admin/role.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::{ define_basic_unique_mapped_view, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - NamedCollection, Schematic, + Collection, CollectionDocument, CollectionName, DefaultSerialization, NamedCollection, + Schematic, }, Error, }; @@ -35,7 +35,7 @@ impl Role { } impl Collection for Role { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "role") } diff --git a/crates/bonsaidb-core/src/admin/user.rs b/crates/bonsaidb-core/src/admin/user.rs index 895955da226..a749e4d296c 100644 --- a/crates/bonsaidb-core/src/admin/user.rs +++ b/crates/bonsaidb-core/src/admin/user.rs @@ -9,8 +9,8 @@ use crate::{ document::KeyId, permissions::Permissions, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - NamedCollection, Schematic, + Collection, CollectionDocument, CollectionName, DefaultSerialization, NamedCollection, + Schematic, }, Error, ENCRYPTION_ENABLED, }; @@ -103,7 +103,7 @@ impl Collection for User { } } - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "user") } diff --git a/crates/bonsaidb-core/src/connection.rs b/crates/bonsaidb-core/src/connection.rs index d0e6ab7b7c6..553107e852b 100644 --- a/crates/bonsaidb-core/src/connection.rs +++ b/crates/bonsaidb-core/src/connection.rs @@ -42,7 +42,7 @@ pub trait Connection: Send + Sync { contents: Vec, ) -> Result { let results = self - .apply_transaction(Transaction::insert(C::collection_name()?, id, contents)) + .apply_transaction(Transaction::insert(C::collection_name(), id, contents)) .await?; if let OperationResult::DocumentUpdated { header, .. } = &results[0] { Ok(header.clone()) @@ -59,7 +59,7 @@ pub trait Connection: Send + Sync { async fn update(&self, doc: &mut Document) -> Result<(), Error> { let results = self .apply_transaction(Transaction::update( - C::collection_name()?, + C::collection_name(), doc.header.clone(), doc.contents.clone(), )) @@ -98,7 +98,7 @@ pub trait Connection: Send + Sync { async fn delete(&self, doc: &Document) -> Result<(), Error> { let results = self .apply_transaction(Transaction::delete( - C::collection_name()?, + C::collection_name(), doc.header.clone(), )) .await?; @@ -779,7 +779,7 @@ pub trait StorageConnection: Send + Sync { name: &str, only_if_needed: bool, ) -> Result<(), crate::Error> { - self.create_database_with_schema(name, DB::schema_name()?, only_if_needed) + self.create_database_with_schema(name, DB::schema_name(), only_if_needed) .await } diff --git a/crates/bonsaidb-core/src/schema/collection.rs b/crates/bonsaidb-core/src/schema/collection.rs index 0ee154107dc..bed37188650 100644 --- a/crates/bonsaidb-core/src/schema/collection.rs +++ b/crates/bonsaidb-core/src/schema/collection.rs @@ -6,7 +6,6 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use transmog::{Format, OwnedDeserializer}; use transmog_pot::Pot; -use super::names::InvalidNameError; use crate::{ connection::Connection, document::{Document, Header, KeyId}, @@ -18,7 +17,7 @@ use crate::{ #[async_trait] pub trait Collection: Debug + Send + Sync { /// The `Id` of this collection. - fn collection_name() -> Result; + fn collection_name() -> CollectionName; /// Defines all `View`s in this collection in `schema`. fn define_views(schema: &mut Schematic) -> Result<(), Error>; @@ -325,11 +324,9 @@ where if is_first_loop { is_first_loop = false; } else { - *self = C::get(self.header.id, connection).await?.ok_or_else(|| { - C::collection_name().map_or_else(Error::from, |collection| { - Error::DocumentNotFound(collection, self.header.id) - }) - })?; + *self = C::get(self.header.id, connection) + .await? + .ok_or_else(|| Error::DocumentNotFound(C::collection_name(), self.header.id))?; } modifier(&mut *self); match self.update(connection).await { diff --git a/crates/bonsaidb-core/src/schema/mod.rs b/crates/bonsaidb-core/src/schema/mod.rs index 932dedef072..29da58bc9ff 100644 --- a/crates/bonsaidb-core/src/schema/mod.rs +++ b/crates/bonsaidb-core/src/schema/mod.rs @@ -23,7 +23,7 @@ use crate::Error; /// Defines a group of collections that are stored into a single database. pub trait Schema: Send + Sync + Debug + 'static { /// Returns the unique [`SchemaName`] for this schema. - fn schema_name() -> Result; + fn schema_name() -> SchemaName; /// Defines the `Collection`s into `schema`. fn define_collections(schema: &mut Schematic) -> Result<(), Error>; @@ -38,7 +38,7 @@ pub trait Schema: Send + Sync + Debug + 'static { /// collections isn't required. For example, accessing only the key-value store /// or pubsub. impl Schema for () { - fn schema_name() -> Result { + fn schema_name() -> SchemaName { SchemaName::new("", "") } @@ -51,9 +51,9 @@ impl Schema for T where T: Collection + 'static, { - fn schema_name() -> Result { - let CollectionName { authority, name } = Self::collection_name()?; - Ok(SchemaName { authority, name }) + fn schema_name() -> SchemaName { + let CollectionName { authority, name } = Self::collection_name(); + SchemaName { authority, name } } fn define_collections(schema: &mut Schematic) -> Result<(), Error> { diff --git a/crates/bonsaidb-core/src/schema/names.rs b/crates/bonsaidb-core/src/schema/names.rs index 6bbef1194ab..dddb74a09c2 100644 --- a/crates/bonsaidb-core/src/schema/names.rs +++ b/crates/bonsaidb-core/src/schema/names.rs @@ -1,80 +1,148 @@ use std::{ borrow::Cow, - convert::{TryFrom, TryInto}, fmt::{Debug, Display, Write}, sync::Arc, }; use serde::{Deserialize, Serialize}; -/// A valid schema name. Must be alphanumeric (`a-zA-Z9-0`) or a hyphen (`-`). -/// Cloning this structure shares the underlying string data, regardless of -/// whether it's a static string literal or an owned String. +/// A schema name. Cloning is inexpensive. #[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)] #[serde(try_from = "String")] #[serde(into = "String")] -pub struct Name(Arc>); +pub struct Name { + name: Arc>, + needs_escaping: bool, +} -/// An invalid name was used in a schema definition. +/// A name was unable to e parsed. #[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone)] #[error("invalid name: {0}")] pub struct InvalidNameError(pub String); impl Name { - /// Creates a new name after validating it. + /// Creates a new name. + pub fn new>(contents: T) -> Self { + contents.into() + } + + /// Parses a name that was previously encoded via [`Self::encoded()`]. /// /// # Errors - /// Returns [`InvalidNameError`] if the value passed contains any characters - /// other than `a-zA-Z9-0` or a hyphen (`-`). - pub fn new>( - contents: T, - ) -> Result { - contents.try_into() + /// + /// Returns [`InvalidNameError`] if the name contains invalid escape + /// sequences. + pub fn parse_encoded(encoded: &str) -> Result { + let mut bytes = encoded.bytes(); + let mut decoded = Vec::with_capacity(encoded.len()); + while let Some(byte) = bytes.next() { + if byte == b'_' { + if let (Some(high), Some(low)) = (bytes.next(), bytes.next()) { + if let Some(byte) = hex_chars_to_byte(high, low) { + decoded.push(byte); + continue; + } + } + return Err(InvalidNameError(encoded.to_string())); + } + + decoded.push(byte); + } + + String::from_utf8(decoded) + .map(Self::from) + .map_err(|_| InvalidNameError(encoded.to_string())) } - fn validate_name(name: &str) -> Result<(), InvalidNameError> { - if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { - Ok(()) - } else { - Err(InvalidNameError(name.to_string())) - } + /// Returns an encoded version of this name that contains only alphanumeric + /// ASCII, underscore, and hyphen. + #[must_use] + pub fn encoded(&self) -> String { + format!("{:#}", self) } } -impl TryFrom<&'static str> for Name { - type Error = InvalidNameError; - - fn try_from(value: &'static str) -> Result { - Self::validate_name(value)?; - Ok(Self(Arc::new(Cow::Borrowed(value)))) +impl From> for Name { + fn from(value: Cow<'static, str>) -> Self { + let needs_escaping = !value + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-'); + Self { + name: Arc::new(value), + needs_escaping, + } } } -impl TryFrom for Name { - type Error = InvalidNameError; +impl From<&'static str> for Name { + fn from(value: &'static str) -> Self { + Self::from(Cow::Borrowed(value)) + } +} - fn try_from(value: String) -> Result { - Self::validate_name(&value)?; - Ok(Self(Arc::new(Cow::Owned(value)))) +impl From for Name { + fn from(value: String) -> Self { + Self::from(Cow::Owned(value)) } } #[allow(clippy::from_over_into)] // the auto into impl doesn't work with serde(into) impl Into for Name { fn into(self) -> String { - self.0.to_string() + self.name.to_string() } } impl Display for Name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.0, f) + if f.alternate() && self.needs_escaping { + for byte in self.name.bytes() { + if byte.is_ascii_alphanumeric() || byte == b'-' { + f.write_char(byte as char)?; + } else { + // Encode the byte as _FF + f.write_char('_')?; + f.write_char(nibble_to_hex_char(byte >> 4))?; + f.write_char(nibble_to_hex_char(byte & 0xF))?; + } + } + Ok(()) + } else { + Display::fmt(&self.name, f) + } + } +} + +const fn nibble_to_hex_char(nibble: u8) -> char { + let ch = match nibble { + 0..=9 => b'0' + nibble, + _ => b'a' + nibble - 10, + }; + ch as char +} + +const fn hex_chars_to_byte(high_nibble: u8, low_nibble: u8) -> Option { + match ( + hex_char_to_nibble(high_nibble), + hex_char_to_nibble(low_nibble), + ) { + (Some(high_nibble), Some(low_nibble)) => Some(high_nibble << 4 | low_nibble), + _ => None, } } +const fn hex_char_to_nibble(nibble: u8) -> Option { + let ch = match nibble { + b'0'..=b'9' => nibble - b'0', + b'a'..=b'f' => nibble - b'a' + 10, + _ => return None, + }; + Some(ch) +} + impl AsRef for Name { fn as_ref(&self) -> &str { - self.0.as_ref() + self.name.as_ref() } } @@ -86,19 +154,27 @@ impl AsRef for Name { #[serde(transparent)] pub struct Authority(Name); -impl TryFrom<&'static str> for Authority { - type Error = InvalidNameError; +impl From> for Authority { + fn from(value: Cow<'static, str>) -> Self { + Self::from(Name::from(value)) + } +} - fn try_from(value: &'static str) -> Result { - Ok(Self(Name::new(value)?)) +impl From<&'static str> for Authority { + fn from(value: &'static str) -> Self { + Self::from(Cow::Borrowed(value)) } } -impl TryFrom for Authority { - type Error = InvalidNameError; +impl From for Authority { + fn from(value: String) -> Self { + Self::from(Cow::Owned(value)) + } +} - fn try_from(value: String) -> Result { - Ok(Self(Name::new(value)?)) +impl From for Authority { + fn from(value: Name) -> Self { + Self(value) } } @@ -120,16 +196,36 @@ pub struct SchemaName { impl SchemaName { /// Creates a new schema name. - pub fn new< - A: TryInto, - N: TryInto, - >( - authority: A, - name: N, - ) -> Result { - let authority = authority.try_into()?; - let name = name.try_into()?; - Ok(Self { authority, name }) + pub fn new, N: Into>(authority: A, name: N) -> Self { + let authority = authority.into(); + let name = name.into(); + Self { authority, name } + } + + /// Parses a schema name that was previously encoded via + /// [`Self::encoded()`]. + /// + /// # Errors + /// + /// Returns [`InvalidNameError`] if the name contains invalid escape + /// sequences or contains more than two periods. + pub fn parse_encoded(schema_name: &str) -> Result { + let mut parts = schema_name.split('.'); + if let (Some(authority), Some(name), None) = (parts.next(), parts.next(), parts.next()) { + let authority = Name::parse_encoded(authority)?; + let name = Name::parse_encoded(name)?; + + Ok(Self::new(authority, name)) + } else { + Err(InvalidNameError(schema_name.to_string())) + } + } + + /// Encodes this schema name such that the authority and name can be + /// safely parsed using [`Self::parse_encoded`]. + #[must_use] + pub fn encoded(&self) -> String { + format!("{:#}", self) } } @@ -141,23 +237,6 @@ impl Display for SchemaName { } } -impl TryFrom<&str> for SchemaName { - type Error = InvalidNameError; - - fn try_from(schema_name: &str) -> Result { - let parts = schema_name.split('.').collect::>(); - if parts.len() == 2 { - let mut parts = parts.into_iter(); - let authority = parts.next().unwrap(); - let name = parts.next().unwrap(); - - Self::new(authority.to_string(), name.to_string()) - } else { - Err(InvalidNameError(schema_name.to_string())) - } - } -} - /// The name of a [`Collection`](super::Collection). #[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] pub struct CollectionName { @@ -170,16 +249,36 @@ pub struct CollectionName { impl CollectionName { /// Creates a new collection name. - pub fn new< - A: TryInto, - N: TryInto, - >( - authority: A, - name: N, - ) -> Result { - let authority = authority.try_into()?; - let name = name.try_into()?; - Ok(Self { authority, name }) + pub fn new, N: Into>(authority: A, name: N) -> Self { + let authority = authority.into(); + let name = name.into(); + Self { authority, name } + } + + /// Parses a colleciton name that was previously encoded via + /// [`Self::encoded()`]. + /// + /// # Errors + /// + /// Returns [`InvalidNameError`] if the name contains invalid escape + /// sequences or contains more than two periods. + pub fn parse_encoded(collection_name: &str) -> Result { + let mut parts = collection_name.split('.'); + if let (Some(authority), Some(name), None) = (parts.next(), parts.next(), parts.next()) { + let authority = Name::parse_encoded(authority)?; + let name = Name::parse_encoded(name)?; + + Ok(Self::new(authority, name)) + } else { + Err(InvalidNameError(collection_name.to_string())) + } + } + + /// Encodes this collection name such that the authority and name can be + /// safely parsed using [`Self::parse_encoded`]. + #[must_use] + pub fn encoded(&self) -> String { + format!("{:#}", self) } } @@ -191,23 +290,6 @@ impl Display for CollectionName { } } -impl TryFrom<&str> for CollectionName { - type Error = InvalidNameError; - - fn try_from(collection_name: &str) -> Result { - let parts = collection_name.split('.').collect::>(); - if parts.len() == 2 { - let mut parts = parts.into_iter(); - let authority = parts.next().unwrap(); - let name = parts.next().unwrap(); - - Self::new(authority.to_string(), name.to_string()) - } else { - Err(InvalidNameError(collection_name.to_string())) - } - } -} - /// The name of a [`View`](super::View). #[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] pub struct ViewName { @@ -241,7 +323,32 @@ impl Display for ViewName { } #[test] -fn name_validation_tests() { - assert!(Name::new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-").is_ok()); - assert!(matches!(Name::new("."), Err(InvalidNameError(_)))); +fn name_escaping_tests() { + const VALID_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"; + const INVALID_CHARS: &str = "._hello\u{1F680}"; + const ESCAPED_INVALID: &str = "_2e_5fhello_f0_9f_9a_80"; + assert_eq!(Name::new(VALID_CHARS).to_string(), VALID_CHARS); + assert_eq!(Name::new(INVALID_CHARS).to_string(), INVALID_CHARS); + assert_eq!(Name::new(INVALID_CHARS).encoded(), ESCAPED_INVALID); + assert_eq!( + Name::parse_encoded(ESCAPED_INVALID).unwrap(), + Name::new(INVALID_CHARS) + ); + Name::parse_encoded("_").unwrap_err(); + Name::parse_encoded("_0").unwrap_err(); + Name::parse_encoded("_z").unwrap_err(); + Name::parse_encoded("_0z").unwrap_err(); +} + +#[test] +fn joined_names_tests() { + const INVALID_CHARS: &str = "._hello\u{1F680}.._world\u{1F680}"; + const ESCAPED_INVALID: &str = "_2e_5fhello_f0_9f_9a_80._2e_5fworld_f0_9f_9a_80"; + let collection = CollectionName::parse_encoded(ESCAPED_INVALID).unwrap(); + assert_eq!(collection.to_string(), INVALID_CHARS); + assert_eq!(collection.encoded(), ESCAPED_INVALID); + + let schema_name = SchemaName::parse_encoded(ESCAPED_INVALID).unwrap(); + assert_eq!(schema_name.to_string(), INVALID_CHARS); + assert_eq!(schema_name.encoded(), ESCAPED_INVALID); } diff --git a/crates/bonsaidb-core/src/schema/schematic.rs b/crates/bonsaidb-core/src/schema/schematic.rs index 40d03f97c14..c4b1f7d51b3 100644 --- a/crates/bonsaidb-core/src/schema/schematic.rs +++ b/crates/bonsaidb-core/src/schema/schematic.rs @@ -13,7 +13,7 @@ use crate::{ map::{self, MappedValue}, Key, Serialized, SerializedView, ViewSchema, }, - CollectionName, InvalidNameError, Schema, SchemaName, View, ViewName, + CollectionName, Schema, SchemaName, View, ViewName, }, Error, }; @@ -36,7 +36,7 @@ impl Schematic { /// Returns an initialized version from `S`. pub fn from_schema() -> Result { let mut schematic = Self { - name: S::schema_name()?, + name: S::schema_name(), contained_collections: HashSet::new(), collections_by_type_id: HashMap::new(), collection_encryption_keys: HashMap::new(), @@ -51,7 +51,7 @@ impl Schematic { /// Adds the collection `C` and its views. pub fn define_collection(&mut self) -> Result<(), Error> { - let name = C::collection_name()?; + let name = C::collection_name(); if self.contained_collections.contains(&name) { Err(Error::CollectionAlreadyDefined) } else { @@ -83,8 +83,8 @@ impl Schematic { schema: S, ) -> Result<(), Error> { let instance = ViewInstance { view, schema }; - let name = instance.view_name()?; - let collection = instance.collection()?; + let name = instance.view_name(); + let collection = instance.collection(); let unique = instance.unique(); self.views.insert(TypeId::of::(), Box::new(instance)); // TODO check for name collision @@ -192,7 +192,7 @@ where S: ViewSchema, ::Key: 'static, { - fn collection(&self) -> Result { + fn collection(&self) -> CollectionName { <::Collection as Collection>::collection_name() } @@ -204,7 +204,7 @@ where self.schema.version() } - fn view_name(&self) -> Result { + fn view_name(&self) -> ViewName { self.view.view_name() } @@ -244,12 +244,12 @@ fn schema_tests() -> anyhow::Result<()> { assert_eq!(schema.collections_by_type_id.len(), 1); assert_eq!( schema.collections_by_type_id[&TypeId::of::()], - Basic::collection_name()? + Basic::collection_name() ); assert_eq!(schema.views.len(), 4); assert_eq!( - schema.views[&TypeId::of::()].view_name()?, - View::view_name(&BasicCount)? + schema.views[&TypeId::of::()].view_name(), + View::view_name(&BasicCount) ); Ok(()) diff --git a/crates/bonsaidb-core/src/schema/view.rs b/crates/bonsaidb-core/src/schema/view.rs index cc492100681..c9574295441 100644 --- a/crates/bonsaidb-core/src/schema/view.rs +++ b/crates/bonsaidb-core/src/schema/view.rs @@ -8,8 +8,7 @@ use crate::{ document::Document, schema::{ view::map::{Mappings, ViewMappedValue}, - Collection, CollectionDocument, CollectionName, InvalidNameError, Name, - SerializedCollection, ViewName, + Collection, CollectionDocument, CollectionName, Name, SerializedCollection, ViewName, }, AnyError, }; @@ -67,14 +66,14 @@ pub trait View: Send + Sync + Debug + 'static { type Value: Send + Sync; /// The name of the view. Must be unique per collection. - fn name(&self) -> Result; + fn name(&self) -> Name; /// The namespaced name of the view. - fn view_name(&self) -> Result { - Ok(ViewName { - collection: Self::Collection::collection_name()?, - name: self.name()?, - }) + fn view_name(&self) -> ViewName { + ViewName { + collection: Self::Collection::collection_name(), + name: self.name(), + } } } @@ -247,13 +246,13 @@ where /// Wraps a [`View`] with serialization to erase the associated types pub trait Serialized: Send + Sync + Debug { /// Wraps returing [`::collection_name()`](crate::schema::Collection::collection_name) - fn collection(&self) -> Result; + fn collection(&self) -> CollectionName; /// Wraps [`ViewSchema::unique`] fn unique(&self) -> bool; /// Wraps [`ViewSchema::version`] fn version(&self) -> u64; /// Wraps [`View::view_name`] - fn view_name(&self) -> Result; + fn view_name(&self) -> ViewName; /// Wraps [`ViewSchema::map`] fn map(&self, document: &Document) -> Result, Error>; /// Wraps [`ViewSchema::reduce`] @@ -332,7 +331,7 @@ macro_rules! define_mapped_view { type Key = $key; type Value = $value; - fn name(&self) -> Result<$crate::schema::Name, $crate::schema::InvalidNameError> { + fn name(&self) -> $crate::schema::Name { $crate::schema::Name::new($name) } } diff --git a/crates/bonsaidb-core/src/test_util.rs b/crates/bonsaidb-core/src/test_util.rs index 89a2b43cfba..ea7487a7feb 100644 --- a/crates/bonsaidb-core/src/test_util.rs +++ b/crates/bonsaidb-core/src/test_util.rs @@ -24,9 +24,8 @@ use crate::{ map::{Mappings, ViewMappedValue}, DefaultViewSerialization, ReduceResult, ViewSchema, }, - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - MappedValue, Name, NamedCollection, Schema, SchemaName, Schematic, SerializedCollection, - View, ViewMapResult, + Collection, CollectionDocument, CollectionName, DefaultSerialization, MappedValue, Name, + NamedCollection, Schema, SchemaName, Schematic, SerializedCollection, View, ViewMapResult, }, Error, ENCRYPTION_ENABLED, }; @@ -67,8 +66,10 @@ impl Basic { } impl Collection for Basic { - fn collection_name() -> Result { - CollectionName::new("khonsulabs", "basic") + fn collection_name() -> CollectionName { + // This collection purposely uses names with characters that need + // escaping, since it's used in backup/restore. + CollectionName::new("khonsulabs_", "_basic") } fn define_views(schema: &mut Schematic) -> Result<(), Error> { @@ -89,7 +90,7 @@ impl View for BasicCount { type Key = (); type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("count") } } @@ -120,7 +121,7 @@ impl View for BasicByParentId { type Key = Option; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-parent-id") } } @@ -155,7 +156,7 @@ impl View for BasicByCategory { type Key = String; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-category") } } @@ -191,7 +192,7 @@ impl View for BasicByTag { type Key = String; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-tag") } } @@ -228,7 +229,7 @@ impl View for BasicByBrokenParentId { type Key = (); type Value = (); - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-parent-id") } } @@ -280,7 +281,7 @@ impl Collection for EncryptedBasic { } } - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "encrypted-basic") } @@ -301,7 +302,7 @@ impl View for EncryptedBasicCount { type Key = (); type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("count") } } @@ -332,7 +333,7 @@ impl View for EncryptedBasicByParentId { type Key = Option; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-parent-id") } } @@ -364,7 +365,7 @@ impl View for EncryptedBasicByCategory { type Key = String; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-category") } } @@ -396,7 +397,7 @@ impl DefaultViewSerialization for EncryptedBasicByCategory {} pub struct BasicSchema; impl Schema for BasicSchema { - fn schema_name() -> Result { + fn schema_name() -> SchemaName { SchemaName::new("khonsulabs", "basic") } @@ -421,7 +422,7 @@ impl Unique { } impl Collection for Unique { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "unique") } @@ -440,7 +441,7 @@ impl View for UniqueValue { type Key = String; type Value = (); - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("unique-value") } } @@ -504,7 +505,7 @@ impl Deref for TestDirectory { pub struct BasicCollectionWithNoViews; impl Collection for BasicCollectionWithNoViews { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { Basic::collection_name() } @@ -526,7 +527,7 @@ impl SerializedCollection for BasicCollectionWithNoViews { pub struct BasicCollectionWithOnlyBrokenParentId; impl Collection for BasicCollectionWithOnlyBrokenParentId { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { Basic::collection_name() } @@ -539,7 +540,7 @@ impl Collection for BasicCollectionWithOnlyBrokenParentId { pub struct UnassociatedCollection; impl Collection for UnassociatedCollection { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "unassociated") } @@ -839,7 +840,7 @@ pub async fn store_retrieve_update_delete_tests(db: &C) -> anyhow .documents() .expect("incorrect transaction type"); assert_eq!(changed_documents.len(), 1); - assert_eq!(changed_documents[0].collection, Basic::collection_name()?); + assert_eq!(changed_documents[0].collection, Basic::collection_name()); assert_eq!(changed_documents[0].id, header.id); assert!(!changed_documents[0].deleted); } @@ -856,7 +857,7 @@ pub async fn store_retrieve_update_delete_tests(db: &C) -> anyhow .documents() .expect("incorrect transaction type"); assert_eq!(changed_documents.len(), 1); - assert_eq!(changed_documents[0].collection, Basic::collection_name()?); + assert_eq!(changed_documents[0].collection, Basic::collection_name()); assert_eq!(changed_documents[0].id, header.id); assert!(changed_documents[0].deleted); @@ -914,7 +915,7 @@ pub async fn conflict_tests(db: &C) -> anyhow::Result<()> { .expect_err("conflict should have generated an error") { Error::DocumentConflict(collection, id) => { - assert_eq!(collection, Basic::collection_name()?); + assert_eq!(collection, Basic::collection_name()); assert_eq!(id, doc.header.id); } other => return Err(anyhow::Error::from(other)), @@ -937,7 +938,7 @@ pub async fn bad_update_tests(db: &C) -> anyhow::Result<()> { let mut doc = Document::with_contents(1, &Basic::default())?; match db.update::(&mut doc).await { Err(Error::DocumentNotFound(collection, id)) => { - assert_eq!(collection, Basic::collection_name()?); + assert_eq!(collection, Basic::collection_name()); assert_eq!(id, 1); Ok(()) } @@ -1447,7 +1448,7 @@ pub async fn unique_view_tests(db: &C) -> anyhow::Result<()> { conflicting_document, }) = db.collection::().push(&Unique::new("1")).await { - assert_eq!(view, UniqueValue.view_name()?); + assert_eq!(view, UniqueValue.view_name()); assert_eq!(existing_document.id, first_doc.id); // We can't predict the conflicting document id since it's generated // inside of the transaction, but we can assert that it's different than @@ -1468,7 +1469,7 @@ pub async fn unique_view_tests(db: &C) -> anyhow::Result<()> { conflicting_document, }) = db.update::(&mut second_doc).await { - assert_eq!(view, UniqueValue.view_name()?); + assert_eq!(view, UniqueValue.view_name()); assert_eq!(existing_document.id, first_doc.id); assert_eq!(conflicting_document.id, second_doc.header.id); } else { @@ -2106,13 +2107,15 @@ pub async fn basic_server_connection_tests( ) -> anyhow::Result<()> { let mut schemas = server.list_available_schemas().await?; schemas.sort(); - assert!(schemas.contains(&Basic::schema_name()?)); - assert!(schemas.contains(&SchemaName::new("khonsulabs", "bonsaidb-admin")?)); + assert!(schemas.contains(&BasicSchema::schema_name())); + assert!(schemas.contains(&SchemaName::new("khonsulabs", "bonsaidb-admin"))); let databases = server.list_databases().await?; assert!(databases.iter().any(|db| db.name == "tests")); - server.create_database::(newdb_name, false).await?; + server + .create_database::(newdb_name, false) + .await?; server.delete_database(newdb_name).await?; assert!(matches!( @@ -2121,17 +2124,19 @@ pub async fn basic_server_connection_tests( )); assert!(matches!( - server.create_database::("tests", false).await, + server.create_database::("tests", false).await, Err(Error::DatabaseNameAlreadyTaken(_)) )); assert!(matches!( - server.create_database::("tests", true).await, + server.create_database::("tests", true).await, Ok(_) )); assert!(matches!( - server.create_database::("|invalidname", false).await, + server + .create_database::("|invalidname", false) + .await, Err(Error::InvalidDatabaseName(_)) )); diff --git a/crates/bonsaidb-core/src/transaction.rs b/crates/bonsaidb-core/src/transaction.rs index 0fd9a910fbd..ad3c79a4111 100644 --- a/crates/bonsaidb-core/src/transaction.rs +++ b/crates/bonsaidb-core/src/transaction.rs @@ -92,7 +92,7 @@ impl Operation { contents: &C::Contents, ) -> Result { let contents = C::serialize(contents)?; - Ok(Self::insert(C::collection_name()?, id, contents)) + Ok(Self::insert(C::collection_name(), id, contents)) } /// Updates a document in `collection`. @@ -110,7 +110,7 @@ impl Operation { contents: &C::Contents, ) -> Result { let contents = C::serialize(contents)?; - Ok(Self::update(C::collection_name()?, header, contents)) + Ok(Self::update(C::collection_name(), header, contents)) } /// Deletes a document from a `collection`. diff --git a/crates/bonsaidb-local/src/config.rs b/crates/bonsaidb-local/src/config.rs index 95a540479ea..96bcb409337 100644 --- a/crates/bonsaidb-local/src/config.rs +++ b/crates/bonsaidb-local/src/config.rs @@ -62,10 +62,8 @@ pub struct StorageConfiguration { impl StorageConfiguration { /// Registers the schema provided. pub fn register_schema(&mut self) -> Result<(), Error> { - self.initial_schemas.insert( - S::schema_name()?, - Box::new(StorageSchemaOpener::::new()?), - ); + self.initial_schemas + .insert(S::schema_name(), Box::new(StorageSchemaOpener::::new()?)); Ok(()) } } diff --git a/crates/bonsaidb-local/src/database.rs b/crates/bonsaidb-local/src/database.rs index 770c1995e67..3c11507b0db 100644 --- a/crates/bonsaidb-local/src/database.rs +++ b/crates/bonsaidb-local/src/database.rs @@ -183,8 +183,8 @@ impl Database { let view_entries = self .roots() .tree(self.collection_tree( - &view.collection()?, - view_entries_tree_name(&view.view_name()?), + &view.collection(), + view_entries_tree_name(&view.view_name()), )?) .map_err(Error::from)?; @@ -196,7 +196,7 @@ impl Database { if matches!(access_policy, AccessPolicy::UpdateAfter) { let db = self.clone(); - let view_name = view.view_name()?; + let view_name = view.view_name(); tokio::task::spawn(async move { let view = db .data @@ -336,7 +336,7 @@ impl Database { let mut documents = self .get_multiple_from_collection_id( &results.iter().map(|m| m.source.id).collect::>(), - &view.collection()?, + &view.collection(), ) .await? .into_iter() @@ -396,7 +396,7 @@ impl Database { .schema .view_by_name(view) .ok_or(bonsaidb_core::Error::CollectionNotFound)?; - let collection = view.collection()?; + let collection = view.collection(); let mut transaction = Transaction::default(); self.for_each_in_view(view, key, Sort::Ascending, None, access_policy, |entry| { let entry = ViewEntry::from(entry); @@ -655,7 +655,7 @@ impl Database { let changed_documents = changed_documents.collect::>(); for view in views { if !view.unique() { - let view_name = view.view_name().map_err(bonsaidb_core::Error::from)?; + let view_name = view.view_name(); for changed_document in &changed_documents { let invalidated_docs = roots_transaction .tree::( @@ -862,7 +862,7 @@ impl Database { .unique_views_in_collection(&operation.collection) { for view in unique_views { - let name = view.view_name().map_err(bonsaidb_core::Error::from)?; + let name = view.view_name(); mapper::DocumentRequest { database: self, document_id, @@ -1086,8 +1086,7 @@ impl Connection for Database { &self, id: u64, ) -> Result, bonsaidb_core::Error> { - self.get_from_collection_id(id, &C::collection_name()?) - .await + self.get_from_collection_id(id, &C::collection_name()).await } #[cfg_attr(feature = "tracing", tracing::instrument(skip(ids)))] @@ -1095,7 +1094,7 @@ impl Connection for Database { &self, ids: &[u64], ) -> Result, bonsaidb_core::Error> { - self.get_multiple_from_collection_id(ids, &C::collection_name()?) + self.get_multiple_from_collection_id(ids, &C::collection_name()) .await } @@ -1106,7 +1105,7 @@ impl Connection for Database { order: Sort, limit: Option, ) -> Result, bonsaidb_core::Error> { - self.list(ids.into(), order, limit, &C::collection_name()?) + self.list(ids.into(), order, limit, &C::collection_name()) .await } @@ -1260,7 +1259,7 @@ impl Connection for Database { let result = self .reduce_in_view( - &view.view_name()?, + &view.view_name(), key.map(|key| key.serialized()).transpose()?, access_policy, ) @@ -1287,7 +1286,7 @@ impl Connection for Database { let results = self .grouped_reduce_in_view( - &view.view_name()?, + &view.view_name(), key.map(|key| key.serialized()).transpose()?, access_policy, ) @@ -1312,7 +1311,7 @@ impl Connection for Database { where Self: Sized, { - let collection = ::collection_name()?; + let collection = ::collection_name(); let mut transaction = Transaction::default(); self.for_each_view_entry::( key, @@ -1345,7 +1344,7 @@ impl Connection for Database { async fn compact_collection(&self) -> Result<(), bonsaidb_core::Error> { self.storage() .tasks() - .compact_collection(self.clone(), C::collection_name()?) + .compact_collection(self.clone(), C::collection_name()) .await?; Ok(()) } diff --git a/crates/bonsaidb-local/src/open_trees.rs b/crates/bonsaidb-local/src/open_trees.rs index c4d154952d3..5fafb10656c 100644 --- a/crates/bonsaidb-local/src/open_trees.rs +++ b/crates/bonsaidb-local/src/open_trees.rs @@ -76,7 +76,7 @@ impl OpenTrees { if let Some(views) = schema.views_in_collection(collection) { for view in views { - let view_name = view.view_name()?; + let view_name = view.view_name(); if view.unique() { self.open_tree::( &view_omitted_docs_tree_name(&view_name), diff --git a/crates/bonsaidb-local/src/storage.rs b/crates/bonsaidb-local/src/storage.rs index 2f5860abb8d..1bd0b6d1a26 100644 --- a/crates/bonsaidb-local/src/storage.rs +++ b/crates/bonsaidb-local/src/storage.rs @@ -271,7 +271,7 @@ impl Storage { let mut schemas = fast_async_write!(self.data.schemas); if schemas .insert( - DB::schema_name()?, + DB::schema_name(), Box::new(StorageSchemaOpener::::new()?), ) .is_none() @@ -279,7 +279,7 @@ impl Storage { Ok(()) } else { Err(Error::Core(bonsaidb_core::Error::SchemaAlreadyRegistered( - DB::schema_name()?, + DB::schema_name(), ))) } } @@ -537,12 +537,12 @@ impl StorageConnection for Storage { name: &str, ) -> Result { let db = self.database_without_schema(name).await?; - if db.data.schema.name == DB::schema_name()? { + if db.data.schema.name == DB::schema_name() { Ok(db) } else { Err(bonsaidb_core::Error::SchemaMismatch { database_name: name.to_owned(), - schema: DB::schema_name()?, + schema: DB::schema_name(), stored_schema: db.data.schema.name.clone(), }) } diff --git a/crates/bonsaidb-local/src/storage/backup.rs b/crates/bonsaidb-local/src/storage/backup.rs index 3b5437f2a6e..a471bccac25 100644 --- a/crates/bonsaidb-local/src/storage/backup.rs +++ b/crates/bonsaidb-local/src/storage/backup.rs @@ -111,13 +111,14 @@ impl Storage { let documents = database .list(Range::from(..), Sort::Ascending, None, &collection) .await?; + let collection_name = collection.encoded(); // TODO consider how to best parallelize -- perhaps a location can opt into parallelization? for document in documents { location .store( &schema, database.name(), - &collection.to_string(), + &collection_name, &document.id.to_string(), &document.contents, ) @@ -149,14 +150,14 @@ impl Storage { // Restore all the collections. However, there's one collection we don't // want to restore: the Databases list. This will be recreated during // the process of restoring the backup, so we skip it. - let database_collection = admin::Database::collection_name()?; + let database_collection = admin::Database::collection_name(); for collection in database .schematic() .collections() .into_iter() .filter(|c| c != &database_collection) { - let collection_name = collection.to_string(); + let collection_name = collection.encoded(); for (id, id_string) in location .list_stored(&schema, database.name(), &collection_name) .await? @@ -301,7 +302,7 @@ impl<'a> BackupLocation for &'a Path { async fn list_schemas(&self) -> Result, Self::Error> { iterate_directory(self, |entry, file_name| async move { if entry.file_type().await?.is_dir() { - if let Ok(schema_name) = SchemaName::try_from(file_name.as_str()) { + if let Ok(schema_name) = SchemaName::parse_encoded(file_name.as_str()) { return Ok(Some(schema_name)); } } @@ -442,7 +443,7 @@ impl IoResultExt for Result { } fn schema_folder(base: &Path, schema: &SchemaName) -> PathBuf { - base.join(schema.to_string()) + base.join(schema.encoded()) } fn database_folder(base: &Path, schema: &SchemaName, database_name: &str) -> PathBuf { diff --git a/crates/bonsaidb-local/src/tasks.rs b/crates/bonsaidb-local/src/tasks.rs index 8f2959bb053..9e68a50cb4e 100644 --- a/crates/bonsaidb-local/src/tasks.rs +++ b/crates/bonsaidb-local/src/tasks.rs @@ -70,8 +70,8 @@ impl TaskManager { let statuses = fast_async_read!(self.statuses); if let Some(last_transaction_indexed) = statuses.view_update_last_status.get(&( database.data.name.clone(), - view.collection()?, - view.view_name()?, + view.collection(), + view.view_name(), )) { last_transaction_indexed < ¤t_transaction_id } else { @@ -88,8 +88,8 @@ impl TaskManager { database: database.clone(), map: Map { database: database.data.name.clone(), - collection: view.collection()?, - view_name: view_name.clone()?, + collection: view.collection(), + view_name: view_name.clone(), }, }) .await; @@ -126,11 +126,11 @@ impl TaskManager { view: &dyn view::Serialized, database: &Database, ) -> Result>, crate::Error> { - let view_name = view.view_name()?; + let view_name = view.view_name(); if !self .view_integrity_checked( database.data.name.clone(), - view.collection()?, + view.collection(), view_name.clone(), ) .await @@ -142,7 +142,7 @@ impl TaskManager { scan: IntegrityScan { database: database.data.name.clone(), view_version: view.version(), - collection: view.collection()?, + collection: view.collection(), view_name, }, }) diff --git a/crates/bonsaidb-local/src/tasks/compactor.rs b/crates/bonsaidb-local/src/tasks/compactor.rs index 15ec120802a..83156ba87b3 100644 --- a/crates/bonsaidb-local/src/tasks/compactor.rs +++ b/crates/bonsaidb-local/src/tasks/compactor.rs @@ -131,7 +131,7 @@ fn compact_collection(database: &Database, collection: &CollectionName) -> Resul // Compact the views if let Some(views) = database.data.schema.views_in_collection(collection) { for view in views { - compact_view(database, &view.view_name()?)?; + compact_view(database, &view.view_name())?; } } compact_tree::(database, view_versions_tree_name(collection))?; diff --git a/crates/bonsaidb-local/src/views.rs b/crates/bonsaidb-local/src/views.rs index afe44da18b3..35c87792c5b 100644 --- a/crates/bonsaidb-local/src/views.rs +++ b/crates/bonsaidb-local/src/views.rs @@ -24,22 +24,22 @@ pub mod integrity_scanner; pub mod mapper; pub fn view_entries_tree_name(view_name: &impl Display) -> String { - format!("view.{}", view_name) + format!("view.{:#}", view_name) } /// Used to store Document ID -> Key mappings, so that when a document is updated, we can remove the old entry. pub fn view_document_map_tree_name(view_name: &impl Display) -> String { - format!("view.{}.document-map", view_name) + format!("view.{:#}.document-map", view_name) } pub fn view_invalidated_docs_tree_name(view_name: &impl Display) -> String { - format!("view.{}.invalidated", view_name) + format!("view.{:#}.invalidated", view_name) } pub fn view_omitted_docs_tree_name(view_name: &impl Display) -> String { - format!("view.{}.omitted", view_name) + format!("view.{:#}.omitted", view_name) } pub fn view_versions_tree_name(collection: &CollectionName) -> String { - format!("view-versions.{}", collection) + format!("view-versions.{:#}", collection) } diff --git a/crates/bonsaidb-server/src/hosted.rs b/crates/bonsaidb-server/src/hosted.rs index 110eca758e0..ae4b5484a5c 100644 --- a/crates/bonsaidb-server/src/hosted.rs +++ b/crates/bonsaidb-server/src/hosted.rs @@ -2,8 +2,8 @@ use bonsaidb_core::{ define_basic_mapped_view, define_basic_unique_mapped_view, document::KeyId, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - NamedCollection, Schema, SchemaName, Schematic, + Collection, CollectionDocument, CollectionName, DefaultSerialization, NamedCollection, + Schema, SchemaName, Schematic, }, ENCRYPTION_ENABLED, }; @@ -14,7 +14,7 @@ use serde::{de::Visitor, Deserialize, Serialize}; pub struct Hosted; impl Schema for Hosted { - fn schema_name() -> Result { + fn schema_name() -> SchemaName { SchemaName::new("khonsulabs", "hosted") } @@ -46,7 +46,7 @@ impl Collection for TlsCertificate { } } - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "tls-certificates") } diff --git a/crates/bonsaidb-server/src/server/acme.rs b/crates/bonsaidb-server/src/server/acme.rs index ad025e54263..931eab3d9c7 100644 --- a/crates/bonsaidb-server/src/server/acme.rs +++ b/crates/bonsaidb-server/src/server/acme.rs @@ -7,8 +7,8 @@ use bonsaidb_core::{ define_basic_unique_mapped_view, document::KeyId, schema::{ - Collection, CollectionDocument, CollectionName, DefaultSerialization, InvalidNameError, - Schematic, SerializedCollection, + Collection, CollectionDocument, CollectionName, DefaultSerialization, Schematic, + SerializedCollection, }, ENCRYPTION_ENABLED, }; @@ -32,7 +32,7 @@ impl Collection for AcmeAccount { } } - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "acme-accounts") } diff --git a/crates/bonsaidb/crate-docs.md b/crates/bonsaidb/crate-docs.md index 8d40388fcb6..1ac183981d1 100644 --- a/crates/bonsaidb/crate-docs.md +++ b/crates/bonsaidb/crate-docs.md @@ -17,7 +17,7 @@ struct Shape { } impl Collection for Shape { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "shapes") } @@ -36,7 +36,7 @@ impl View for ShapesByNumberOfSides { type Key = u32; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-number-of-sides") } } diff --git a/crates/bonsaidb/tests/simultaneous-connections.rs b/crates/bonsaidb/tests/simultaneous-connections.rs index 86e4debf685..34dd73102b7 100644 --- a/crates/bonsaidb/tests/simultaneous-connections.rs +++ b/crates/bonsaidb/tests/simultaneous-connections.rs @@ -4,7 +4,7 @@ use bonsaidb::{ client::{url::Url, Client}, core::{ connection::StorageConnection, - test_util::{self, Basic, BasicSchema, TestDirectory}, + test_util::{self, BasicSchema, TestDirectory}, }, local::config::Builder, server::{DefaultPermissions, Server, ServerConfiguration}, @@ -46,10 +46,10 @@ async fn simultaneous_connections() -> anyhow::Result<()> { async fn test_one_client(client: Client, database_name: String) -> anyhow::Result<()> { for _ in 0u32..50 { client - .create_database::(&database_name, false) + .create_database::(&database_name, false) .await .unwrap(); - let db = client.database::(&database_name).await?; + let db = client.database::(&database_name).await?; test_util::store_retrieve_update_delete_tests(&db) .await .unwrap(); diff --git a/examples/basic-local/examples/basic-local-multidb.rs b/examples/basic-local/examples/basic-local-multidb.rs index 837fc3521ca..96611153a6f 100644 --- a/examples/basic-local/examples/basic-local-multidb.rs +++ b/examples/basic-local/examples/basic-local-multidb.rs @@ -4,8 +4,7 @@ use bonsaidb::{ core::{ connection::{Connection, StorageConnection}, schema::{ - Collection, CollectionName, DefaultSerialization, InvalidNameError, Schematic, - SerializedCollection, + Collection, CollectionName, DefaultSerialization, Schematic, SerializedCollection, }, Error, }, @@ -23,7 +22,7 @@ struct Message { } impl Collection for Message { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "messages") } diff --git a/examples/basic-local/examples/basic-local.rs b/examples/basic-local/examples/basic-local.rs index a5569da3c71..de249e4f1e5 100644 --- a/examples/basic-local/examples/basic-local.rs +++ b/examples/basic-local/examples/basic-local.rs @@ -3,8 +3,7 @@ use std::time::SystemTime; use bonsaidb::{ core::{ schema::{ - Collection, CollectionName, DefaultSerialization, InvalidNameError, Schematic, - SerializedCollection, + Collection, CollectionName, DefaultSerialization, Schematic, SerializedCollection, }, Error, }, @@ -22,7 +21,7 @@ struct Message { } impl Collection for Message { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "messages") } diff --git a/examples/basic-local/examples/view-examples.rs b/examples/basic-local/examples/view-examples.rs index d68770806a0..fa452d9fe45 100644 --- a/examples/basic-local/examples/view-examples.rs +++ b/examples/basic-local/examples/view-examples.rs @@ -3,8 +3,8 @@ use bonsaidb::{ connection::Connection, schema::{ view::CollectionViewSchema, Collection, CollectionDocument, CollectionName, - DefaultSerialization, DefaultViewSerialization, InvalidNameError, Name, ReduceResult, - Schematic, SerializedCollection, View, ViewMapResult, ViewMappedValue, + DefaultSerialization, DefaultViewSerialization, Name, ReduceResult, Schematic, + SerializedCollection, View, ViewMapResult, ViewMappedValue, }, Error, }, @@ -22,7 +22,7 @@ struct Shape { } impl Collection for Shape { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "shapes") } @@ -41,7 +41,7 @@ impl View for ShapesByNumberOfSides { type Key = u32; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-number-of-sides") } } diff --git a/examples/basic-server/examples/support/schema.rs b/examples/basic-server/examples/support/schema.rs index c993f0803f6..05f2e7f421a 100644 --- a/examples/basic-server/examples/support/schema.rs +++ b/examples/basic-server/examples/support/schema.rs @@ -1,8 +1,8 @@ use bonsaidb::core::{ schema::{ view::CollectionViewSchema, Collection, CollectionDocument, CollectionName, - DefaultSerialization, DefaultViewSerialization, InvalidNameError, Name, ReduceResult, - Schematic, View, ViewMapResult, ViewMappedValue, + DefaultSerialization, DefaultViewSerialization, Name, ReduceResult, Schematic, View, + ViewMapResult, ViewMappedValue, }, Error, }; @@ -20,7 +20,7 @@ impl Shape { } impl Collection for Shape { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("khonsulabs", "shapes") } @@ -39,7 +39,7 @@ impl View for ShapesByNumberOfSides { type Key = u32; type Value = usize; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("by-number-of-sides") } } diff --git a/examples/view-histogram/examples/view-histogram.rs b/examples/view-histogram/examples/view-histogram.rs index 31488d6d89a..b8fcf2491e4 100644 --- a/examples/view-histogram/examples/view-histogram.rs +++ b/examples/view-histogram/examples/view-histogram.rs @@ -20,8 +20,8 @@ use bonsaidb::{ connection::{AccessPolicy, Connection}, schema::{ view::CollectionViewSchema, Collection, CollectionDocument, CollectionName, - DefaultSerialization, InvalidNameError, Name, ReduceResult, Schematic, SerializedView, - View, ViewMappedValue, + DefaultSerialization, Name, ReduceResult, Schematic, SerializedView, View, + ViewMappedValue, }, transmog::{Format, OwnedDeserializer}, }, @@ -103,7 +103,7 @@ pub struct Samples { } impl Collection for Samples { - fn collection_name() -> Result { + fn collection_name() -> CollectionName { CollectionName::new("histogram-example", "samples") } @@ -123,7 +123,7 @@ impl View for AsHistogram { type Key = u64; type Value = SyncHistogram; - fn name(&self) -> Result { + fn name(&self) -> Name { Name::new("as-histogram") } }