From c61bc79b99f4cb7d63a491c3b7c842848ed7cf90 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Wed, 13 Dec 2017 14:19:05 -0600 Subject: [PATCH] Sync metadata schema and SyncMetadataClient. (#502) r=rnewman --- Cargo.toml | 3 + edn/src/lib.rs | 1 + tolstoy/Cargo.toml | 28 ++++++++++ tolstoy/src/lib.rs | 41 ++++++++++++++ tolstoy/src/metadata.rs | 74 +++++++++++++++++++++++++ tolstoy/src/schema.rs | 120 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 267 insertions(+) create mode 100644 tolstoy/Cargo.toml create mode 100644 tolstoy/src/lib.rs create mode 100644 tolstoy/src/metadata.rs create mode 100644 tolstoy/src/schema.rs diff --git a/Cargo.toml b/Cargo.toml index 161e53ca..388b8c7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,5 +66,8 @@ path = "tx" [dependencies.mentat_tx_parser] path = "tx-parser" +[dependencies.mentat_tolstoy] +path = "tolstoy" + [profile.release] debug = true diff --git a/edn/src/lib.rs b/edn/src/lib.rs index 0e067993..6a0130b8 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -33,6 +33,7 @@ pub use uuid::Uuid; // Export from our modules. pub use parse::ParseError; +pub use uuid::ParseError as UuidParseError; pub use types::{ FromMicros, Span, diff --git a/tolstoy/Cargo.toml b/tolstoy/Cargo.toml new file mode 100644 index 00000000..03f57bef --- /dev/null +++ b/tolstoy/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "mentat_tolstoy" +version = "0.0.1" +workspace = ".." +authors = ["Grisha Kruglov "] + +[dependencies] +futures = "0.1" +hyper = "0.11" +tokio-core = "0.1" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" +lazy_static = "0.2" +uuid = { version = "0.5", features = ["v4", "serde"] } + +error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" } + +[dependencies.mentat_db] +path = "../db" + +[dependencies.edn] +path = "../edn" + +[dependencies.rusqlite] +version = "0.12" +# System sqlite might be very old. +features = ["bundled", "limits"] diff --git a/tolstoy/src/lib.rs b/tolstoy/src/lib.rs new file mode 100644 index 00000000..f7b70e18 --- /dev/null +++ b/tolstoy/src/lib.rs @@ -0,0 +1,41 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +#[macro_use] +extern crate error_chain; + +#[macro_use] +extern crate lazy_static; + +extern crate hyper; +extern crate tokio_core; +extern crate futures; +extern crate serde; +extern crate serde_json; +extern crate mentat_db; +extern crate rusqlite; +extern crate edn; +extern crate uuid; + +pub mod schema; +pub mod metadata; + +error_chain! { + types { + Error, ErrorKind, ResultExt, Result; + } + + foreign_links { + IOError(std::io::Error); + HttpError(hyper::Error); + SqlError(rusqlite::Error); + UuidParseError(edn::UuidParseError); + } +} diff --git a/tolstoy/src/metadata.rs b/tolstoy/src/metadata.rs new file mode 100644 index 00000000..2dc26121 --- /dev/null +++ b/tolstoy/src/metadata.rs @@ -0,0 +1,74 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +#![allow(dead_code)] + +use rusqlite; +use uuid::Uuid; + +use schema; +use Result; + +trait HeadTrackable { + fn remote_head(&self) -> Result; + fn set_remote_head(&mut self, uuid: &Uuid) -> Result<()>; +} + +struct SyncMetadataClient { + conn: rusqlite::Connection +} + +impl SyncMetadataClient { + fn new(conn: rusqlite::Connection) -> Self { + SyncMetadataClient { + conn: conn + } + } +} + +impl HeadTrackable for SyncMetadataClient { + fn remote_head(&self) -> Result { + self.conn.query_row( + "SELECT value FROM tolstoy_metadata WHERE key = ?", + &[&schema::REMOTE_HEAD_KEY], |r| { + let bytes: Vec = r.get(0); + Uuid::from_bytes(bytes.as_slice()) + } + )?.map_err(|e| e.into()) + } + + fn set_remote_head(&mut self, uuid: &Uuid) -> Result<()> { + let tx = self.conn.transaction()?; + let uuid_bytes = uuid.as_bytes().to_vec(); + tx.execute("UPDATE tolstoy_metadata SET value = ? WHERE key = ?", &[&uuid_bytes, &schema::REMOTE_HEAD_KEY])?; + tx.commit().map_err(|e| e.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_remote_head_default() { + let conn = schema::tests::setup_conn(); + let metadata_client: SyncMetadataClient = SyncMetadataClient::new(conn); + assert_eq!(Uuid::nil(), metadata_client.remote_head().expect("fetch succeeded")); + } + + #[test] + fn test_set_and_get_remote_head() { + let conn = schema::tests::setup_conn(); + let uuid = Uuid::new_v4(); + let mut metadata_client: SyncMetadataClient = SyncMetadataClient::new(conn); + metadata_client.set_remote_head(&uuid).expect("update succeeded"); + assert_eq!(uuid, metadata_client.remote_head().expect("fetch succeeded")); + } +} diff --git a/tolstoy/src/schema.rs b/tolstoy/src/schema.rs new file mode 100644 index 00000000..a7b5f9a6 --- /dev/null +++ b/tolstoy/src/schema.rs @@ -0,0 +1,120 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use rusqlite; +use Result; + +pub static REMOTE_HEAD_KEY: &str = r#"remote_head"#; + +lazy_static! { + /// SQL statements to be executed, in order, to create the Tolstoy SQL schema (version 1). + #[cfg_attr(rustfmt, rustfmt_skip)] + static ref SCHEMA_STATEMENTS: Vec<&'static str> = { vec![ + r#"CREATE TABLE IF NOT EXISTS tolstoy_tu (tx INTEGER PRIMARY KEY, uuid BLOB NOT NULL UNIQUE) WITHOUT ROWID"#, + r#"CREATE TABLE IF NOT EXISTS tolstoy_metadata (key BLOB NOT NULL UNIQUE, value BLOB NOT NULL)"#, + r#"CREATE INDEX IF NOT EXISTS idx_tolstoy_tu_ut ON tolstoy_tu (uuid, tx)"#, + ] + }; +} + +pub fn ensure_current_version(conn: &mut rusqlite::Connection) -> Result<()> { + let tx = conn.transaction()?; + + for statement in (&SCHEMA_STATEMENTS).iter() { + tx.execute(statement, &[])?; + } + + tx.execute("INSERT OR IGNORE INTO tolstoy_metadata (key, value) VALUES (?, zeroblob(16))", &[&REMOTE_HEAD_KEY])?; + tx.commit().map_err(|e| e.into()) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use uuid::Uuid; + + fn setup_conn_bare() -> rusqlite::Connection { + let conn = rusqlite::Connection::open_in_memory().unwrap(); + + conn.execute_batch(" + PRAGMA page_size=32768; + PRAGMA journal_mode=wal; + PRAGMA wal_autocheckpoint=32; + PRAGMA journal_size_limit=3145728; + PRAGMA foreign_keys=ON; + ").expect("success"); + + conn + } + + pub fn setup_conn() -> rusqlite::Connection { + let mut conn = setup_conn_bare(); + ensure_current_version(&mut conn).expect("connection setup"); + conn + } + + #[test] + fn test_empty() { + let mut conn = setup_conn_bare(); + + assert!(ensure_current_version(&mut conn).is_ok()); + + let mut stmt = conn.prepare("SELECT key FROM tolstoy_metadata WHERE value = zeroblob(16)").unwrap(); + let mut keys_iter = stmt.query_map(&[], |r| r.get(0)).expect("query works"); + + let first: Result = keys_iter.next().unwrap().map_err(|e| e.into()); + let second: Option<_> = keys_iter.next(); + match (first, second) { + (Ok(key), None) => { + assert_eq!(key, REMOTE_HEAD_KEY); + }, + (_, _) => { panic!("Wrong number of results."); }, + } + } + + #[test] + fn test_non_empty() { + let mut conn = setup_conn_bare(); + + assert!(ensure_current_version(&mut conn).is_ok()); + + let test_uuid = Uuid::new_v4(); + { + let tx = conn.transaction().unwrap(); + let uuid_bytes = test_uuid.as_bytes().to_vec(); + match tx.execute("UPDATE tolstoy_metadata SET value = ? WHERE key = ?", &[&uuid_bytes, &REMOTE_HEAD_KEY]) { + Err(e) => panic!("Error running an update: {}", e), + _ => () + } + match tx.commit() { + Err(e) => panic!("Error committing an update: {}", e), + _ => () + } + } + + assert!(ensure_current_version(&mut conn).is_ok()); + + // Check that running ensure_current_version on an initialized conn doesn't change anything. + let mut stmt = conn.prepare("SELECT value FROM tolstoy_metadata").unwrap(); + let mut values_iter = stmt.query_map(&[], |r| { + let raw_uuid: Vec = r.get(0); + Uuid::from_bytes(raw_uuid.as_slice()).unwrap() + }).expect("query works"); + + let first: Result = values_iter.next().unwrap().map_err(|e| e.into()); + let second: Option<_> = values_iter.next(); + match (first, second) { + (Ok(uuid), None) => { + assert_eq!(test_uuid, uuid); + }, + (_, _) => { panic!("Wrong number of results."); }, + } + } +}