diff --git a/db/src/db.rs b/db/src/db.rs index 68f96966..103275b9 100644 --- a/db/src/db.rs +++ b/db/src/db.rs @@ -214,43 +214,52 @@ fn get_user_version(conn: &rusqlite::Connection) -> Result { .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 { +/// 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 { + 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) diff --git a/src/conn.rs b/src/conn.rs index 798d278d..b686ad45 100644 --- a/src/conn.rs +++ b/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 { + 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 { 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 { + 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 { let db = db::ensure_current_version(sqlite) .chain_err(|| "Unable to initialize Mentat store")?; diff --git a/src/errors.rs b/src/errors.rs index 35910e02..de0a57e8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -54,6 +54,11 @@ error_chain! { } errors { + PathAlreadyExists(path: String) { + description("path already exists") + display("path {} already exists", path) + } + UnboundVariables(names: BTreeSet) { description("unbound variables at query execution time") display("variables {:?} unbound at query execution time", names) diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 8a91bc31..9ce17596 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -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), Open(String), + OpenEmpty(String), Query(String), Schema, Sync(Vec), @@ -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 { } 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 { }); spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 10], _> + .with(choice::<[&mut Parser>; 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), diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index d23c8ce7..3cb3dcd4 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -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(&mut self, path: T) -> ::mentat::errors::Result<()> + where T: Into { + 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();