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")
|
.chain_err(|| "Could not get_user_version")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rename "SQL" functions to align with "datoms" functions.
|
/// Do just enough work that either `create_current_version` or sync can populate the DB.
|
||||||
pub fn create_current_version(conn: &mut rusqlite::Connection) -> Result<DB> {
|
pub fn create_empty_current_version(conn: &mut rusqlite::Connection) -> Result<(rusqlite::Transaction, DB)> {
|
||||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||||
|
|
||||||
for statement in (&V1_STATEMENTS).iter() {
|
for statement in (&V1_STATEMENTS).iter() {
|
||||||
tx.execute(statement, &[])?;
|
tx.execute(statement, &[])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_user_version(&tx, CURRENT_VERSION)?;
|
||||||
|
|
||||||
|
let bootstrap_schema = bootstrap::bootstrap_schema();
|
||||||
let bootstrap_partition_map = bootstrap::bootstrap_partition_map();
|
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: 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: install these using bootstrap assertions. It's tricky because the part ranges are implicit.
|
||||||
// TODO: one insert, chunk into 999/3 sections, for safety.
|
// 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.
|
// 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.
|
// TODO: Convert "keyword" part to SQL using Value conversion.
|
||||||
tx.execute("INSERT INTO parts VALUES (?, ?, ?)", &[part, &partition.start, &partition.index])?;
|
tx.execute("INSERT INTO parts VALUES (?, ?, ?)", &[part, &partition.start, &partition.index])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: return to transact_internal to self-manage the encompassing SQLite transaction.
|
// 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 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.
|
// TODO: validate metadata mutations that aren't schema related, like additional partitions.
|
||||||
if let Some(next_schema) = next_schema {
|
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
|
// 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")));
|
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?
|
// TODO: use the drop semantics to do this automagically?
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
|
||||||
let bootstrap_db = DB::new(next_partition_map, bootstrap_schema);
|
db.partition_map = next_partition_map;
|
||||||
Ok(bootstrap_db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// (def v2-statements v1-statements)
|
// (def v2-statements v1-statements)
|
||||||
|
|
38
src/conn.rs
38
src/conn.rs
|
@ -10,7 +10,17 @@
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![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;
|
||||||
use rusqlite::{
|
use rusqlite::{
|
||||||
|
@ -128,6 +138,21 @@ pub struct Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn open(path: &str) -> Result<Store> {
|
||||||
let mut connection = ::new_connection(path)?;
|
let mut connection = ::new_connection(path)?;
|
||||||
let conn = Conn::connect(&mut connection)?;
|
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> {
|
pub fn connect(sqlite: &mut rusqlite::Connection) -> Result<Conn> {
|
||||||
let db = db::ensure_current_version(sqlite)
|
let db = db::ensure_current_version(sqlite)
|
||||||
.chain_err(|| "Unable to initialize Mentat store")?;
|
.chain_err(|| "Unable to initialize Mentat store")?;
|
||||||
|
|
|
@ -54,6 +54,11 @@ error_chain! {
|
||||||
}
|
}
|
||||||
|
|
||||||
errors {
|
errors {
|
||||||
|
PathAlreadyExists(path: String) {
|
||||||
|
description("path already exists")
|
||||||
|
display("path {} already exists", path)
|
||||||
|
}
|
||||||
|
|
||||||
UnboundVariables(names: BTreeSet<String>) {
|
UnboundVariables(names: BTreeSet<String>) {
|
||||||
description("unbound variables at query execution time")
|
description("unbound variables at query execution time")
|
||||||
display("variables {:?} unbound at query execution time", names)
|
display("variables {:?} unbound at query execution time", names)
|
||||||
|
|
|
@ -34,6 +34,7 @@ use edn;
|
||||||
|
|
||||||
pub static HELP_COMMAND: &'static str = &"help";
|
pub static HELP_COMMAND: &'static str = &"help";
|
||||||
pub static OPEN_COMMAND: &'static str = &"open";
|
pub static OPEN_COMMAND: &'static str = &"open";
|
||||||
|
pub static OPEN_EMPTY_COMMAND: &'static str = &"empty";
|
||||||
pub static CLOSE_COMMAND: &'static str = &"close";
|
pub static CLOSE_COMMAND: &'static str = &"close";
|
||||||
pub static LONG_QUERY_COMMAND: &'static str = &"query";
|
pub static LONG_QUERY_COMMAND: &'static str = &"query";
|
||||||
pub static SHORT_QUERY_COMMAND: &'static str = &"q";
|
pub static SHORT_QUERY_COMMAND: &'static str = &"q";
|
||||||
|
@ -53,6 +54,7 @@ pub enum Command {
|
||||||
Exit,
|
Exit,
|
||||||
Help(Vec<String>),
|
Help(Vec<String>),
|
||||||
Open(String),
|
Open(String),
|
||||||
|
OpenEmpty(String),
|
||||||
Query(String),
|
Query(String),
|
||||||
Schema,
|
Schema,
|
||||||
Sync(Vec<String>),
|
Sync(Vec<String>),
|
||||||
|
@ -76,6 +78,7 @@ impl Command {
|
||||||
&Command::Timer(_) |
|
&Command::Timer(_) |
|
||||||
&Command::Help(_) |
|
&Command::Help(_) |
|
||||||
&Command::Open(_) |
|
&Command::Open(_) |
|
||||||
|
&Command::OpenEmpty(_) |
|
||||||
&Command::Close |
|
&Command::Close |
|
||||||
&Command::Exit |
|
&Command::Exit |
|
||||||
&Command::Sync(_) |
|
&Command::Sync(_) |
|
||||||
|
@ -91,6 +94,7 @@ impl Command {
|
||||||
&Command::Timer(_) |
|
&Command::Timer(_) |
|
||||||
&Command::Help(_) |
|
&Command::Help(_) |
|
||||||
&Command::Open(_) |
|
&Command::Open(_) |
|
||||||
|
&Command::OpenEmpty(_) |
|
||||||
&Command::Close |
|
&Command::Close |
|
||||||
&Command::Exit |
|
&Command::Exit |
|
||||||
&Command::Sync(_) |
|
&Command::Sync(_) |
|
||||||
|
@ -115,6 +119,9 @@ impl Command {
|
||||||
&Command::Open(ref args) => {
|
&Command::Open(ref args) => {
|
||||||
format!(".{} {}", OPEN_COMMAND, args)
|
format!(".{} {}", OPEN_COMMAND, args)
|
||||||
},
|
},
|
||||||
|
&Command::OpenEmpty(ref args) => {
|
||||||
|
format!(".{} {}", OPEN_EMPTY_COMMAND, args)
|
||||||
|
},
|
||||||
&Command::Close => {
|
&Command::Close => {
|
||||||
format!(".{}", CLOSE_COMMAND)
|
format!(".{}", CLOSE_COMMAND)
|
||||||
},
|
},
|
||||||
|
@ -163,6 +170,19 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
||||||
}
|
}
|
||||||
Ok(Command::Open(args[0].clone()))
|
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()
|
let no_arg_parser = || arguments()
|
||||||
.skip(spaces())
|
.skip(spaces())
|
||||||
|
@ -236,10 +256,11 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
||||||
});
|
});
|
||||||
spaces()
|
spaces()
|
||||||
.skip(token('.'))
|
.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(help_parser),
|
||||||
&mut try(timer_parser),
|
&mut try(timer_parser),
|
||||||
&mut try(open_parser),
|
&mut try(open_parser),
|
||||||
|
&mut try(open_empty_parser),
|
||||||
&mut try(close_parser),
|
&mut try(close_parser),
|
||||||
&mut try(explain_query_parser),
|
&mut try(explain_query_parser),
|
||||||
&mut try(exit_parser),
|
&mut try(exit_parser),
|
||||||
|
|
|
@ -189,6 +189,12 @@ impl Repl {
|
||||||
Err(e) => eprintln!("{:?}", e)
|
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::Close => self.close(),
|
||||||
Command::Query(query) => self.execute_query(query),
|
Command::Query(query) => self.execute_query(query),
|
||||||
Command::QueryExplain(query) => self.explain_query(query),
|
Command::QueryExplain(query) => self.explain_query(query),
|
||||||
|
@ -228,6 +234,18 @@ impl Repl {
|
||||||
Ok(())
|
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.
|
// Close the current store by opening a new in-memory store in its place.
|
||||||
fn close(&mut self) {
|
fn close(&mut self) {
|
||||||
let old_db_name = self.db_name();
|
let old_db_name = self.db_name();
|
||||||
|
|
Loading…
Reference in a new issue