-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: 1.0 stabilization #575
Comments
Hey there! Here’s a few bits and pieces i noticed. Some of them may be interesting enough to be added to v1. User ID & User VersionHow about adding a User ID and User Version to the database header? Stored as I suggest the following API changes to accomodate this (naming bikesheddable): impl Database {
// These two are just wrappers around tiny read transactions.
fn user_id(&self) -> u32;
fn user_version(&self) -> u32;
// Merely reads the two header fields and then compares them against
// the given parameters.
fn check_user_metadata(&mut self, expected_id: u32, expected_version: u32)
-> Result<NeedsMigration, Error>;
// `steps` is a map of `version → migration`
// This handy helper function assumes that versions are
// monotonically incrementing in apps.
fn migrate_to(&mut self, target_version: u32, steps: &[impl Migration])
-> Result<(), Error> {
assert!(target_version < steps.len());
let user_id = self.user_id();
loop {
match self.check_user_metadata(user_id, self.user_version())? {
NeedsMigration::None => return Ok(()),
NeedsMigration::UpFrom { current_version }
=> if let Some(mig) = steps.get(current_version as usize) {
let mut wtx = self.begin_write()?;
wtx.set_durability(Durability::Paranoid);
mig.up(&wtx, current_version)?;
wtx.write_user_version(current_version + 1);
wtx.commit()?;
}
else { return Err(Error::NoMigrationForVersion(current_version)); },
NeedsMigration::DownFrom …
}
}
}
}
enum NeedsMigration {
None,
UpFrom { current_version: u32 },
DownFrom { current_version: u32 },
}
enum Error {
…
// Thrown by `check_user_metadata` and `migrate_to`.
UserIdMismatch(u32),
// Thrown if the database header’s user version would
// overflow `steps` in `migrate_to`, i.e. when there is no
// known migration for the reported version.
NoMigrationForVersion(u32),
…
}
impl WriteTransaction {
// Exclusive access, mainly to prevent migrations from touching these.
fn write_user_id(&mut self, user_id: u32);
fn write_user_version(&mut self, user_version: u32);
}
impl ReadTransaction {
fn user_id(&self) -> u32;
fn user_version(&self) -> u32;
}
trait Migration {
// Redundant `current_version` to help catch migration step ordering bugs.
// ` up` goes `current_version → current_version+1`
// `down` goes `current_version → current_version−1`
fn up(&self, &WriteTransaction, current_version: u32) -> Result<(), Error>;
fn down(&self, &WriteTransaction, current_version: u32) -> Result<(), Error>;
}
impl<F> Migration for (F, F)
where F: FnOnce(&WriteTransaction, u32) -> Result<(), Error>
{
…
} Table Name TypesI know this would be a very deep-running change, but i’d like to get rid of Now, adding a Change all table names to Now everything that can be inlined into 16 bytes, or anything that can be hashed into 16 bytes, can be used as a table name. And the database engine can perform much cheaper table look-ups. Here’s pretty much what the user-visible API changes would be: impl TableDefinition<'db, K, V> {
pub const fn from_name(name: &str) -> Self {
// `const fn` hash function required
Self::from_u128(const_xxH3_128(name))
}
#[cfg(feature = "uuid")]
pub const fn from_uuid(id: uuid::Uuid) -> Self {
Self::from_u128(id.as_u128())
}
// and whatever more…
// This here is the only strictly necessary function.
// `from_name` just has to exist to tell developers that yes, it is
// okay to use strings.
// `from_uuid` just has to exist to tell developers who like UUID
// that yes, they can even use UUID if they want to.
// But they can also just use magic numbers like `0xC01D_C0FFEE`
// or `as u128` any `#[repr(…)] enum`.
pub const fn from_u128(id: u128) -> Self;
} Something similar could happen to SessionsCurrently, Redb has two slots for active transaction commit slots. I propose adding a region of additional commit slots, each assigned a The idea is to add some kind of session API to the database connection. Here’s what it may look like: impl Database {
pub fn create_session(&self, id: u128) -> Result<Session<'_>, Error>;
pub fn open_session(&self, id: u128) -> Result<Session<'_>, Error>;
pub fn list_sessions(&self) -> impl Iterator<Item = u128>;
}
impl<'db> Session<'db> {
// These transactions do not use the commit slots of the super header,
// but alternative named commit slots.
pub fn begin_read( &self) -> Result< ReadTransaction<'db>, Error>;
pub fn begin_write(&self) -> Result<WriteTransaction<'db>, Error>;
// Overrides the commit slots with the super header commit slots,
// synchronising this session with the current main database state.
pub fn pull_from_main(&mut self) -> Result<(), Error>;
// Overrides the commit slots with the ones of another session.
pub fn pull_from_session(&mut self, other_id: u128) -> Result<(), Error>;
// Default on drop is to `save`. Just means the session commit slots
// are kept on disk.
pub fn save(self) -> Result<(), Error>;
// Deletes the session from disk, turning its commit slots into a free
// page. When scanning for free database pages, pages only pointed
// at by this session are now also free.
pub fn delete(self) -> Result<(), Error>;
// Overrides the super header commit slots by this session’s
// and also deletes this session.
pub fn merge_into_main(self) -> Result<(), Error>;
// Overrides the commit slots of the other session by this one’s,
// then deletes this one.
pub fn merge_into_session(self, other_id: u128) -> Result<(), Error>;
} Why would that be useful? I can see two use-cases. It would allow applications to selectively preserve some historical state, shall the user wish to. Like drafts of a project you can check out any time. Simply call It allows for coarser rollback points when users of an app are expected to perform lots of small changes. Think of hitting An app may immediately Semantically, a Error ChangesA bunch of current Corrupted & TableTypeMismatchI suggest completely removing these error variants in favour of these: struct TableLayout {
pub table_id : u128,
pub key_type: Type,
pub value_type: Type,
}
struct Type {
pub name: TypeName, // Or TypeId or…
pub align_log2: u8,
pub byte_size: u32,
}
enum Error {
…
LayoutMismatch { expected: TableLayout, actual: TableLayout },
DowngradeRequired(u8),
…
} Push message formatting into a method on that enum, don’t push TableDoesNotExist & TableAlreadyOpenIf the name change happens, then |
Hey, I've been recently using I do have a few suggestions relating to
|
impl Borrow<...>Your RedbValueI'd love to simplify this, but I don't think your idea will work. If the lifetime is moved to the trait, let's call it |
Thanks for the detailed feedback! Thoughts & questions below: User ID & User VersionI think this can be achieved by the user creating a table such as Table Name TypesWhy do you think SessionsThis is an interesting idea, and the coarser rollback points you bring up is similar to the request for persistent savepoints (#574). Would persistent savepoints cover your use case? Error ChangesGood point! I should revisit all the variants of |
@Zenithsiz ya, unfortunately I don't think the The alternative that I would suggest for your use case is to add special values into the serialization format. Like prefix your |
@cberner Hm, I'm not capable of testing it right now, but maybe if instead of Also another possible issue is that in |
Unfortunately I need the |
I was going to suggest implementing I also played around the idea of simplifying |
It used to use HRTBs and the number of hacks required was even worse ;( #366 |
I changed the |
Hello! =)
From the perspective of one specific app: Yes. However, from the perspective of »any random app«, not quite. The neat thing about SQLite’s header-embedded meta data is that any app can make sense of it without knowing anything about the schema. App 1 can reject a (I forgot, I think it’d be helpful to add these as a standardised identification manner, but really it’s a matter of taste. And if you’d rather keep the API down to the absolute basics, then that’s a good enough reason for me to not standardised a »user id« and »user version«.
They do seem to be pretty much the same thing, so yes. Good to see there’s already an issue open for that. =)
That’s just me being me, which is why that section of my comment was full of »if«s. I come from a perspective where easy to avoid points of failure are preferrably avoided. That includes heap allocations for small pieces of data (e.g. So to summarise: I think
While at it, it may be worth considering breaking up SQLite, for example, has like a billion error and status codes, yet individual functions only throw a small subset of them at a time. Sometimes these functions even actually turn out to be infallible, but SQLite didn’t document that, and still had them return status codes. That makes it quite annoying for super diligent error handling and error recovery code, for you paranoidly have to expect everything to happen, even if it seems to make no sense. I had similar pain working with UEFI crates. You get a lot of |
an |
Off the cuff i only know myself doing the per-function error enums in Rust. And i blame that on Rust-the-language making that super tedious and annoying. If you instead look at Zig, there it’s super quick and easy to declare and use error code subsets, and thus Zig programmers do often have per-function error code sets. |
I know it's a big stretch at this point, but would be cool to be able to store custom data in branch pages. For example what if each branch contains a value that is equal to Motivational example 1: We store a tree of customers, each customer could create a ticket with a date and a priority, it can close the ticket and change the priority. We want to find the oldest active ticket of the highest priority. Let's store Motivational example 2: We store an online leaderboard for the game, players are sorted by the max level they achieved, players with the same level are sorted by the date they achieved that level. How can we find a placement for a given player? Let's store |
Interesting idea! Ya, I don't think I'll add that as I think the upside is too small to justify the added complexity. I experimented with B-epsilon trees which are vaguely similar (leaf values buffered in the branch pages), but ultimately decided they weren't worthwhile. |
I'm planning to release version 1.0 soon, and am looking for feedback on the file format, API, and bug reports!
Please comment here, or file a separate issue if more appropriate. After releasing version 1.0, I'm going to try hard to avoid any file format changes, as putting in upgrade logic would be a maintenance burden.
Please note, I place a high value on keeping the code base simple, so while I'll give every feature suggestion serious consideration, please don't be offended when I (likely) reject it.
The text was updated successfully, but these errors were encountered: