diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..4fffb2f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 00000000..9588f406
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "store"
+version = "0.1.0"
+edition = "2021"
+
+
+[dependencies]
+utils = { path = "../utils" }
+rand = "0.8.5"
+roaring = "0.10.1"
+rocksdb = "0.20.1"
+serde = { version = "1.0", features = ["derive"]}
+ahash = { version = "0.8.0", features = ["serde"] }
+bitpacking = "0.8.4"
+lazy_static = "1.4"
+whatlang = "0.16" # Language detection
+rust-stemmers = "1.2" # Stemmers
+tinysegmenter = "0.1" # Japanese tokenizer
+jieba-rs = "0.6" # Chinese stemmer
diff --git a/pepe.toml b/pepe.toml
new file mode 100644
index 00000000..5264166f
--- /dev/null
+++ b/pepe.toml
@@ -0,0 +1,92 @@
+I have the following SQLite table for storing email data:
+
+CREATE TABLE email (
+ email_id INTEGER PRIMARY KEY,
+ blob_id TEXT NOT NULL,
+ thread_id INTEGER NOT NULL,
+ size INTEGER NOT NULL,
+ received_at TIMESTAMP NOT NULL,
+ message_id TEXT NOT NULL,
+ in_reply_to TEXT NOT NULL,
+ sender TEXT NOT NULL,
+ from TEXT NOT NULL,
+ to TEXT NOT NULL,
+ cc TEXT NOT NULL,
+ bcc TEXT NOT NULL,
+ reply_to TEXT NOT NULL,
+ subject TEXT NOT NULL,
+ sent_at TIMESTAMP NOT NULL,
+ has_attachment BOOL NOT NULL,
+ preview TEXT NOT NULL
+);
+
+The mailboxes and keywords for each message are stored in separate tables:
+
+CREATE TABLE email_mailbox (
+ email_id INTEGER PRIMARY KEY,
+ mailbox_id INTEGER NOT NULL,
+)
+
+CREATE TABLE email_keyword (
+ email_id INTEGER PRIMARY KEY,
+ keyword TEXT NOT NULL
+);
+
+How would you write a SQLite query to list the email IDs of all the Emails with the subject "sales" sorted by messages that belong to the same Thread and have a the keyword "draft", then sorted by received at and then has attachment.
+
+
+[email]
+id: INT
+blob_id: HASH
+thread_id: INT
+size: INT
+received_at: TIMESTAMP
+message_id: TEXT
+in_reply_to: TEXT
+sender: TEXT
+from: TEXT
+to: TEXT
+cc: TEXT
+bcc: TEXT
+reply_to: TEXT
+subject: TEXT
+sent_at: TIMESTAMP
+has_attachment: BOOL
+preview: TEXT
+
+[email_mailbox]
+email_id: INT
+mailbox_id: INT
+imap_uid: INT
+
+[email_keyword]
+email_id: INT
+keyword: TEXT
+
+
+/*
+
+ o id
+ o blobId
+ o threadId
+ o mailboxIds
+ o keywords
+ o size
+ o receivedAt
+ o messageId
+ o inReplyTo
+ o sender
+ o from
+ o to
+ o cc
+ o bcc
+ o replyTo
+ o subject
+ o sentAt
+ o hasAttachment
+ o preview
+
+[ "partId", "blobId", "size", "name", "type", "charset",
+ "disposition", "cid", "language", "location" ]
+
+*/
\ No newline at end of file
diff --git a/src/backend/foundationdb/mod.rs b/src/backend/foundationdb/mod.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
new file mode 100644
index 00000000..54822ba1
--- /dev/null
+++ b/src/backend/mod.rs
@@ -0,0 +1,2 @@
+pub mod foundationdb;
+pub mod rocksdb;
diff --git a/src/backend/rocksdb/bitmap.rs b/src/backend/rocksdb/bitmap.rs
new file mode 100644
index 00000000..e355701a
--- /dev/null
+++ b/src/backend/rocksdb/bitmap.rs
@@ -0,0 +1,326 @@
+/*
+ * Copyright (c) 2020-2022, Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart JMAP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+use crate::{Deserialize, Serialize};
+use roaring::RoaringBitmap;
+use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
+
+pub const BIT_SET: u8 = 0x80;
+pub const BIT_CLEAR: u8 = 0;
+
+pub const IS_BITLIST: u8 = 0;
+pub const IS_BITMAP: u8 = 1;
+
+#[inline(always)]
+pub fn deserialize_bitlist(bm: &mut RoaringBitmap, bytes: &[u8]) {
+ let mut it = bytes[1..].iter();
+
+ 'inner: while let Some(header) = it.next() {
+ let mut items = (header & 0x7F) + 1;
+ let is_set = (header & BIT_SET) != 0;
+
+ while items > 0 {
+ if let Some(doc_id) = it.next_leb128() {
+ if is_set {
+ bm.insert(doc_id);
+ } else {
+ bm.remove(doc_id);
+ }
+ items -= 1;
+ } else {
+ debug_assert!(items == 0, "{:?}", bytes);
+ break 'inner;
+ }
+ }
+ }
+}
+
+#[inline(always)]
+pub fn deserialize_bitmap(bytes: &[u8]) -> Option {
+ RoaringBitmap::deserialize_unchecked_from(&bytes[1..]).ok()
+}
+
+impl Deserialize for RoaringBitmap {
+ fn deserialize(bytes: &[u8]) -> Option {
+ match *bytes.first()? {
+ IS_BITMAP => deserialize_bitmap(bytes),
+ IS_BITLIST => {
+ let mut bm = RoaringBitmap::new();
+ deserialize_bitlist(&mut bm, bytes);
+ Some(bm)
+ }
+ _ => None,
+ }
+ }
+}
+
+impl Serialize for RoaringBitmap {
+ fn serialize(self) -> Vec {
+ let mut bytes = Vec::with_capacity(self.serialized_size() + 1);
+ bytes.push(IS_BITMAP);
+ let _ = self.serialize_into(&mut bytes);
+ bytes
+ }
+}
+
+macro_rules! impl_bit {
+ ($single:ident, $many:ident, $flag:ident) => {
+ #[inline(always)]
+ pub fn $single(document: u32) -> Vec {
+ let mut buf = Vec::with_capacity(std::mem::size_of::() + 2);
+ buf.push(IS_BITLIST);
+ buf.push($flag);
+ buf.push_leb128(document);
+ buf
+ }
+
+ #[inline(always)]
+ pub fn $many(documents: T) -> Vec
+ where
+ T: Iterator- ,
+ {
+ debug_assert!(documents.size_hint().0 > 0);
+
+ let mut buf = Vec::with_capacity(
+ ((std::mem::size_of::() + 1)
+ * documents
+ .size_hint()
+ .1
+ .unwrap_or_else(|| documents.size_hint().0))
+ + 2,
+ );
+
+ buf.push(IS_BITLIST);
+
+ let mut header_pos = 0;
+ let mut total_docs = 0;
+
+ for (pos, document) in documents.enumerate() {
+ if pos & 0x7F == 0 {
+ header_pos = buf.len();
+ buf.push($flag | 0x7F);
+ }
+ buf.push_leb128(document);
+ total_docs = pos;
+ }
+
+ buf[header_pos] = $flag | ((total_docs & 0x7F) as u8);
+
+ buf
+ }
+ };
+}
+
+impl_bit!(set_bit, set_bits, BIT_SET);
+impl_bit!(clear_bit, clear_bits, BIT_CLEAR);
+
+#[inline(always)]
+pub fn set_clear_bits(documents: T) -> Vec
+where
+ T: Iterator
- ,
+{
+ debug_assert!(documents.size_hint().0 > 0);
+
+ let total_docs = documents
+ .size_hint()
+ .1
+ .unwrap_or_else(|| documents.size_hint().0);
+ let buf_len = (std::mem::size_of::() * total_docs) + (total_docs / 0x7F) + 2;
+ let mut set_buf = Vec::with_capacity(buf_len);
+ let mut clear_buf = Vec::with_capacity(buf_len);
+
+ let mut set_header_pos = 0;
+ let mut set_total_docs = 0;
+
+ let mut clear_header_pos = 0;
+ let mut clear_total_docs = 0;
+
+ set_buf.push(IS_BITLIST);
+ clear_buf.push(IS_BITLIST);
+
+ for (document, is_set) in documents {
+ if is_set {
+ if set_total_docs & 0x7F == 0 {
+ set_header_pos = set_buf.len();
+ set_buf.push(BIT_SET | 0x7F);
+ }
+ set_buf.push_leb128(document);
+ set_total_docs += 1;
+ } else {
+ if clear_total_docs & 0x7F == 0 {
+ clear_header_pos = clear_buf.len();
+ clear_buf.push(BIT_CLEAR | 0x7F);
+ }
+ clear_buf.push_leb128(document);
+ clear_total_docs += 1;
+ }
+ }
+
+ if set_total_docs > 0 {
+ set_buf[set_header_pos] = BIT_SET | (((set_total_docs - 1) & 0x7F) as u8);
+ }
+
+ if clear_total_docs > 0 {
+ clear_buf[clear_header_pos] = BIT_CLEAR | (((clear_total_docs - 1) & 0x7F) as u8);
+ }
+
+ if set_total_docs > 0 && clear_total_docs > 0 {
+ set_buf.extend_from_slice(&clear_buf[1..]);
+ set_buf
+ } else if set_total_docs > 0 {
+ set_buf
+ } else {
+ clear_buf
+ }
+}
+
+#[inline(always)]
+pub fn bitmap_merge<'x>(
+ existing_val: Option<&[u8]>,
+ operands_len: usize,
+ operands: impl IntoIterator
- ,
+) -> Option> {
+ let mut bm = match existing_val {
+ Some(existing_val) => RoaringBitmap::deserialize(existing_val)?,
+ None if operands_len == 1 => {
+ return Some(Vec::from(operands.into_iter().next().unwrap()));
+ }
+ _ => RoaringBitmap::new(),
+ };
+
+ for op in operands.into_iter() {
+ match *op.first()? {
+ IS_BITMAP => {
+ if let Some(union_bm) = deserialize_bitmap(op) {
+ if !bm.is_empty() {
+ bm |= union_bm;
+ } else {
+ bm = union_bm;
+ }
+ } else {
+ debug_assert!(false, "Failed to deserialize bitmap.");
+ return None;
+ }
+ }
+ IS_BITLIST => {
+ deserialize_bitlist(&mut bm, op);
+ }
+ _ => {
+ debug_assert!(false, "This should not have happend");
+ return None;
+ }
+ }
+ }
+
+ let mut bytes = Vec::with_capacity(bm.serialized_size() + 1);
+ bytes.push(IS_BITMAP);
+ bm.serialize_into(&mut bytes).ok()?;
+ Some(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+
+ #[test]
+ fn merge_bitmaps() {
+ let v1 = set_clear_bits([(1, true), (2, true), (3, false), (4, true)].into_iter());
+ let v2 = set_clear_bits([(1, false), (4, false)].into_iter());
+ let v3 = set_clear_bits([(5, true)].into_iter());
+ assert_eq!(
+ RoaringBitmap::from_iter([1, 2, 4]),
+ RoaringBitmap::deserialize(&v1).unwrap()
+ );
+ assert_eq!(
+ RoaringBitmap::from_iter([1, 2, 4]),
+ RoaringBitmap::deserialize(&bitmap_merge(None, 1, [v1.as_ref()]).unwrap()).unwrap()
+ );
+ assert_eq!(
+ RoaringBitmap::from_iter([2]),
+ RoaringBitmap::deserialize(&bitmap_merge(None, 2, [v1.as_ref(), v2.as_ref()]).unwrap())
+ .unwrap()
+ );
+ assert_eq!(
+ RoaringBitmap::from_iter([2, 5]),
+ RoaringBitmap::deserialize(
+ &bitmap_merge(None, 3, [v1.as_ref(), v2.as_ref(), v3.as_ref()]).unwrap()
+ )
+ .unwrap()
+ );
+ assert_eq!(
+ RoaringBitmap::from_iter([2, 5]),
+ RoaringBitmap::deserialize(
+ &bitmap_merge(Some(v1.as_ref()), 2, [v2.as_ref(), v3.as_ref()]).unwrap()
+ )
+ .unwrap()
+ );
+ assert_eq!(
+ RoaringBitmap::from_iter([5]),
+ RoaringBitmap::deserialize(&bitmap_merge(Some(v2.as_ref()), 1, [v3.as_ref()]).unwrap())
+ .unwrap()
+ );
+
+ assert_eq!(
+ RoaringBitmap::from_iter([1, 2, 4]),
+ RoaringBitmap::deserialize(
+ &bitmap_merge(
+ Some(RoaringBitmap::from_iter([1, 2, 3, 4]).serialize().as_ref()),
+ 1,
+ [v1.as_ref()]
+ )
+ .unwrap()
+ )
+ .unwrap()
+ );
+
+ assert_eq!(
+ RoaringBitmap::from_iter([1, 2, 3, 4, 5, 6]),
+ RoaringBitmap::deserialize(
+ &bitmap_merge(
+ Some(RoaringBitmap::from_iter([1, 2, 3, 4]).serialize().as_ref()),
+ 1,
+ [RoaringBitmap::from_iter([5, 6]).serialize().as_ref()]
+ )
+ .unwrap()
+ )
+ .unwrap()
+ );
+
+ assert_eq!(
+ RoaringBitmap::from_iter([1, 2, 4, 5, 6]),
+ RoaringBitmap::deserialize(
+ &bitmap_merge(
+ Some(RoaringBitmap::from_iter([1, 2, 3, 4]).serialize().as_ref()),
+ 2,
+ [
+ RoaringBitmap::from_iter([5, 6]).serialize().as_ref(),
+ v1.as_ref()
+ ]
+ )
+ .unwrap()
+ )
+ .unwrap()
+ );
+ }
+}
diff --git a/src/backend/rocksdb/main.rs b/src/backend/rocksdb/main.rs
new file mode 100644
index 00000000..750fcbb0
--- /dev/null
+++ b/src/backend/rocksdb/main.rs
@@ -0,0 +1,135 @@
+use std::path::PathBuf;
+
+use roaring::RoaringBitmap;
+use rocksdb::{ColumnFamilyDescriptor, MergeOperands, OptimisticTransactionDB, Options};
+
+use crate::{Deserialize, Error, Store};
+
+use super::{CF_BITMAPS, CF_BLOBS, CF_INDEXES, CF_LOGS, CF_VALUES};
+
+impl Store {
+ pub fn open() -> crate::Result {
+ // Create the database directory if it doesn't exist
+ let path = PathBuf::from(
+ "/tmp/rocksdb.test", /*&settings
+ .get("db-path")
+ .unwrap_or_else(|| "/usr/local/stalwart-jmap/data".to_string())*/
+ );
+ let mut idx_path = path;
+ idx_path.push("idx");
+ std::fs::create_dir_all(&idx_path).map_err(|err| {
+ Error::InternalError(format!(
+ "Failed to create index directory {}: {:?}",
+ idx_path.display(),
+ err
+ ))
+ })?;
+
+ // Bitmaps
+ let cf_bitmaps = {
+ let mut cf_opts = Options::default();
+ //cf_opts.set_max_write_buffer_number(16);
+ cf_opts.set_merge_operator("merge", bitmap_merge, bitmap_partial_merge);
+ cf_opts.set_compaction_filter("compact", bitmap_compact);
+ ColumnFamilyDescriptor::new(CF_BITMAPS, cf_opts)
+ };
+
+ // Stored values
+ let cf_values = {
+ let mut cf_opts = Options::default();
+ cf_opts.set_merge_operator_associative("merge", numeric_value_merge);
+ ColumnFamilyDescriptor::new(CF_VALUES, cf_opts)
+ };
+
+ // Secondary indexes
+ let cf_indexes = {
+ let cf_opts = Options::default();
+ ColumnFamilyDescriptor::new(CF_INDEXES, cf_opts)
+ };
+
+ // Blobs
+ let cf_blobs = {
+ let mut cf_opts = Options::default();
+ cf_opts.set_enable_blob_files(true);
+ cf_opts.set_min_blob_size(
+ 16834, /*settings.parse("blob-min-size").unwrap_or(16384) */
+ );
+ ColumnFamilyDescriptor::new(CF_BLOBS, cf_opts)
+ };
+
+ // Raft log and change log
+ let cf_log = {
+ let cf_opts = Options::default();
+ ColumnFamilyDescriptor::new(CF_LOGS, cf_opts)
+ };
+
+ let mut db_opts = Options::default();
+ db_opts.create_missing_column_families(true);
+ db_opts.create_if_missing(true);
+
+ Ok(Store {
+ db: OptimisticTransactionDB::open_cf_descriptors(
+ &db_opts,
+ idx_path,
+ vec![cf_bitmaps, cf_values, cf_indexes, cf_blobs, cf_log],
+ )
+ .map_err(|e| Error::InternalError(e.into_string()))?,
+ })
+ }
+
+ pub fn close(&self) -> crate::Result<()> {
+ self.db
+ .flush()
+ .map_err(|e| Error::InternalError(e.to_string()))?;
+ self.db.cancel_all_background_work(true);
+ Ok(())
+ }
+}
+
+pub fn numeric_value_merge(
+ _key: &[u8],
+ value: Option<&[u8]>,
+ operands: &MergeOperands,
+) -> Option> {
+ let mut value = if let Some(value) = value {
+ i64::from_le_bytes(value.try_into().ok()?)
+ } else {
+ 0
+ };
+
+ for op in operands.iter() {
+ value += i64::from_le_bytes(op.try_into().ok()?);
+ }
+
+ let mut bytes = Vec::with_capacity(std::mem::size_of::());
+ bytes.extend_from_slice(&value.to_le_bytes());
+ Some(bytes)
+}
+
+pub fn bitmap_merge(
+ _new_key: &[u8],
+ existing_val: Option<&[u8]>,
+ operands: &MergeOperands,
+) -> Option> {
+ super::bitmap::bitmap_merge(existing_val, operands.len(), operands.into_iter())
+}
+
+pub fn bitmap_partial_merge(
+ _new_key: &[u8],
+ _existing_val: Option<&[u8]>,
+ _operands: &MergeOperands,
+) -> Option> {
+ // Force a full merge
+ None
+}
+
+pub fn bitmap_compact(
+ _level: u32,
+ _key: &[u8],
+ value: &[u8],
+) -> rocksdb::compaction_filter::Decision {
+ match RoaringBitmap::deserialize(value) {
+ Some(bm) if bm.is_empty() => rocksdb::compaction_filter::Decision::Remove,
+ _ => rocksdb::compaction_filter::Decision::Keep,
+ }
+}
diff --git a/src/backend/rocksdb/mod.rs b/src/backend/rocksdb/mod.rs
new file mode 100644
index 00000000..47b544fe
--- /dev/null
+++ b/src/backend/rocksdb/mod.rs
@@ -0,0 +1,50 @@
+use crate::{write::key::KeySerializer, BitmapKey, IndexKey, Serialize, ValueKey};
+
+pub mod bitmap;
+pub mod main;
+pub mod read;
+
+pub const CF_BITMAPS: &str = "b";
+pub const CF_VALUES: &str = "v";
+pub const CF_LOGS: &str = "l";
+pub const CF_BLOBS: &str = "o";
+pub const CF_INDEXES: &str = "i";
+
+pub const COLLECTION_PREFIX_LEN: usize = std::mem::size_of::() + std::mem::size_of::();
+pub const FIELD_PREFIX_LEN: usize = COLLECTION_PREFIX_LEN + std::mem::size_of::();
+pub const ACCOUNT_KEY_LEN: usize =
+ std::mem::size_of::() + std::mem::size_of::() + std::mem::size_of::();
+
+impl Serialize for IndexKey<'_> {
+ fn serialize(self) -> Vec {
+ KeySerializer::new(std::mem::size_of::() + self.key.len())
+ .write(self.account_id)
+ .write(self.collection)
+ .write(self.field)
+ .write(self.key)
+ .finalize()
+ }
+}
+
+impl Serialize for ValueKey {
+ fn serialize(self) -> Vec {
+ KeySerializer::new(std::mem::size_of::())
+ .write_leb128(self.account_id)
+ .write(self.collection)
+ .write_leb128(self.document_id)
+ .write(self.field)
+ .finalize()
+ }
+}
+
+impl Serialize for BitmapKey<'_> {
+ fn serialize(self) -> Vec {
+ KeySerializer::new(std::mem::size_of::() + self.key.len())
+ .write(self.key)
+ .write(self.field)
+ .write(self.collection)
+ .write(self.family)
+ .write_leb128(self.account_id)
+ .finalize()
+ }
+}
diff --git a/src/backend/rocksdb/read.rs b/src/backend/rocksdb/read.rs
new file mode 100644
index 00000000..b119a460
--- /dev/null
+++ b/src/backend/rocksdb/read.rs
@@ -0,0 +1,217 @@
+use std::ops::{BitAndAssign, BitOrAssign};
+
+use roaring::RoaringBitmap;
+use rocksdb::{Direction, IteratorMode};
+
+use crate::{
+ query::Operator, write::key::DeserializeBigEndian, BitmapKey, Deserialize, Error, IndexKey,
+ Serialize, Store, ValueKey, BM_DOCUMENT_IDS,
+};
+
+use super::{CF_BITMAPS, CF_INDEXES, CF_VALUES, FIELD_PREFIX_LEN};
+
+impl Store {
+ #[inline(always)]
+ pub fn get_value(&self, key: ValueKey) -> crate::Result