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:
Richard Newman 2018-02-16 02:01:00 -08:00 committed by Grisha Kruglov
parent 93e5dff9c8
commit ae91603bd0
5 changed files with 101 additions and 12 deletions

View file

@ -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)

View file

@ -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")?;

View file

@ -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)

View file

@ -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),

View file

@ -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();