Provide an API for creating truly empty stores (#561) r=grisha
* Part 1: split create_current_version. * Part 2: add Store::create_empty and Conn::empty. * Part 3 - Expose 'open_empty' command via CLI
This commit is contained in:
parent
93e5dff9c8
commit
ae91603bd0
5 changed files with 101 additions and 12 deletions
29
db/src/db.rs
29
db/src/db.rs
|
@ -214,43 +214,52 @@ fn get_user_version(conn: &rusqlite::Connection) -> Result<i32> {
|
|||
.chain_err(|| "Could not get_user_version")
|
||||
}
|
||||
|
||||
// TODO: rename "SQL" functions to align with "datoms" functions.
|
||||
pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
||||
/// Do just enough work that either `create_current_version` or sync can populate the DB.
|
||||
pub fn create_empty_current_version(conn: &mut rusqlite::Connection) -> Result<(rusqlite::Transaction, DB)> {
|
||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||
|
||||
for statement in (&V1_STATEMENTS).iter() {
|
||||
tx.execute(statement, &[])?;
|
||||
}
|
||||
|
||||
set_user_version(&tx, CURRENT_VERSION)?;
|
||||
|
||||
let bootstrap_schema = bootstrap::bootstrap_schema();
|
||||
let bootstrap_partition_map = bootstrap::bootstrap_partition_map();
|
||||
|
||||
Ok((tx, DB::new(bootstrap_partition_map, bootstrap_schema)))
|
||||
}
|
||||
|
||||
// TODO: rename "SQL" functions to align with "datoms" functions.
|
||||
pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
||||
let (tx, mut db) = create_empty_current_version(conn)?;
|
||||
|
||||
// TODO: think more carefully about allocating new parts and bitmasking part ranges.
|
||||
// TODO: install these using bootstrap assertions. It's tricky because the part ranges are implicit.
|
||||
// TODO: one insert, chunk into 999/3 sections, for safety.
|
||||
// This is necessary: `transact` will only UPDATE parts, not INSERT them if they're missing.
|
||||
for (part, partition) in bootstrap_partition_map.iter() {
|
||||
for (part, partition) in db.partition_map.iter() {
|
||||
// TODO: Convert "keyword" part to SQL using Value conversion.
|
||||
tx.execute("INSERT INTO parts VALUES (?, ?, ?)", &[part, &partition.start, &partition.index])?;
|
||||
}
|
||||
|
||||
// TODO: return to transact_internal to self-manage the encompassing SQLite transaction.
|
||||
let bootstrap_schema = bootstrap::bootstrap_schema();
|
||||
let bootstrap_schema_for_mutation = Schema::default(); // The bootstrap transaction will populate this schema.
|
||||
let (_report, next_partition_map, next_schema) = transact(&tx, bootstrap_partition_map, &bootstrap_schema_for_mutation, &bootstrap_schema, bootstrap::bootstrap_entities())?;
|
||||
|
||||
let (_report, next_partition_map, next_schema) = transact(&tx, db.partition_map, &bootstrap_schema_for_mutation, &db.schema, bootstrap::bootstrap_entities())?;
|
||||
// TODO: validate metadata mutations that aren't schema related, like additional partitions.
|
||||
if let Some(next_schema) = next_schema {
|
||||
if next_schema != bootstrap_schema {
|
||||
if next_schema != db.schema {
|
||||
// TODO Use custom ErrorKind https://github.com/brson/error-chain/issues/117
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Initial bootstrap transaction did not produce expected bootstrap schema")));
|
||||
}
|
||||
}
|
||||
|
||||
set_user_version(&tx, CURRENT_VERSION)?;
|
||||
|
||||
// TODO: use the drop semantics to do this automagically?
|
||||
tx.commit()?;
|
||||
|
||||
let bootstrap_db = DB::new(next_partition_map, bootstrap_schema);
|
||||
Ok(bootstrap_db)
|
||||
db.partition_map = next_partition_map;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
// (def v2-statements v1-statements)
|
||||
|
|
38
src/conn.rs
38
src/conn.rs
|
@ -10,7 +10,17 @@
|
|||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use std::path::{
|
||||
Path,
|
||||
};
|
||||
|
||||
use std::sync::{
|
||||
Arc,
|
||||
Mutex,
|
||||
RwLock,
|
||||
RwLockReadGuard,
|
||||
RwLockWriteGuard,
|
||||
};
|
||||
|
||||
use rusqlite;
|
||||
use rusqlite::{
|
||||
|
@ -128,6 +138,21 @@ pub struct Store {
|
|||
}
|
||||
|
||||
impl Store {
|
||||
pub fn open_empty(path: &str) -> Result<Store> {
|
||||
if !path.is_empty() {
|
||||
if Path::new(path).exists() {
|
||||
bail!(ErrorKind::PathAlreadyExists(path.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut connection = ::new_connection(path)?;
|
||||
let conn = Conn::empty(&mut connection)?;
|
||||
Ok(Store {
|
||||
conn: conn,
|
||||
sqlite: connection,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(path: &str) -> Result<Store> {
|
||||
let mut connection = ::new_connection(path)?;
|
||||
let conn = Conn::connect(&mut connection)?;
|
||||
|
@ -441,6 +466,17 @@ impl Conn {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prepare the provided SQLite handle for use as a Mentat store. Creates tables but
|
||||
/// _does not_ write the bootstrap schema. This constructor should only be used by
|
||||
/// consumers that expect to populate raw transaction data themselves.
|
||||
fn empty(sqlite: &mut rusqlite::Connection) -> Result<Conn> {
|
||||
let (tx, db) = db::create_empty_current_version(sqlite)
|
||||
.chain_err(|| "Unable to initialize Mentat store")?;
|
||||
tx.commit()?;
|
||||
Ok(Conn::new(db.partition_map, db.schema))
|
||||
}
|
||||
|
||||
|
||||
pub fn connect(sqlite: &mut rusqlite::Connection) -> Result<Conn> {
|
||||
let db = db::ensure_current_version(sqlite)
|
||||
.chain_err(|| "Unable to initialize Mentat store")?;
|
||||
|
|
|
@ -54,6 +54,11 @@ error_chain! {
|
|||
}
|
||||
|
||||
errors {
|
||||
PathAlreadyExists(path: String) {
|
||||
description("path already exists")
|
||||
display("path {} already exists", path)
|
||||
}
|
||||
|
||||
UnboundVariables(names: BTreeSet<String>) {
|
||||
description("unbound variables at query execution time")
|
||||
display("variables {:?} unbound at query execution time", names)
|
||||
|
|
|
@ -34,6 +34,7 @@ use edn;
|
|||
|
||||
pub static HELP_COMMAND: &'static str = &"help";
|
||||
pub static OPEN_COMMAND: &'static str = &"open";
|
||||
pub static OPEN_EMPTY_COMMAND: &'static str = &"empty";
|
||||
pub static CLOSE_COMMAND: &'static str = &"close";
|
||||
pub static LONG_QUERY_COMMAND: &'static str = &"query";
|
||||
pub static SHORT_QUERY_COMMAND: &'static str = &"q";
|
||||
|
@ -53,6 +54,7 @@ pub enum Command {
|
|||
Exit,
|
||||
Help(Vec<String>),
|
||||
Open(String),
|
||||
OpenEmpty(String),
|
||||
Query(String),
|
||||
Schema,
|
||||
Sync(Vec<String>),
|
||||
|
@ -76,6 +78,7 @@ impl Command {
|
|||
&Command::Timer(_) |
|
||||
&Command::Help(_) |
|
||||
&Command::Open(_) |
|
||||
&Command::OpenEmpty(_) |
|
||||
&Command::Close |
|
||||
&Command::Exit |
|
||||
&Command::Sync(_) |
|
||||
|
@ -91,6 +94,7 @@ impl Command {
|
|||
&Command::Timer(_) |
|
||||
&Command::Help(_) |
|
||||
&Command::Open(_) |
|
||||
&Command::OpenEmpty(_) |
|
||||
&Command::Close |
|
||||
&Command::Exit |
|
||||
&Command::Sync(_) |
|
||||
|
@ -115,6 +119,9 @@ impl Command {
|
|||
&Command::Open(ref args) => {
|
||||
format!(".{} {}", OPEN_COMMAND, args)
|
||||
},
|
||||
&Command::OpenEmpty(ref args) => {
|
||||
format!(".{} {}", OPEN_EMPTY_COMMAND, args)
|
||||
},
|
||||
&Command::Close => {
|
||||
format!(".{}", CLOSE_COMMAND)
|
||||
},
|
||||
|
@ -163,6 +170,19 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
|||
}
|
||||
Ok(Command::Open(args[0].clone()))
|
||||
});
|
||||
|
||||
let open_empty_parser = string(OPEN_EMPTY_COMMAND)
|
||||
.with(spaces())
|
||||
.with(arguments())
|
||||
.map(|args| {
|
||||
if args.len() < 1 {
|
||||
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
|
||||
}
|
||||
if args.len() > 1 {
|
||||
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])));
|
||||
}
|
||||
Ok(Command::OpenEmpty(args[0].clone()))
|
||||
});
|
||||
|
||||
let no_arg_parser = || arguments()
|
||||
.skip(spaces())
|
||||
|
@ -236,10 +256,11 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
|||
});
|
||||
spaces()
|
||||
.skip(token('.'))
|
||||
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 10], _>
|
||||
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 11], _>
|
||||
([&mut try(help_parser),
|
||||
&mut try(timer_parser),
|
||||
&mut try(open_parser),
|
||||
&mut try(open_empty_parser),
|
||||
&mut try(close_parser),
|
||||
&mut try(explain_query_parser),
|
||||
&mut try(exit_parser),
|
||||
|
|
|
@ -189,6 +189,12 @@ impl Repl {
|
|||
Err(e) => eprintln!("{:?}", e)
|
||||
};
|
||||
}
|
||||
Command::OpenEmpty(db) => {
|
||||
match self.open_empty(db) {
|
||||
Ok(_) => println!("Empty database {:?} opened", self.db_name()),
|
||||
Err(e) => eprintln!("{}", e.to_string()),
|
||||
};
|
||||
},
|
||||
Command::Close => self.close(),
|
||||
Command::Query(query) => self.execute_query(query),
|
||||
Command::QueryExplain(query) => self.explain_query(query),
|
||||
|
@ -228,6 +234,18 @@ impl Repl {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn open_empty<T>(&mut self, path: T) -> ::mentat::errors::Result<()>
|
||||
where T: Into<String> {
|
||||
let path = path.into();
|
||||
if self.path.is_empty() || path != self.path {
|
||||
let next = Store::open_empty(path.as_str())?;
|
||||
self.path = path;
|
||||
self.store = next;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Close the current store by opening a new in-memory store in its place.
|
||||
fn close(&mut self) {
|
||||
let old_db_name = self.db_name();
|
||||
|
|
Loading…
Reference in a new issue