I've been thinking about writing a "simple" nosql strongly typed relational database, and find myself tempted to make it use a global variable, which of course horrifies me, but for which I can't see a cleaner approach. So I'd like to talk through my reasoning hear and see if y'all have a better idea.
By a relational database, I mean that data can refer to other data, including in cycles. By strongly typed, I mean that users will specify their "tables" as rust types. e.g.
struct Person {
name: String,
birth_year: i64,
mother: Option<Key<Person>>,
}
To make the database relational, when a datum is inserted into the database, a Key<T>
will be created, which is the primary key, and functions like a pointer to the datum. In the above example, the mother field could hold a key to the mother (if mother is in the database).
I would prevent the use of invalid keys but not making public any constructors that would allow the creation of invalid Keys. This would avoid numerous possible runtime errors and bugs. There is a catch, though: what if a user creates two databases, and tries to use a key from one database in another database? All those runtime errors become possible again.
So how to handle this?
Plan A: There Can Be Only One
Put the database in a global variable. Don't let users close it and open another.
This solves all the consistency issues (apart from a corrupt database, of course) and allows panic free operation (provided users don't do stupid unsafe things). It also means that we could look up key data more simply (e.g. implement Deref
for Key<T>
even if it is Copy
).
Plan B: Trust/blame the User
Document that you run into trouble of you mix up the keys for two different databases and leave it at that. Requires panic if user is naughty, and that panic could happen deep in the code. Also, looking up data from keys requires user to also say which database it is for. Clumsy and panic-ridden doesn't make this sound appealing.
Plan C: Heavyweight Keys
Store a reference (presumably Arc<Mutex<...>>
) to the database in the keys themselves. When using keys check that they are for the right database. We then have panics at the leaves of the API. Deref becomes a possibility, but not a great idea, if it might lead to deadlock. But some sort of nice API is probably possible. It does mean keys can't be Copy so the API cannot be as pretty as for Plan A.
Plan D: Wish for Witness Types
If I were writing in Haskell, I'd use existential witness types. Basically the function that opens a database could create a brand new type that would allow to ensure that any keys used for that database would have a different type than keys for any other database. So far as I know, rust does not have this capability.
Conclusion
On the whole, I lean towards the single global database option. But I'm also uncomfortable with it. Any suggestions?