├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── benches └── bench.rs └── src ├── cmp.rs ├── idx.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | # can't support release channels due to #![feature] 4 | # - stable 5 | # - beta 6 | - nightly 7 | cache: cargo 8 | matrix: 9 | allow_failures: 10 | - rust: nightly 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shortcut" 3 | version = "4.1.3" 4 | 5 | description = "an indexed, queryable column-based storage system" 6 | readme = "README.md" 7 | 8 | authors = ["Jon Gjengset "] 9 | 10 | documentation = "https://docs.rs/shortcut" 11 | homepage = "https://github.com/jonhoo/shortcut" 12 | repository = "https://github.com/jonhoo/shortcut.git" 13 | 14 | keywords = ["storage","indexing","query","database"] 15 | categories = ["database-implementations", "data-structures"] 16 | 17 | license = "MIT/Apache-2.0" 18 | 19 | [badges] 20 | travis-ci = { repository = "jonhoo/shortcut" } 21 | maintenance = { status = "as-is" } 22 | 23 | [dependencies] 24 | 25 | [dev-dependencies] 26 | docopt = "0.6" 27 | time = "0.1" 28 | 29 | [[bench]] 30 | name = "bench" 31 | path = "benches/bench.rs" 32 | harness = false 33 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jon Gjengset 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shortcut 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/shortcut.svg)](https://crates.io/crates/shortcut) 4 | [![Documentation](https://docs.rs/shortcut/badge.svg)](https://docs.rs/shortcut/) 5 | [![Build Status](https://travis-ci.org/jonhoo/shortcut.svg?branch=master)](https://travis-ci.org/jonhoo/shortcut) 6 | 7 | This crate provides an indexed, queryable column-based storage system. 8 | 9 | The storage system is, fundamentally, row-based storage, where all rows have the same number of 10 | columns. All columns are the same "type", but given that they can be enum types, you can 11 | effectively use differently typed values. Data is stored in a `BTreeMap>`, 12 | where the outermost `BTreeMap` is dynamically sized (and may be re-allocated as more rows come 13 | in), whereas the innermost `Vec` is expected to never change. The map index is an 14 | autoincremented row identifier similar to the one used by SQLite: 15 | https://www.sqlite.org/lang_createtable.html#rowid. 16 | 17 | What makes this crate interesting is that it also allows you to place indices on columns for 18 | fast lookups. These indices are automatically updated whenever the dataset changes, so that 19 | queries continue to return correct results. Indices should conform to either the 20 | `EqualityIndex` trait or the `RangeIndex` trait. As you would expect, the former allows 21 | speeding up exact lookups, whereas the latter can also perform efficient range queries. 22 | 23 | Queries are performed over the dataset by calling `find` with a set of `Condition`s that will 24 | be `AND`ed together. `OR` is currently not supported --- issue multiple quieries instead. Each 25 | `Condition` represents a value comparison against the value in a single column. The system 26 | automatically picks what index to use to satisfy the query, using a heuristic based on the 27 | expected number of rows returned for that column for each index. 28 | 29 | ## Known limitations 30 | 31 | - The set of match operations is currently fairly limited. 32 | - The system currently provides an add/remove-only abstraction (i.e., no edit). 33 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/shortcut.svg)](https://crates.io/crates/shortcut) 4 | [![Documentation](https://docs.rs/shortcut/badge.svg)](https://docs.rs/shortcut/) 5 | [![Build Status](https://travis-ci.org/jonhoo/shortcut.svg?branch=master)](https://travis-ci.org/jonhoo/shortcut) 6 | 7 | {{readme}} 8 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | // Basic microbenchmark. 4 | // 5 | // Usage: 6 | // 7 | // $ cargo bench --bench bench -- --use-index [...] 8 | // 9 | 10 | extern crate docopt; 11 | extern crate shortcut; 12 | extern crate test; 13 | extern crate time; 14 | 15 | use std::borrow::Cow; 16 | 17 | use docopt::Docopt; 18 | use shortcut::cmp; 19 | use shortcut::idx; 20 | use shortcut::Store; 21 | use time::PreciseTime; 22 | 23 | const USAGE: &'static str = " 24 | Benchmark shortcut. 25 | 26 | Usage: 27 | bench [--rounds=N --use-index --bench] 28 | 29 | Options: 30 | --rounds=N Number of rounds to run. [default: 1000000] 31 | --use-index Install a hash index for fast lookups. 32 | --bench Appease `cargo bench`. No effect. 33 | "; 34 | 35 | fn main() { 36 | let args = Docopt::new(USAGE) 37 | .and_then(|dopt| dopt.parse()) 38 | .unwrap_or_else(|e| e.exit()); 39 | 40 | let mut store = Store::new(2); 41 | 42 | let rounds: u32 = args.get_str("--rounds").parse().unwrap(); 43 | 44 | if args.get_bool("--use-index") { 45 | store.index(0, idx::HashIndex::new()); 46 | } 47 | 48 | let t0 = PreciseTime::now(); 49 | 50 | // Put. 51 | for i in 0..rounds { 52 | let istr = format!("{}", i); 53 | store.insert(vec![istr.clone(), istr]) 54 | } 55 | 56 | let t1 = PreciseTime::now(); 57 | 58 | // Get. 59 | for i in 0..rounds { 60 | let cmp = [cmp::Condition { 61 | column: 0, 62 | cmp: cmp::Comparison::Equal(cmp::Value::Const(Cow::Owned(format!("{}", i)))), 63 | }]; 64 | 65 | let rows = store.find(&cmp); 66 | 67 | for row in rows { 68 | test::black_box(row); 69 | } 70 | } 71 | 72 | let t2 = PreciseTime::now(); 73 | 74 | println!( 75 | "put time: {:.2}ms ({:.2} puts/sec)", 76 | t0.to(t1).num_milliseconds(), 77 | ops_per_sec(rounds, t0, t1) 78 | ); 79 | 80 | println!( 81 | "get time: {:.2}ms ({:.2} gets/sec)", 82 | t1.to(t2).num_milliseconds(), 83 | ops_per_sec(rounds, t1, t2) 84 | ); 85 | } 86 | 87 | fn ops_per_sec(rounds: u32, start: PreciseTime, end: PreciseTime) -> f64 { 88 | 1000.0 * (rounds as f64) / (start.to(end).num_milliseconds() as f64) 89 | } 90 | -------------------------------------------------------------------------------- /src/cmp.rs: -------------------------------------------------------------------------------- 1 | use Row; 2 | use std::fmt; 3 | use std::borrow::Cow; 4 | use std::borrow::Borrow; 5 | 6 | /// A value represents something to compare against. 7 | #[derive(Clone, Debug)] 8 | pub enum Value<'a, T: Clone + 'a> { 9 | /// A constant value literal. 10 | Const(Cow<'a, T>), 11 | 12 | /// A different column for the same row. Note that comparisons of this kind *cannot use an 13 | /// index*, at least not in the current implementation. 14 | Column(usize), 15 | } 16 | 17 | impl<'a, T: Clone + 'a> Value<'a, T> { 18 | /// Extract the value literal for this `Value` when evaluated for the given row. 19 | /// For `Const` values, this evaluates to the `Const` value itself. For `Column`, it evaluates 20 | /// to the value of that column in the given row. 21 | pub fn value<'b: 'a, R: Row + ?Sized>(&'b self, row: &'b R) -> &'b T { 22 | match *self { 23 | Value::Column(i) => &row.index(i), 24 | Value::Const(ref val) => val, 25 | } 26 | } 27 | 28 | /// Construct a new `Value` by moving an existing value. 29 | pub fn new>(t: I) -> Self { 30 | Value::Const(Cow::Owned(t.into())) 31 | } 32 | 33 | /// Construct a new `Value` by using a reference to an existing value. 34 | pub fn using>(t: &'a I) -> Self { 35 | Value::Const(Cow::Borrowed(t.borrow())) 36 | } 37 | 38 | /// Construct a new `Value` that refers to the value in a particular column of a row. 39 | pub fn column(c: usize) -> Self { 40 | Value::Column(c) 41 | } 42 | } 43 | 44 | /// A comparison to perform for a literal value against a `Value`. 45 | #[derive(Clone, Debug)] 46 | pub enum Comparison<'a, T: Clone + 'a> { 47 | /// Is the value equal to the given `Value`? 48 | Equal(Value<'a, T>), 49 | } 50 | 51 | impl<'a, T: Ord + Clone + 'a> Comparison<'a, T> { 52 | /// Returns true if the given value compares successfully against this `Value` when evaluated 53 | /// against the given row. 54 | pub fn matches + ?Sized>(&self, value: &T, row: &R) -> bool { 55 | match *self { 56 | Comparison::Equal(ref v) => value == v.value(row), 57 | } 58 | } 59 | } 60 | 61 | /// A single condition to evaluate for a row in the dataset. 62 | #[derive(Clone, Debug)] 63 | pub struct Condition<'a, T: Clone + 'a> { 64 | /// The column of the row to use as the comparison value. 65 | pub column: usize, 66 | 67 | /// The comparison to perform on the selected value. 68 | pub cmp: Comparison<'a, T>, 69 | } 70 | 71 | impl<'a, T: Ord + Clone + 'a> Condition<'a, T> { 72 | /// Returns true if this condition holds true for the given row. To determine if this is the 73 | /// case, `row[self.column]` is extracted, and is evaluated using the comparison in `self.cmp`. 74 | pub fn matches + ?Sized>(&self, row: &R) -> bool { 75 | self.cmp.matches(&row.index(self.column), row) 76 | } 77 | } 78 | 79 | impl<'a, T: fmt::Display + Clone + 'a> fmt::Display for Value<'a, T> { 80 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | match *self { 82 | Value::Column(i) => write!(f, "[{}]", i), 83 | Value::Const(ref val) => write!(f, "{}", val), 84 | } 85 | } 86 | } 87 | 88 | impl<'a, T: fmt::Display + Clone + 'a> fmt::Display for Comparison<'a, T> { 89 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 90 | match *self { 91 | Comparison::Equal(ref v) => write!(f, "= {}", v), 92 | } 93 | } 94 | } 95 | 96 | impl<'a, T: fmt::Display + Clone + 'a> fmt::Display for Condition<'a, T> { 97 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 | write!(f, "[{}] {}", self.column, self.cmp) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn value() { 108 | let a = &["a"]; 109 | let b = &["b"]; 110 | assert_eq!(Value::column(0).value(&a[..]), &"a"); 111 | assert_eq!(Value::new("a").value(&b[..]), &"a"); 112 | } 113 | 114 | #[test] 115 | fn cmp_eq() { 116 | let a = &["a"]; 117 | let b = &["b"]; 118 | assert!(Comparison::Equal(Value::column(0)).matches(&"a", &a[..])); 119 | assert!(!Comparison::Equal(Value::column(0)).matches(&"a", &b[..])); 120 | assert!(Comparison::Equal(Value::new("a")).matches(&"a", &b[..])); 121 | assert!(!Comparison::Equal(Value::new("b")).matches(&"a", &a[..])); 122 | } 123 | 124 | #[test] 125 | fn borrowed_values() { 126 | let a = vec!["a".to_string()]; 127 | let b = vec!["b".to_string()]; 128 | assert!(Comparison::Equal(Value::column(0)).matches(&a[0], &a)); 129 | assert!(!Comparison::Equal(Value::column(0)).matches(&a[0], &b)); 130 | assert!(Comparison::Equal(Value::using(&a[0])).matches(&a[0], &b)); 131 | assert!(!Comparison::Equal(Value::using(&b[0])).matches(&a[0], &a)); 132 | } 133 | 134 | #[test] 135 | fn through_deref() { 136 | let a = vec!["a".to_string()]; 137 | let b = vec!["b".to_string()]; 138 | assert!(Comparison::Equal(Value::column(0)).matches(&a[0], &a)); 139 | assert!(!Comparison::Equal(Value::column(0)).matches(&a[0], &b)); 140 | assert!(Comparison::Equal(Value::new("a")).matches(&a[0], &b)); 141 | assert!(!Comparison::Equal(Value::new("b")).matches(&a[0], &a)); 142 | } 143 | 144 | #[test] 145 | fn cond_eq() { 146 | let cmpf0 = Comparison::Equal(Value::column(0)); 147 | let cmpca = Comparison::Equal(Value::new("a")); 148 | let cmpcb = Comparison::Equal(Value::new("b")); 149 | 150 | let cf10 = Condition { 151 | column: 1, 152 | cmp: cmpf0, 153 | }; 154 | let cca = Condition { 155 | column: 0, 156 | cmp: cmpca, 157 | }; 158 | let ccb = Condition { 159 | column: 0, 160 | cmp: cmpcb, 161 | }; 162 | 163 | let a = &["a"]; 164 | let b = &["b"]; 165 | let aa = &["a", "a"]; 166 | let ab = &["a", "b"]; 167 | assert!(cf10.matches(&aa[..])); 168 | assert!(!cf10.matches(&ab[..])); 169 | assert!(cca.matches(&a[..])); 170 | assert!(!cca.matches(&b[..])); 171 | assert!(ccb.matches(&b[..])); 172 | assert!(!ccb.matches(&a[..])); 173 | } 174 | 175 | #[test] 176 | fn display() { 177 | let cf01: Condition = Condition { 178 | column: 0, 179 | cmp: Comparison::Equal(Value::Column(1)), 180 | }; 181 | 182 | let cca = Condition { 183 | column: 0, 184 | cmp: Comparison::Equal::<&str>(Value::new("a")), 185 | }; 186 | 187 | assert_eq!(format!("{}", cf01), "[0] = [1]"); 188 | assert_eq!(format!("{}", cca), "[0] = a") 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/idx.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::Hash; 3 | 4 | use std::collections::BTreeMap; 5 | use std::ops::Bound; 6 | 7 | /// An `EqualityIndex` is an index that can perform *efficient* equality lookups. 8 | pub trait EqualityIndex { 9 | /// Return an iterator that yields the indices of all rows that match the given value. 10 | fn lookup<'a>(&'a self, &T) -> Box + 'a>; 11 | 12 | /// Add the given row index to the index under the given value. 13 | fn index(&mut self, T, usize); 14 | 15 | /// Remove the given row index under the given value from the index. 16 | fn undex(&mut self, &T, usize); 17 | 18 | /// Give the expected number of rows returned for a key. 19 | /// This method may be called often, and in rapid succession, and so should return quickly. 20 | fn estimate(&self) -> usize; 21 | } 22 | 23 | /// An implementation of `EqualityIndex` that uses a `HashMap`. 24 | #[derive(Clone)] 25 | pub struct HashIndex { 26 | num: usize, 27 | map: HashMap>, 28 | } 29 | 30 | impl HashIndex { 31 | /// Allocate a new `HashIndex`. 32 | pub fn new() -> HashIndex { 33 | HashIndex { 34 | map: HashMap::new(), 35 | num: 0, 36 | } 37 | } 38 | } 39 | 40 | impl EqualityIndex for HashIndex { 41 | fn lookup<'a>(&'a self, key: &T) -> Box + 'a> { 42 | match self.map.get(key) { 43 | Some(ref v) => Box::new(v.iter().map(|row| *row)), 44 | None => Box::new(None.into_iter()), 45 | } 46 | } 47 | 48 | fn index(&mut self, key: T, row: usize) { 49 | self.map.entry(key).or_insert_with(Vec::new).push(row); 50 | self.num += 1; 51 | } 52 | 53 | fn undex(&mut self, key: &T, row: usize) { 54 | let mut empty = false; 55 | if let Some(mut l) = self.map.get_mut(key) { 56 | empty = { 57 | match l.iter().position(|&r| r == row) { 58 | Some(i) => { 59 | l.swap_remove(i); 60 | } 61 | None => unreachable!(), 62 | } 63 | l.is_empty() 64 | }; 65 | } 66 | if empty { 67 | self.map.remove(key); 68 | } 69 | } 70 | 71 | fn estimate(&self) -> usize { 72 | let len = self.map.len(); 73 | if len > 0 { 74 | self.num / self.map.len() 75 | } else { 76 | 0 77 | } 78 | } 79 | } 80 | 81 | /// A `RangeIndex` is an index that, in addition to performing efficient equality lookups, can 82 | /// *also* perform efficient range queries. 83 | pub trait RangeIndex: EqualityIndex { 84 | /// Return an iterator that yields the indices of all rows whose value (in the column this 85 | /// index is assigned to) lies within the given `Bound`s. 86 | fn between<'a>(&'a self, Bound<&T>, Bound<&T>) -> Box + 'a>; 87 | } 88 | 89 | /// An implementation of `RangeIndex` using a `BTreeMap`. 90 | #[derive(Clone)] 91 | pub struct BTreeIndex { 92 | num: usize, 93 | map: BTreeMap>, 94 | } 95 | 96 | impl BTreeIndex { 97 | /// Allocate a new `BTreeIndex`. 98 | pub fn new() -> BTreeIndex { 99 | BTreeIndex { 100 | map: BTreeMap::new(), 101 | num: 0, 102 | } 103 | } 104 | } 105 | 106 | impl EqualityIndex for BTreeIndex { 107 | fn lookup<'a>(&'a self, key: &T) -> Box + 'a> { 108 | match self.map.get(key) { 109 | Some(ref v) => Box::new(v.iter().map(|row| *row)), 110 | None => Box::new(None.into_iter()), 111 | } 112 | } 113 | 114 | fn index(&mut self, key: T, row: usize) { 115 | self.map.entry(key).or_insert_with(Vec::new).push(row); 116 | self.num += 1; 117 | } 118 | 119 | fn undex(&mut self, key: &T, row: usize) { 120 | if let Some(ref mut l) = self.map.get_mut(key) { 121 | self.num -= l.len(); 122 | l.retain(|&i| i != row); 123 | self.num += l.len(); 124 | } 125 | } 126 | 127 | fn estimate(&self) -> usize { 128 | self.num / self.map.len() 129 | } 130 | } 131 | impl RangeIndex for BTreeIndex { 132 | fn between<'a>(&'a self, min: Bound<&T>, max: Bound<&T>) -> Box + 'a> { 133 | Box::new(self.map.range((min, max)).flat_map(|rows| rows.1.iter().map(|row| *row))) 134 | } 135 | } 136 | 137 | /// A sum type expressing all different types of indices so they can easily be stored. Since all 138 | /// indices must at least implement `EqualityIndex`, this enum also forwards all calls of 139 | /// that trait to the underlying index for convenience. 140 | pub enum Index { 141 | /// A `RangeIndex` trait object. 142 | Range(Box + Send + Sync>), 143 | /// An `EqualityIndex` trait object. 144 | Equality(Box + Send + Sync>), 145 | } 146 | 147 | impl EqualityIndex for Index { 148 | fn lookup<'a>(&'a self, key: &T) -> Box + 'a> { 149 | match *self { 150 | Index::Range(ref ri) => ri.lookup(key), 151 | Index::Equality(ref ei) => ei.lookup(key), 152 | } 153 | } 154 | fn index(&mut self, key: T, row: usize) { 155 | match *self { 156 | Index::Range(ref mut ri) => ri.index(key, row), 157 | Index::Equality(ref mut ei) => ei.index(key, row), 158 | } 159 | } 160 | fn undex(&mut self, key: &T, row: usize) { 161 | match *self { 162 | Index::Range(ref mut ri) => ri.undex(key, row), 163 | Index::Equality(ref mut ei) => ei.undex(key, row), 164 | } 165 | } 166 | fn estimate(&self) -> usize { 167 | match *self { 168 | Index::Range(ref ri) => ri.estimate(), 169 | Index::Equality(ref ei) => ei.estimate(), 170 | } 171 | } 172 | } 173 | 174 | impl From> for Index { 175 | fn from(x: HashIndex) -> Index { 176 | Index::Equality(Box::new(x)) 177 | } 178 | } 179 | 180 | impl From> for Index { 181 | fn from(x: BTreeIndex) -> Index { 182 | Index::Range(Box::new(x)) 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use super::*; 189 | 190 | #[test] 191 | fn hashmap_eq_index() { 192 | use super::EqualityIndex; 193 | let mut eqidx = HashIndex::new(); 194 | assert_eq!(eqidx.lookup(&"a").count(), 0); 195 | eqidx.index("a", 0); 196 | assert_eq!(eqidx.lookup(&"a").count(), 1); 197 | eqidx.index("a", 1); 198 | assert_eq!(eqidx.lookup(&"a").count(), 2); 199 | eqidx.undex(&"a", 0); 200 | assert_eq!(eqidx.lookup(&"a").count(), 1); 201 | } 202 | 203 | #[test] 204 | fn btree_eq_index() { 205 | use super::EqualityIndex; 206 | let mut idx = BTreeIndex::new(); 207 | assert_eq!(idx.lookup(&"a").count(), 0); 208 | idx.index("a", 0); 209 | assert_eq!(idx.lookup(&"a").count(), 1); 210 | idx.index("a", 1); 211 | assert_eq!(idx.lookup(&"a").count(), 2); 212 | idx.undex(&"a", 0); 213 | assert_eq!(idx.lookup(&"a").count(), 1); 214 | } 215 | 216 | #[test] 217 | fn btree_range_index() { 218 | use super::RangeIndex; 219 | use std::ops::Bound::Included; 220 | 221 | let mut idx = BTreeIndex::new(); 222 | assert_eq!(idx.between(Included(&"a"), Included(&"b")).count(), 0); 223 | idx.index("a", 0); 224 | assert_eq!(idx.between(Included(&"a"), Included(&"b")).count(), 1); 225 | idx.index("b", 1); 226 | assert_eq!(idx.between(Included(&"a"), Included(&"b")).count(), 2); 227 | idx.undex(&"b", 1); 228 | assert_eq!(idx.between(Included(&"a"), Included(&"b")).count(), 1); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This create provides an indexed, queryable column-based storage system. 2 | //! 3 | //! The storage system is, fundamentally, row-based storage, where all rows have the same number of 4 | //! columns. All columns are the same "type", but given that they can be enum types, you can 5 | //! effectively use differently typed values. Data is stored in a `BTreeMap>`, 6 | //! where the outermost `BTreeMap` is dynamically sized (and may be re-allocated as more rows come 7 | //! in), whereas the innermost `Vec` is expected to never change. The map index is an 8 | //! autoincremented row identifier similar to the one used by SQLite: 9 | //! https://www.sqlite.org/lang_createtable.html#rowid. 10 | //! 11 | //! What makes this crate interesting is that it also allows you to place indices on columns for 12 | //! fast lookups. These indices are automatically updates whenever the dataset changes, so that 13 | //! queries continue to return correct results. Indices should conform to either the 14 | //! `EqualityIndex` trait or the `RangeIndex` trait. As you would expect, the former allows 15 | //! speeding up exact lookups, whereas the latter can also perform efficient range queries. 16 | //! 17 | //! Queries are performed over the dataset by calling `find` with a set of `Condition`s that will 18 | //! be `AND`ed together. `OR` is currently not supported --- issue multiple quieries instead. Each 19 | //! `Condition` represents a value comparison against the value in a single column. The system 20 | //! automatically picks what index to use to satisfy the query, using a heuristic based on the 21 | //! expected number of rows returned for that column for each index. 22 | //! 23 | //! # Known limitations 24 | //! 25 | //! - The set of match operations is currently fairly limited. 26 | //! - The system currently provides an add/remove-only abstraction (i.e., no edit). 27 | 28 | #![deny(missing_docs)] 29 | 30 | use std::collections::HashMap; 31 | use std::collections::BTreeMap; 32 | 33 | /// The `cmp` module holds the mechanisms needed to compare values and express conditionals. 34 | pub mod cmp; 35 | pub use cmp::Comparison; 36 | pub use cmp::Condition; 37 | pub use cmp::Value; 38 | 39 | /// The `idx` module described the traits indexers must adhere to, and implements sensible default 40 | /// indexers. 41 | pub mod idx; 42 | pub use idx::EqualityIndex; 43 | pub use idx::RangeIndex; 44 | pub use idx::Index; 45 | 46 | /// A `Store` is the main storage unit in shortcut. It keeps track of all the rows of data, as well 47 | /// as what indices are available. You will generally be accessing the `Store` either through the 48 | /// `find` method (which lets you find rows that match a certain condition), or through the 49 | /// `insert` method, which lets you add another row. 50 | /// 51 | /// Note that the type used for the rows needs to be `Clone`. This is because the value is also 52 | /// given to the index, which (currently) take a full value, not just a borrow. This *might* change 53 | /// down the line, but it's tricky to get the lifetimes to work out, because the indices would then 54 | /// be scoped by the lifetime of the `Store`. 55 | pub struct Store> { 56 | cols: usize, 57 | rowid: usize, 58 | rows: BTreeMap, 59 | indices: HashMap>, 60 | } 61 | 62 | /// Implementors of `Row` can be used to store the individual rows of a `Store`. 63 | /// 64 | /// The only requirement of implementors is that they can be indexed by a column number. 65 | /// To allow debug assertion on `Store` insertions, we also require `Row` implementors to be able 66 | /// to reveal the number of columns they are storing. 67 | pub trait Row { 68 | /// Look up the value in the given column of this `Row`. 69 | fn index(&self, column: usize) -> &T; 70 | /// Returns the number of columns in this `Row`. 71 | fn columns(&self) -> usize; 72 | } 73 | 74 | impl Store 75 | where T: Ord + Clone, 76 | R: Row 77 | { 78 | /// Allocate a new `Store` with the given number of columns. The column count is checked in 79 | /// `insert` at runtime (bleh). 80 | pub fn new(cols: usize) -> Store { 81 | Store { 82 | cols: cols, 83 | rowid: 0, 84 | rows: BTreeMap::new(), 85 | indices: HashMap::new(), 86 | } 87 | } 88 | 89 | /// Decide what index to use in order to match the given conditions most efficiently. Note that 90 | /// the iterator returned by this method will return a superset of the rows that match the 91 | /// given conditions. Users will need to match each individual row against `conds` again. 92 | /// 93 | /// The lifetime bounds here deserve some explanation. Previously, this was simply `'a` for 94 | /// everything, but this means that the items returned from the iterator were bound by the 95 | /// lifetime of the conditions. This is clearly not necessary. It also meant that you couldn't 96 | /// `.collect()` the results and continue referring to them after the conditions have gone out 97 | /// of scope. 98 | fn using_index<'c, 's: 'c>(&'s self, 99 | conds: &'c [cmp::Condition<'c, T>]) 100 | -> Box + 's> { 101 | 102 | use EqualityIndex; 103 | let best_idx = conds.iter() 104 | .enumerate() 105 | .filter_map(|(ci, c)| self.indices.get(&c.column).and_then(|idx| Some((ci, idx)))) 106 | .filter(|&(ci, _)| { 107 | // does this index work for the operation in question? 108 | match conds[ci].cmp { 109 | cmp::Comparison::Equal(cmp::Value::Const(..)) => true, 110 | _ => false, 111 | } 112 | }) 113 | .min_by_key(|&(_, idx)| idx.estimate()); 114 | 115 | best_idx.and_then(|(ci, idx)| match conds[ci].cmp { 116 | cmp::Comparison::Equal(cmp::Value::Const(ref v)) => Some(idx.lookup(v)), 117 | _ => unreachable!(), 118 | }) 119 | .unwrap_or_else(|| Box::new(self.rows.keys().map(|k| *k))) 120 | } 121 | 122 | /// Returns an iterator that yields all rows matching all the given `Condition`s. 123 | /// 124 | /// This method will automatically determine what index to use to satisfy this query. It 125 | /// currently uses a fairly simple heuristic: it picks the index that: a) is over one of 126 | /// columns being filtered on; b) supports the operation for that filter; and c) has the lowest 127 | /// expected number of rows for a single value. This latter metric is generally the total 128 | /// number of rows divided by the number of entries in the index. See `EqualityIndex::estimate` 129 | /// for details. 130 | pub fn find<'c, 's: 'c>(&'s self, 131 | conds: &'c [cmp::Condition<'c, T>]) 132 | -> Box + 'c> { 133 | let is_a_match = move |r: &&'s _| conds.iter().all(|c| c.matches(*r)); 134 | Box::new(self.using_index(conds) 135 | .map(move |rowi| &self.rows[&rowi]) 136 | .filter(is_a_match)) 137 | } 138 | 139 | /// Delete all rows that match the given conditions. 140 | pub fn delete(&mut self, conds: &[cmp::Condition]) { 141 | self.delete_filter(conds, |_| true); 142 | } 143 | 144 | /// Delete all rows that match the given conditions *and* where the given filter function 145 | /// returns true. 146 | /// 147 | /// This requires that `R` implements `IntoIterator`, and that the columns are yielded *in 148 | /// column order*. 149 | pub fn delete_filter(&mut self, conds: &[cmp::Condition], mut f: F) 150 | where F: FnMut(&R) -> bool 151 | { 152 | // find the rows we should delete 153 | let rowids = self.using_index(conds) 154 | .map(|rowi| (rowi, &self.rows[&rowi])) 155 | .filter(move |&(_, row)| conds.iter().all(|c| c.matches(row))) 156 | .filter(|&(_, row)| f(row)) 157 | .map(|(rowid, _)| rowid) 158 | .collect::>(); 159 | 160 | let deleted = rowids.into_iter() 161 | .map(|rowid| (rowid, self.rows.remove(&rowid).unwrap())) 162 | .collect::>(); 163 | 164 | for (rowid, row) in deleted.into_iter() { 165 | for (col, idx) in self.indices.iter_mut() { 166 | idx.undex(row.index(*col), rowid); 167 | } 168 | } 169 | } 170 | 171 | /// Insert a new data row into the `Store`. The row **must** have the same number of columns as 172 | /// specified when the `Store` was created. If it does not, the code will panic with an 173 | /// assertion failure. 174 | /// 175 | /// Inserting a row has similar complexity to `BTreeMap::insert`, and *may* need to re-allocate 176 | /// the backing memory for the `Store`. The insertion also updates all maintained indices, 177 | /// which may also re-allocate. 178 | pub fn insert(&mut self, row: R) { 179 | debug_assert_eq!(row.columns(), self.cols); 180 | let rowid = self.rowid; 181 | for (column, idx) in self.indices.iter_mut() { 182 | use EqualityIndex; 183 | idx.index(row.index(*column).clone(), rowid); 184 | } 185 | self.rows.insert(self.rowid, row); 186 | self.rowid += 1; 187 | } 188 | 189 | /// Add an index on the given colum using the given indexer. The indexer *must*, at the very 190 | /// least, implement `EqualityIndex`. It *may* also implement other, more sophisticated, 191 | /// indexing strategies outlined in `Index`. 192 | /// 193 | /// When an index is added, it is immediately fed all rows in the current dataset. Thus, adding 194 | /// an index to a `Store` with many rows can be fairly costly. Keep this in mind! 195 | pub fn index>>(&mut self, column: usize, indexer: I) { 196 | use EqualityIndex; 197 | let mut idx = indexer.into(); 198 | 199 | // populate the new index 200 | for (rowid, row) in self.rows.iter() { 201 | idx.index(row.index(column).clone(), *rowid); 202 | } 203 | 204 | self.indices.insert(column, idx); 205 | } 206 | } 207 | 208 | impl<'a, T> Row for &'a [T] { 209 | fn index(&self, i: usize) -> &T { 210 | &self[i] 211 | } 212 | fn columns(&self) -> usize { 213 | self.len() 214 | } 215 | } 216 | 217 | impl Row for [T] { 218 | fn index(&self, i: usize) -> &T { 219 | &self[i] 220 | } 221 | fn columns(&self) -> usize { 222 | self.len() 223 | } 224 | } 225 | 226 | impl Row for Vec { 227 | fn index(&self, i: usize) -> &T { 228 | &self[i] 229 | } 230 | fn columns(&self) -> usize { 231 | self.len() 232 | } 233 | } 234 | 235 | use std::sync; 236 | impl Row for sync::Arc> { 237 | fn index(&self, i: usize) -> &T { 238 | &self[i] 239 | } 240 | fn columns(&self) -> usize { 241 | self.len() 242 | } 243 | } 244 | 245 | #[cfg(test)] 246 | mod tests { 247 | use super::*; 248 | 249 | #[test] 250 | fn it_works() { 251 | let mut store = Store::new(2); 252 | store.insert(vec!["a1", "a2"]); 253 | store.insert(vec!["b1", "b2"]); 254 | store.insert(vec!["c1", "c2"]); 255 | assert_eq!(store.find(&[]).count(), 3); 256 | } 257 | 258 | #[test] 259 | fn it_works_w_non_vec() { 260 | use std::sync; 261 | let mut store = Store::new(2); 262 | store.insert(sync::Arc::new(vec!["a1", "a2"])); 263 | store.insert(sync::Arc::new(vec!["b1", "b2"])); 264 | store.insert(sync::Arc::new(vec!["c1", "c2"])); 265 | assert_eq!(store.find(&[]).count(), 3); 266 | } 267 | 268 | #[test] 269 | fn it_works_with_indices() { 270 | let mut store = Store::new(2); 271 | store.index(0, idx::HashIndex::new()); 272 | store.insert(vec!["a1", "a2"]); 273 | store.insert(vec!["b1", "b2"]); 274 | store.insert(vec!["c1", "c2"]); 275 | assert_eq!(store.find(&[]).count(), 3); 276 | } 277 | 278 | #[test] 279 | fn it_filters() { 280 | let mut store = Store::new(2); 281 | store.insert(vec!["a", "x1"]); 282 | store.insert(vec!["a", "x2"]); 283 | store.insert(vec!["b", "x3"]); 284 | let cmp = [cmp::Condition { 285 | column: 0, 286 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 287 | }]; 288 | assert_eq!(store.find(&cmp) 289 | .count(), 290 | 2); 291 | assert!(store.find(&cmp).all(|r| r[0] == "a")); 292 | } 293 | 294 | #[test] 295 | fn it_filters_with_indices() { 296 | let mut store = Store::new(2); 297 | store.index(0, idx::HashIndex::new()); 298 | store.insert(vec!["a", "x1"]); 299 | store.insert(vec!["a", "x2"]); 300 | store.insert(vec!["b", "x3"]); 301 | let cmp = [cmp::Condition { 302 | column: 0, 303 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 304 | }]; 305 | assert_eq!(store.find(&cmp) 306 | .count(), 307 | 2); 308 | assert!(store.find(&cmp).all(|r| r[0] == "a")); 309 | } 310 | 311 | #[test] 312 | fn it_filters_with_partial_indices() { 313 | let mut store = Store::new(2); 314 | store.index(0, idx::HashIndex::new()); 315 | store.insert(vec!["a", "x1"]); 316 | store.insert(vec!["a", "x2"]); 317 | store.insert(vec!["b", "x3"]); 318 | let cmp = [cmp::Condition { 319 | column: 0, 320 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 321 | }, 322 | cmp::Condition { 323 | column: 1, 324 | cmp: cmp::Comparison::Equal(cmp::Value::new("x2")), 325 | }]; 326 | assert_eq!(store.find(&cmp).count(), 1); 327 | assert!(store.find(&cmp).all(|r| r[0] == "a" && r[1] == "x2")); 328 | } 329 | 330 | #[test] 331 | fn it_filters_with_late_indices() { 332 | let mut store = Store::new(2); 333 | store.insert(vec!["a", "x1"]); 334 | store.insert(vec!["a", "x2"]); 335 | store.insert(vec!["b", "x3"]); 336 | store.index(0, idx::HashIndex::new()); 337 | let cmp = [cmp::Condition { 338 | column: 0, 339 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 340 | }]; 341 | assert_eq!(store.find(&cmp) 342 | .count(), 343 | 2); 344 | assert!(store.find(&cmp).all(|r| r[0] == "a")); 345 | } 346 | 347 | #[test] 348 | fn is_send_sync() { 349 | use std::sync; 350 | use std::thread; 351 | let store = sync::Arc::new(Store::<()>::new(0)); 352 | thread::spawn(move || { drop(store); }) 353 | .join() 354 | .unwrap(); 355 | } 356 | 357 | #[test] 358 | fn it_deletes() { 359 | let mut store = Store::new(2); 360 | store.insert(vec!["a1", "a2"]); 361 | store.insert(vec!["b1", "b2"]); 362 | store.insert(vec!["c1", "c2"]); 363 | store.delete(&[]); 364 | assert_eq!(store.find(&[]).count(), 0); 365 | } 366 | 367 | #[test] 368 | fn filtered_delete() { 369 | let mut store = Store::new(2); 370 | store.insert(vec!["a1", "a2"]); 371 | store.insert(vec!["b1", "b2"]); 372 | store.insert(vec!["c1", "c2"]); 373 | store.delete_filter(&[], |row| row[0] != "b1"); 374 | assert_eq!(store.find(&[]).count(), 1); 375 | assert!(store.find(&[]).all(|r| r[0] == "b1")); 376 | } 377 | 378 | #[test] 379 | fn it_deletes_with_filters() { 380 | let mut store = Store::new(2); 381 | store.insert(vec!["a", "x1"]); 382 | store.insert(vec!["a", "x2"]); 383 | store.insert(vec!["b", "x3"]); 384 | let cmp = [cmp::Condition { 385 | column: 0, 386 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 387 | }]; 388 | store.delete(&cmp); 389 | assert_eq!(store.find(&cmp).count(), 0); 390 | assert_eq!(store.find(&[]).count(), 1); 391 | assert!(store.find(&[]).all(|r| r[0] == "b")); 392 | } 393 | 394 | #[test] 395 | fn it_deletes_with_indices() { 396 | let mut store = Store::new(2); 397 | store.index(0, idx::HashIndex::new()); 398 | store.insert(vec!["a", "x1"]); 399 | store.insert(vec!["a", "x2"]); 400 | store.insert(vec!["b", "x3"]); 401 | let cmp = [cmp::Condition { 402 | column: 0, 403 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 404 | }]; 405 | store.delete(&cmp); 406 | assert_eq!(store.find(&cmp).count(), 0); 407 | assert_eq!(store.find(&[]).count(), 1); 408 | assert!(store.find(&[]).all(|r| r[0] == "b")); 409 | } 410 | 411 | #[test] 412 | fn it_deletes_with_partial_indices() { 413 | let mut store = Store::new(2); 414 | store.index(0, idx::HashIndex::new()); 415 | store.insert(vec!["a", "x1"]); 416 | store.insert(vec!["a", "x2"]); 417 | store.insert(vec!["b", "x3"]); 418 | let cmp = [cmp::Condition { 419 | column: 0, 420 | cmp: cmp::Comparison::Equal(cmp::Value::new("a")), 421 | }, 422 | cmp::Condition { 423 | column: 1, 424 | cmp: cmp::Comparison::Equal(cmp::Value::new("x2")), 425 | }]; 426 | store.delete(&cmp); 427 | assert_eq!(store.find(&cmp).count(), 0); 428 | assert_eq!(store.find(&[]).count(), 2); 429 | assert!(store.find(&[]).any(|r| r[0] == "a" && r[1] == "x1")); 430 | assert!(store.find(&[]).any(|r| r[0] == "b" && r[1] == "x3")); 431 | } 432 | } 433 | --------------------------------------------------------------------------------