Skip to content

Commit 5c1b045

Browse files
committed
Add support for array indexes
1 parent 5255342 commit 5c1b045

File tree

2 files changed

+270
-43
lines changed

2 files changed

+270
-43
lines changed

src/index.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ use crate::{
33
c_api::{
44
CBLValueIndexConfiguration, CBLDatabase_GetIndexNames, CBLDatabase_DeleteIndex, CBLError,
55
CBLDatabase_CreateValueIndex, CBLCollection_CreateValueIndex, CBLCollection_DeleteIndex,
6-
CBLCollection_GetIndexNames,
6+
CBLCollection_GetIndexNames, CBLCollection_CreateArrayIndex, CBLArrayIndexConfiguration,
7+
CBLQueryIndex, CBLQueryIndex_Name, CBLQueryIndex_Collection, CBLCollection_GetIndex,
78
},
89
error::{Result, failure},
910
slice::from_str,
1011
QueryLanguage, Array,
1112
collection::Collection,
12-
check_error,
13+
check_error, retain,
1314
};
1415

1516
pub struct ValueIndexConfiguration {
@@ -24,6 +25,10 @@ impl CblRef for ValueIndexConfiguration {
2425
}
2526

2627
impl ValueIndexConfiguration {
28+
/** Create a Value Index Configuration.
29+
@param query_langage The language used in the expressions.
30+
@param expressions The expressions describing each coloumn of the index. The expressions could be specified
31+
in a JSON Array or in N1QL syntax using comma delimiter. */
2732
pub fn new(query_language: QueryLanguage, expressions: &str) -> Self {
2833
let slice = from_str(expressions);
2934
Self {
@@ -35,6 +40,73 @@ impl ValueIndexConfiguration {
3540
}
3641
}
3742

43+
pub struct ArrayIndexConfiguration {
44+
cbl_ref: CBLArrayIndexConfiguration,
45+
}
46+
47+
impl CblRef for ArrayIndexConfiguration {
48+
type Output = CBLArrayIndexConfiguration;
49+
fn get_ref(&self) -> Self::Output {
50+
self.cbl_ref
51+
}
52+
}
53+
54+
impl ArrayIndexConfiguration {
55+
/** Create an Array Index Configuration for indexing property values within arrays
56+
in documents, intended for use with the UNNEST query.
57+
@param query_langage The language used in the expressions (Required).
58+
@param path Path to the array, which can be nested to be indexed (Required).
59+
Use "[]" to represent a property that is an array of each nested array level.
60+
For a single array or the last level array, the "[]" is optional. For instance,
61+
use "contacts[].phones" to specify an array of phones within each contact.
62+
@param expressions Optional expressions representing the values within the array to be
63+
indexed. The expressions could be specified in a JSON Array or in N1QL syntax
64+
using comma delimiter. If the array specified by the path contains scalar values,
65+
the expressions should be left unset or set to null. */
66+
pub fn new(query_language: QueryLanguage, path: &str, expressions: &str) -> Self {
67+
let s_path = from_str(path);
68+
let s_expressions = from_str(expressions);
69+
Self {
70+
cbl_ref: CBLArrayIndexConfiguration {
71+
expressionLanguage: query_language as u32,
72+
path: s_path.get_ref(),
73+
expressions: s_expressions.get_ref(),
74+
},
75+
}
76+
}
77+
}
78+
79+
pub struct QueryIndex {
80+
cbl_ref: *mut CBLQueryIndex,
81+
}
82+
83+
impl CblRef for QueryIndex {
84+
type Output = *mut CBLQueryIndex;
85+
fn get_ref(&self) -> Self::Output {
86+
self.cbl_ref
87+
}
88+
}
89+
90+
impl QueryIndex {
91+
pub(crate) fn retain(cbl_ref: *mut CBLQueryIndex) -> Self {
92+
Self {
93+
cbl_ref: unsafe { retain(cbl_ref) },
94+
}
95+
}
96+
97+
pub fn name(&self) -> String {
98+
unsafe {
99+
CBLQueryIndex_Name(self.get_ref())
100+
.to_string()
101+
.unwrap_or_default()
102+
}
103+
}
104+
105+
pub fn collection(&self) -> Collection {
106+
unsafe { Collection::retain(CBLQueryIndex_Collection(self.get_ref())) }
107+
}
108+
}
109+
38110
impl Database {
39111
#[deprecated(note = "please use `create_index` on default collection instead")]
40112
pub fn create_index(&self, name: &str, config: &ValueIndexConfiguration) -> Result<bool> {
@@ -90,6 +162,23 @@ impl Collection {
90162
failure(err)
91163
}
92164

165+
pub fn create_array_index(&self, name: &str, config: &ArrayIndexConfiguration) -> Result<bool> {
166+
let mut err = CBLError::default();
167+
let slice = from_str(name);
168+
let r = unsafe {
169+
CBLCollection_CreateArrayIndex(
170+
self.get_ref(),
171+
slice.get_ref(),
172+
config.get_ref(),
173+
&mut err,
174+
)
175+
};
176+
if !err {
177+
return Ok(r);
178+
}
179+
failure(err)
180+
}
181+
93182
pub fn delete_index(&self, name: &str) -> Result<bool> {
94183
let mut err = CBLError::default();
95184
let slice = from_str(name);
@@ -105,4 +194,14 @@ impl Collection {
105194
let arr = unsafe { CBLCollection_GetIndexNames(self.get_ref(), &mut err) };
106195
check_error(&err).map(|()| Array::wrap(arr))
107196
}
197+
198+
pub fn get_index(&self, name: &str) -> Result<QueryIndex> {
199+
let mut err = CBLError::default();
200+
let slice = from_str(name);
201+
let index = unsafe { CBLCollection_GetIndex(self.get_ref(), slice.get_ref(), &mut err) };
202+
if !err {
203+
return Ok(QueryIndex::retain(index));
204+
}
205+
failure(err)
206+
}
108207
}

tests/query_tests.rs

Lines changed: 169 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
extern crate couchbase_lite;
22
extern crate regex;
33

4-
use couchbase_lite::index::ValueIndexConfiguration;
4+
use couchbase_lite::index::{ValueIndexConfiguration, ArrayIndexConfiguration};
55
use regex::Regex;
66

77
use self::couchbase_lite::*;
@@ -65,6 +65,90 @@ fn query() {
6565
});
6666
}
6767

68+
#[test]
69+
fn parameters() {
70+
utils::with_db(|db| {
71+
let mut doc = Document::new_with_id("id1");
72+
let mut props = doc.mutable_properties();
73+
props.at("bool").put_bool(true);
74+
props.at("f64").put_f64(3.1);
75+
props.at("i64").put_i64(3);
76+
props.at("string").put_string("allo");
77+
db.save_document_with_concurency_control(&mut doc, ConcurrencyControl::FailOnConflict)
78+
.expect("save");
79+
80+
let query = Query::new(
81+
db,
82+
QueryLanguage::N1QL,
83+
"SELECT _.* FROM _ \
84+
WHERE _.bool=$bool \
85+
AND _.f64=$f64 \
86+
AND _.i64=$i64 \
87+
AND _.string=$string",
88+
)
89+
.expect("create query");
90+
91+
let mut params = MutableDict::new();
92+
params.at("bool").put_bool(true);
93+
params.at("f64").put_f64(3.1);
94+
params.at("i64").put_i64(3);
95+
params.at("string").put_string("allo");
96+
query.set_parameters(&params);
97+
98+
let params = query.parameters();
99+
assert_eq!(params.get("bool").as_bool(), Some(true));
100+
assert_eq!(params.get("f64").as_f64(), Some(3.1));
101+
assert_eq!(params.get("i64").as_i64(), Some(3));
102+
assert_eq!(params.get("string").as_string(), Some("allo"));
103+
104+
assert_eq!(query.execute().unwrap().count(), 1);
105+
});
106+
}
107+
108+
#[test]
109+
fn get_index() {
110+
utils::with_db(|db| {
111+
// Default collection
112+
let default_collection = db.default_collection().unwrap().unwrap();
113+
assert!(default_collection
114+
.create_index(
115+
"new_index1",
116+
&ValueIndexConfiguration::new(QueryLanguage::JSON, r#"[[".someField"]]"#),
117+
)
118+
.unwrap());
119+
120+
let index1 = default_collection.get_index("new_index1").unwrap();
121+
assert_eq!(index1.name(), "new_index1");
122+
assert_eq!(
123+
index1.collection().full_name(),
124+
default_collection.full_name()
125+
);
126+
127+
// New collection
128+
let new_coll = db
129+
.create_collection(String::from("coll"), String::from("scop"))
130+
.unwrap();
131+
132+
assert!(new_coll
133+
.create_index(
134+
"new_index2",
135+
&ValueIndexConfiguration::new(QueryLanguage::JSON, r#"[[".someField2"]]"#),
136+
)
137+
.unwrap());
138+
139+
let index2 = new_coll.get_index("new_index2").unwrap();
140+
assert_eq!(index2.name(), "new_index2");
141+
assert_eq!(index2.collection().full_name(), new_coll.full_name());
142+
})
143+
}
144+
145+
fn get_index_name_from_explain(explain: &str) -> Option<String> {
146+
Regex::new(r"USING INDEX (\w+) ")
147+
.unwrap()
148+
.captures(explain)
149+
.map(|c| c.get(1).unwrap().as_str().to_string())
150+
}
151+
68152
#[test]
69153
fn full_index() {
70154
utils::with_db(|db| {
@@ -88,12 +172,7 @@ fn full_index() {
88172
)
89173
.expect("create query");
90174

91-
let index = Regex::new(r"USING INDEX (\w+) ")
92-
.unwrap()
93-
.captures(&query.explain().unwrap())
94-
.map(|c| c.get(1).unwrap().as_str().to_string())
95-
.unwrap();
96-
175+
let index = get_index_name_from_explain(&query.explain().unwrap()).unwrap();
97176
assert_eq!(index, "new_index");
98177

99178
// Check index not used
@@ -148,12 +227,7 @@ fn partial_index() {
148227
)
149228
.expect("create query");
150229

151-
let index = Regex::new(r"USING INDEX (\w+) ")
152-
.unwrap()
153-
.captures(&query.explain().unwrap())
154-
.map(|c| c.get(1).unwrap().as_str().to_string())
155-
.unwrap();
156-
230+
let index = get_index_name_from_explain(&query.explain().unwrap()).unwrap();
157231
assert_eq!(index, "new_index");
158232

159233
// Check index not used
@@ -174,41 +248,95 @@ fn partial_index() {
174248
}
175249

176250
#[test]
177-
fn parameters() {
251+
fn array_index() {
178252
utils::with_db(|db| {
179-
let mut doc = Document::new_with_id("id1");
180-
let mut props = doc.mutable_properties();
181-
props.at("bool").put_bool(true);
182-
props.at("f64").put_f64(3.1);
183-
props.at("i64").put_i64(3);
184-
props.at("string").put_string("allo");
185-
db.save_document_with_concurency_control(&mut doc, ConcurrencyControl::FailOnConflict)
186-
.expect("save");
253+
let mut default_collection = db.default_collection().unwrap().unwrap();
254+
255+
// Add one document
256+
let mut doc = Document::new();
257+
doc.set_properties_as_json(
258+
r#"{
259+
"name":"Sam",
260+
"contacts":[
261+
{
262+
"type":"primary",
263+
"address":{"street":"1 St","city":"San Pedro","state":"CA"},
264+
"phones":[
265+
{"type":"home","number":"310-123-4567"},
266+
{"type":"mobile","number":"310-123-6789"}
267+
]
268+
},
269+
{
270+
"type":"secondary",
271+
"address":{"street":"5 St","city":"Seattle","state":"WA"},
272+
"phones":[
273+
{"type":"home","number":"206-123-4567"},
274+
{"type":"mobile","number":"206-123-6789"}
275+
]
276+
}
277+
],
278+
"likes":["soccer","travel"]
279+
}"#,
280+
)
281+
.unwrap();
282+
default_collection.save_document(&mut doc).unwrap();
283+
284+
// Index with one level of unnest
285+
let index_configuration = ArrayIndexConfiguration::new(QueryLanguage::N1QL, "likes", "");
286+
287+
assert!(default_collection
288+
.create_array_index("one_level", &index_configuration,)
289+
.unwrap());
187290

188291
let query = Query::new(
189292
db,
190293
QueryLanguage::N1QL,
191-
"SELECT _.* FROM _ \
192-
WHERE _.bool=$bool \
193-
AND _.f64=$f64 \
194-
AND _.i64=$i64 \
195-
AND _.string=$string",
294+
"SELECT _.name, _like FROM _ UNNEST _.likes as _like WHERE _like = 'travel'",
196295
)
197-
.expect("create query");
296+
.unwrap();
198297

199-
let mut params = MutableDict::new();
200-
params.at("bool").put_bool(true);
201-
params.at("f64").put_f64(3.1);
202-
params.at("i64").put_i64(3);
203-
params.at("string").put_string("allo");
204-
query.set_parameters(&params);
298+
let index = get_index_name_from_explain(&query.explain().unwrap()).unwrap();
299+
assert_eq!(index, "one_level");
205300

206-
let params = query.parameters();
207-
assert_eq!(params.get("bool").as_bool(), Some(true));
208-
assert_eq!(params.get("f64").as_f64(), Some(3.1));
209-
assert_eq!(params.get("i64").as_i64(), Some(3));
210-
assert_eq!(params.get("string").as_string(), Some("allo"));
301+
let mut result = query.execute().unwrap();
302+
let row = result.next().unwrap();
303+
assert_eq!(row.as_array().to_json(), r#"["Sam","travel"]"#);
211304

212-
assert_eq!(query.execute().unwrap().count(), 1);
213-
});
305+
assert!(result.next().is_none());
306+
307+
// Index with two levels of unnest
308+
/*let index_configuration = ArrayIndexConfiguration::new(
309+
QueryLanguage::N1QL,
310+
"contacts[].phones",
311+
"",//"type",
312+
);
313+
314+
assert!(default_collection
315+
.create_array_index(
316+
"myindex",
317+
&index_configuration,
318+
).unwrap()
319+
);
320+
321+
let query = Query::new(
322+
db,
323+
QueryLanguage::N1QL,
324+
r#"SELECT _.name, contact.type, phone.number
325+
FROM _
326+
UNNEST _.contacts as contact
327+
UNNEST contact.phones as phone
328+
WHERE phone.type = 'mobile'"#
329+
).unwrap();
330+
331+
println!("Explain: {}", query.explain().unwrap());
332+
333+
let index = get_index_name_from_explain(&query.explain().unwrap()).unwrap();
334+
assert_eq!(index, "two_levels");
335+
336+
let mut result = query.execute().unwrap();
337+
let row = result.next().unwrap();
338+
assert_eq!(row.as_array().to_json(), r#"["Sam","travel"]"#);
339+
340+
assert!(result.next().is_none());*/
341+
})
214342
}

0 commit comments

Comments
 (0)