Skip to content
This repository
Browse code

Implemented reduce and grouping of views.

  • Loading branch information...
commit 7725b257bb3bb80502b7826f0d6709bf1bc4c3f0 1 parent ec35a7a
Jens Alfke snej authored
2  Demo-Mac/DemoAppController.m
@@ -64,7 +64,7 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n {
64 64 [[tdb viewNamed: @"byDate"] setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
65 65 id date = [doc objectForKey: @"created_at"];
66 66 if (date) emit(date, doc);
67   - } version: @"1"];
  67 + } reduceBlock: NULL version: @"1"];
68 68
69 69 // ...and a validation function requiring parseable dates:
70 70 [tdb addValidation: ^(TDRevision* newRevision, id<TDValidationContext>context) {
2  Demo-iOS/RootViewController.m
@@ -107,7 +107,7 @@ - (void)useDatabase:(CouchDatabase*)theDatabase {
107 107 [[delegate.touchDatabase viewNamed: @"byDate"] setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
108 108 id date = [doc objectForKey: @"created_at"];
109 109 if (date) emit(date, doc);
110   - } version: @"1"];
  110 + } reduceBlock: NULL version: @"1"];
111 111
112 112 // ...and a validation function requiring parseable dates:
113 113 [delegate.touchDatabase addValidation: ^(TDRevision* newRevision,
10 Source/TDRouter.m
@@ -588,10 +588,14 @@ - (TDStatus) do_GET_design: (TDDatabase*)db docID: (NSString*)docID {
588 588
589 589 TDView* view = [db viewNamed: viewName];
590 590 TDStatus status;
591   - NSDictionary* result = [view queryWithOptions: &options status: &status];
592   - if (!result)
  591 + NSArray* rows = [view queryWithOptions: &options status: &status];
  592 + if (!rows)
593 593 return status;
594   - _response.bodyObject = result;
  594 + id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
  595 + _response.bodyObject = $dict({@"rows", rows},
  596 + {@"total_rows", $object(rows.count)},
  597 + {@"offset", $object(options.skip)},
  598 + {@"update_seq", updateSeq});;
595 599 return 200;
596 600 }
597 601
30 Source/TDView.h
@@ -17,17 +17,27 @@ typedef void (^TDMapEmitBlock)(id key, id value);
17 17 @param emit A block to be called to add a key/value pair to the view. Your block can call it zero, one or multiple times. */
18 18 typedef void (^TDMapBlock)(NSDictionary* doc, TDMapEmitBlock emit);
19 19
  20 +/** A "reduce" function called to summarize the results of a view.
  21 + @param keys An array of keys to be reduced (or nil if this is a rereduce).
  22 + @param values A parallel array of values to be reduced, corresponding 1::1 with the keys.
  23 + @param rereduce YES if the input values are the results of previous reductions.
  24 + @return The reduced value; almost always a scalar or small fixed-size object. */
  25 +typedef id (^TDReduceBlock)(NSArray* keys, NSArray* values, BOOL rereduce);
  26 +
20 27
21 28 /** Standard query options for views. */
22 29 typedef struct TDQueryOptions {
23 30 __unsafe_unretained id startKey;
24 31 __unsafe_unretained id endKey;
25   - int skip;
26   - int limit;
  32 + unsigned skip;
  33 + unsigned limit;
  34 + unsigned groupLevel;
27 35 BOOL descending;
28 36 BOOL includeDocs;
29 37 BOOL updateSeq;
30 38 BOOL inclusiveEnd;
  39 + BOOL reduce;
  40 + BOOL group;
31 41 } TDQueryOptions;
32 42
33 43 extern const TDQueryOptions kDefaultTDQueryOptions;
@@ -41,6 +51,7 @@ extern const TDQueryOptions kDefaultTDQueryOptions;
41 51 NSString* _name;
42 52 int _viewID;
43 53 TDMapBlock _mapBlock;
  54 + TDReduceBlock _reduceBlock;
44 55 }
45 56
46 57 - (void) deleteView;
@@ -49,12 +60,21 @@ extern const TDQueryOptions kDefaultTDQueryOptions;
49 60 @property (readonly) NSString* name;
50 61
51 62 @property (readonly) TDMapBlock mapBlock;
52   -- (BOOL) setMapBlock: (TDMapBlock)mapBlock version: (NSString*)version;
  63 +@property (readonly) TDReduceBlock reduceBlock;
  64 +
  65 +- (BOOL) setMapBlock: (TDMapBlock)mapBlock
  66 + reduceBlock: (TDReduceBlock)reduceBlock
  67 + version: (NSString*)version;
53 68
54 69 - (void) removeIndex;
55 70 - (TDStatus) updateIndex;
56 71
57   -- (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
58   - status: (TDStatus*)outStatus;
  72 +@property (readonly) SequenceNumber lastSequenceIndexed;
  73 +
  74 +/** Queries the view.
  75 + @param options The options to use.
  76 + @return An array of result rows -- each is a dictionary with "key" and "value" keys, and possibly "id" and "doc". */
  77 +- (NSArray*) queryWithOptions: (const TDQueryOptions*)options
  78 + status: (TDStatus*)outStatus;
59 79
60 80 @end
163 Source/TDView.m
@@ -21,8 +21,13 @@
21 21 #import "FMResultSet.h"
22 22
23 23
  24 +#define kReduceBatchSize 100
  25 +
  26 +
24 27 const TDQueryOptions kDefaultTDQueryOptions = {
25   - nil, nil, 0, INT_MAX, NO, NO, NO, YES
  28 + nil, nil, 0,
  29 + UINT_MAX, 0,
  30 + NO, NO, NO, YES, NO, NO
26 31 };
27 32
28 33
@@ -45,11 +50,13 @@ - (id) initWithDatabase: (TDDatabase*)db name: (NSString*)name {
45 50 - (void)dealloc {
46 51 [_db release];
47 52 [_name release];
  53 + [_mapBlock release];
  54 + [_reduceBlock release];
48 55 [super dealloc];
49 56 }
50 57
51 58
52   -@synthesize database=_db, name=_name, mapBlock=_mapBlock;
  59 +@synthesize database=_db, name=_name, mapBlock=_mapBlock, reduceBlock=_reduceBlock;
53 60
54 61
55 62 - (int) viewID {
@@ -64,11 +71,16 @@ - (SequenceNumber) lastSequenceIndexed {
64 71 }
65 72
66 73
67   -- (BOOL) setMapBlock: (TDMapBlock)mapBlock version:(NSString *)version {
  74 +- (BOOL) setMapBlock: (TDMapBlock)mapBlock
  75 + reduceBlock: (TDReduceBlock)reduceBlock
  76 + version: (NSString *)version
  77 +{
68 78 Assert(mapBlock);
69 79 Assert(version);
70   - [_mapBlock release];
  80 + [_mapBlock autorelease];
71 81 _mapBlock = [mapBlock copy];
  82 + [_reduceBlock autorelease];
  83 + _reduceBlock = [reduceBlock copy];
72 84
73 85 // Update the version column in the db. This is a little weird looking because we want to
74 86 // avoid modifying the db if the version didn't change, and because the row might not exist yet.
@@ -245,21 +257,26 @@ - (TDStatus) updateIndex {
245 257 #pragma mark - QUERYING:
246 258
247 259
248   -//FIX: This has a lot of code in common with -[TDDatabase getAllDocs:]. Unify the two!
249   -- (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
250   - status: (TDStatus*)outStatus
  260 +- (FMResultSet*) resultSetWithOptions: (const TDQueryOptions*)options
  261 + status: (TDStatus*)outStatus
251 262 {
252 263 if (!options)
253 264 options = &kDefaultTDQueryOptions;
254 265
  266 + if (!options->group) {
  267 + if (options->reduce && !_reduceBlock) {
  268 + Warn(@"Cannot use reduce option in view %@ which has no reduce block defined", _name);
  269 + *outStatus = 400;
  270 + return nil;
  271 + }
  272 + if (options->groupLevel > 0)
  273 + Warn(@"Setting groupLevel without group makes no sense");
  274 + }
  275 +
255 276 *outStatus = [self updateIndex];
256 277 if (*outStatus >= 300)
257 278 return nil;
258 279
259   - SequenceNumber update_seq = 0;
260   - if (options->updateSeq)
261   - update_seq = self.lastSequenceIndexed; // TODO: needs to be atomic with the following SELECT
262   -
263 280 NSMutableString* sql = [NSMutableString stringWithString: @"SELECT key, value, docid"];
264 281 if (options->includeDocs)
265 282 [sql appendString: @", revid, json, revs.sequence"];
@@ -287,44 +304,120 @@ - (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
287 304 "ORDER BY key"];
288 305 if (options->descending)
289 306 [sql appendString: @" DESC"];
290   - [sql appendString: @" LIMIT ? OFFSET ?"];
291   - [args addObject: $object(options->limit)];
292   - [args addObject: $object(options->skip)];
  307 + if (options->limit != kDefaultTDQueryOptions.limit) {
  308 + [sql appendString: @" LIMIT ?"];
  309 + [args addObject: $object(options->limit)];
  310 + }
  311 + if (options->skip > 0) {
  312 + [sql appendString: @" OFFSET ?"];
  313 + [args addObject: $object(options->skip)];
  314 + }
293 315
294 316 FMResultSet* r = [_db.fmdb executeQuery: sql withArgumentsInArray: args];
295   - if (!r) {
  317 + if (!r)
296 318 *outStatus = 500;
297   - return nil;
  319 + return r;
  320 +}
  321 +
  322 +
  323 +// Are key1 and key2 grouped together at this groupLevel?
  324 +static bool groupTogether(id key1, id key2, unsigned groupLevel) {
  325 + if (groupLevel == 0 || ![key1 isKindOfClass: [NSArray class]]
  326 + || ![key2 isKindOfClass: [NSArray class]])
  327 + return [key1 isEqual: key2];
  328 + unsigned end = MIN(groupLevel, MIN([key1 count], [key2 count]));
  329 + for (unsigned i = 0; i< end; ++i) {
  330 + if (![[key1 objectAtIndex: i] isEqual: [key2 objectAtIndex: i]])
  331 + return false;
298 332 }
  333 + return true;
  334 +}
  335 +
  336 +// Returns the prefix of the key to use in the result row, at this groupLevel
  337 +static id groupKey(id key, unsigned groupLevel) {
  338 + if (groupLevel > 0 && [key isKindOfClass: [NSArray class]] && [key count] > groupLevel)
  339 + return [key subarrayWithRange: NSMakeRange(0, groupLevel)];
  340 + else
  341 + return key;
  342 +}
  343 +
  344 +
  345 +- (NSArray*) queryWithOptions: (const TDQueryOptions*)options
  346 + status: (TDStatus*)outStatus
  347 +{
  348 + if (!options)
  349 + options = &kDefaultTDQueryOptions;
  350 +
  351 + FMResultSet* r = [self resultSetWithOptions: options status: outStatus];
  352 + if (!r)
  353 + return nil;
299 354
  355 + bool reduce = options->reduce;
  356 + bool group = options->group;
  357 + unsigned groupLevel = options->groupLevel;
300 358 NSMutableArray* rows = $marray();
  359 + NSMutableArray* keysToReduce=nil, *valuesToReduce=nil;
  360 + id lastKey = nil;
  361 + if (reduce) {
  362 + keysToReduce = [[NSMutableArray alloc] initWithCapacity: 100];
  363 + valuesToReduce = [[NSMutableArray alloc] initWithCapacity: 100];
  364 + }
  365 +
301 366 while ([r next]) {
302   - NSData* key = fromJSON([r dataForColumnIndex: 0]);
303   - NSData* value = fromJSON([r dataForColumnIndex: 1]);
304   - NSString* docID = [r stringForColumnIndex: 2];
305   - NSDictionary* docContents = nil;
306   - if (options->includeDocs) {
307   - docContents = [_db documentPropertiesFromJSON: [r dataForColumnIndex: 4]
308   - docID: docID
309   - revID: [r stringForColumnIndex: 3]
310   - sequence: [r longLongIntForColumnIndex: 5]];
  367 + @autoreleasepool {
  368 + id key = fromJSON([r dataForColumnIndex: 0]);
  369 + id value = fromJSON([r dataForColumnIndex: 1]);
  370 + Assert(key);
  371 + if (reduce) {
  372 + // Reduced or grouped query:
  373 + if (group && !groupTogether(key, lastKey, groupLevel) && lastKey) {
  374 + // This pair starts a new group, so reduce & record the last one:
  375 + id reduced = _reduceBlock(keysToReduce, valuesToReduce, NO) ?: $null;
  376 + [rows addObject: $dict({@"key", groupKey(lastKey, groupLevel)},
  377 + {@"value", reduced})];
  378 + [keysToReduce removeAllObjects];
  379 + [valuesToReduce removeAllObjects];
  380 + }
  381 + [keysToReduce addObject: key];
  382 + [valuesToReduce addObject: value ?: $null];
  383 + lastKey = key;
  384 +
  385 + } else {
  386 + // Regular query:
  387 + NSString* docID = [r stringForColumnIndex: 2];
  388 + NSDictionary* docContents = nil;
  389 + if (options->includeDocs) {
  390 + docContents = [_db documentPropertiesFromJSON: [r dataNoCopyForColumnIndex: 4]
  391 + docID: docID
  392 + revID: [r stringForColumnIndex: 3]
  393 + sequence: [r longLongIntForColumnIndex:5]];
  394 + }
  395 + [rows addObject: $dict({@"id", docID},
  396 + {@"key", key},
  397 + {@"value", value},
  398 + {@"doc", docContents})];
  399 + }
311 400 }
312   - NSDictionary* change = $dict({@"id", docID},
313   - {@"key", key},
314   - {@"value", value},
315   - {@"doc", docContents});
316   - [rows addObject: change];
317 401 }
  402 +
  403 + if (reduce) {
  404 + if (keysToReduce.count > 0) {
  405 + // Finish the last group (or the entire list, if no grouping):
  406 + id key = group ? groupKey(lastKey, groupLevel) : $null;
  407 + id reduced = _reduceBlock(keysToReduce, valuesToReduce, NO) ?: $null;
  408 + [rows addObject: $dict({@"key", key}, {@"value", reduced})];
  409 + }
  410 + [keysToReduce release];
  411 + [valuesToReduce release];
  412 + }
  413 +
318 414 [r close];
319 415 *outStatus = 200;
320   - NSUInteger totalRows = rows.count; //??? Is this true, or does it ignore limit/offset?
321   - return $dict({@"rows", rows},
322   - {@"total_rows", $object(totalRows)},
323   - {@"offset", $object(options->skip)},
324   - {@"update_seq", update_seq ? $object(update_seq) : nil});
  416 + return rows;
325 417 }
326 418
327 419
  420 +// This is really just for unit tests & debugging
328 421 - (NSArray*) dump {
329 422 if (self.viewID <= 0)
330 423 return nil;
164 Source/TDView_Tests.m
@@ -30,15 +30,18 @@
30 30 CAssertEqual(view.name, @"aview");
31 31 CAssertNull(view.mapBlock);
32 32
33   - BOOL changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"1"];
  33 + BOOL changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
  34 + reduceBlock: NULL version: @"1"];
34 35 CAssert(changed);
35 36
36 37 CAssertEqual(db.allViews, $array(view));
37 38
38   - changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"1"];
  39 + changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
  40 + reduceBlock: NULL version: @"1"];
39 41 CAssert(!changed);
40 42
41   - changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"2"];
  43 + changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
  44 + reduceBlock: NULL version: @"2"];
42 45 CAssert(changed);
43 46
44 47 [db close];
@@ -71,11 +74,19 @@
71 74 CAssert([doc objectForKey: @"_id"] != nil, @"Missing _id in %@", doc);
72 75 CAssert([doc objectForKey: @"_rev"] != nil, @"Missing _rev in %@", doc);
73 76 emit([doc objectForKey: @"key"], nil);
74   - } version: @"1"];
  77 + } reduceBlock: NULL version: @"1"];
75 78 return view;
76 79 }
77 80
78 81
  82 +static id total(NSArray* keys, NSArray* values) {
  83 + double total = 0;
  84 + for (NSNumber* value in values)
  85 + total += value.doubleValue;
  86 + return $object(total);
  87 +}
  88 +
  89 +
79 90 TestCase(TDView_Index) {
80 91 RequireTestCase(TDView_Create);
81 92 TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
@@ -120,14 +131,11 @@
120 131 $dict({@"key", @"\"one\""}, {@"seq", $object(1)}) ));
121 132
122 133 // Now do a real query:
123   - NSDictionary* query = [view queryWithOptions: NULL status: &status];
  134 + NSArray* rows = [view queryWithOptions: NULL status: &status];
124 135 CAssertEq(status, 200);
125   - CAssertEqual([query objectForKey: @"rows"], $array(
126   - $dict({@"key", @"3hree"}, {@"id", rev3.docID}),
  136 + CAssertEqual(rows, $array( $dict({@"key", @"3hree"}, {@"id", rev3.docID}),
127 137 $dict({@"key", @"four"}, {@"id", rev4.docID}),
128 138 $dict({@"key", @"one"}, {@"id", rev1.docID}) ));
129   - CAssertEqual([query objectForKey: @"total_rows"], $object(3));
130   - CAssertEqual([query objectForKey: @"offset"], $object(0));
131 139
132 140 [view removeIndex];
133 141
@@ -145,56 +153,46 @@
145 153 // Query all rows:
146 154 TDQueryOptions options = kDefaultTDQueryOptions;
147 155 TDStatus status;
148   - NSDictionary* query = [view queryWithOptions: &options status: &status];
  156 + NSArray* rows = [view queryWithOptions: &options status: &status];
149 157 NSArray* expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
150 158 $dict({@"id", @"44444"}, {@"key", @"four"}),
151 159 $dict({@"id", @"11111"}, {@"key", @"one"}),
152 160 $dict({@"id", @"33333"}, {@"key", @"three"}),
153 161 $dict({@"id", @"22222"}, {@"key", @"two"}));
154   - CAssertEqual(query, $dict({@"rows", expectedRows},
155   - {@"total_rows", $object(5)},
156   - {@"offset", $object(0)}));
  162 + CAssertEqual(rows, expectedRows);
157 163
158 164 // Start/end key query:
159 165 options = kDefaultTDQueryOptions;
160 166 options.startKey = @"a";
161 167 options.endKey = @"one";
162   - query = [view queryWithOptions: &options status: &status];
  168 + rows = [view queryWithOptions: &options status: &status];
163 169 expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
164 170 $dict({@"id", @"44444"}, {@"key", @"four"}),
165 171 $dict({@"id", @"11111"}, {@"key", @"one"}));
166   - CAssertEqual(query, $dict({@"rows", expectedRows},
167   - {@"total_rows", $object(3)},
168   - {@"offset", $object(0)}));
  172 + CAssertEqual(rows, expectedRows);
169 173
170 174 // Start/end query without inclusive end:
171 175 options.inclusiveEnd = NO;
172   - query = [view queryWithOptions: &options status: &status];
  176 + rows = [view queryWithOptions: &options status: &status];
173 177 expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
174 178 $dict({@"id", @"44444"}, {@"key", @"four"}));
175   - CAssertEqual(query, $dict({@"rows", expectedRows},
176   - {@"total_rows", $object(2)},
177   - {@"offset", $object(0)}));
  179 + CAssertEqual(rows, expectedRows);
178 180
179 181 // Reversed:
180 182 options.descending = YES;
181 183 options.startKey = @"o";
182 184 options.endKey = @"five";
183 185 options.inclusiveEnd = YES;
184   - query = [view queryWithOptions: &options status: &status];
  186 + rows = [view queryWithOptions: &options status: &status];
185 187 expectedRows = $array($dict({@"id", @"44444"}, {@"key", @"four"}),
186 188 $dict({@"id", @"55555"}, {@"key", @"five"}));
187   - CAssertEqual(query, $dict({@"rows", expectedRows},
188   - {@"total_rows", $object(2)},
189   - {@"offset", $object(0)}));
  189 + CAssertEqual(rows, expectedRows);
190 190
191 191 // Reversed, no inclusive end:
192 192 options.inclusiveEnd = NO;
193   - query = [view queryWithOptions: &options status: &status];
  193 + rows = [view queryWithOptions: &options status: &status];
194 194 expectedRows = $array($dict({@"id", @"44444"}, {@"key", @"four"}));
195   - CAssertEqual(query, $dict({@"rows", expectedRows},
196   - {@"total_rows", $object(1)},
197   - {@"offset", $object(0)}));
  195 + CAssertEqual(rows, expectedRows);
198 196 }
199 197
200 198
@@ -235,6 +233,112 @@
235 233 CAssertEqual(query, $dict({@"rows", expectedRows},
236 234 {@"total_rows", $object(2)},
237 235 {@"offset", $object(0)}));
238   -}
  236 +}
  237 +
  238 +
  239 +TestCase(TDView_Reduce) {
  240 + RequireTestCase(TDView_Query);
  241 + TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
  242 + putDoc(db, $dict({@"_id", @"CD"}, {@"cost", $object(8.99)}));
  243 + putDoc(db, $dict({@"_id", @"App"}, {@"cost", $object(1.95)}));
  244 + putDoc(db, $dict({@"_id", @"Dessert"}, {@"cost", $object(6.50)}));
  245 +
  246 + TDView* view = [db viewNamed: @"totaler"];
  247 + [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
  248 + CAssert([doc objectForKey: @"_id"] != nil, @"Missing _id in %@", doc);
  249 + CAssert([doc objectForKey: @"_rev"] != nil, @"Missing _rev in %@", doc);
  250 + id cost = [doc objectForKey: @"cost"];
  251 + if (cost)
  252 + emit([doc objectForKey: @"_id"], cost);
  253 + } reduceBlock: ^(NSArray* keys, NSArray* values, BOOL rereduce) {
  254 + return total(keys, values);
  255 + } version: @"1"];
  256 +
  257 + CAssertEq([view updateIndex], 200);
  258 + NSArray* dump = [view dump];
  259 + Log(@"View dump: %@", dump);
  260 + CAssertEqual(dump, $array($dict({@"key", @"\"App\""}, {@"value", @"1.95"}, {@"seq", $object(2)}),
  261 + $dict({@"key", @"\"CD\""}, {@"value", @"8.99"}, {@"seq", $object(1)}),
  262 + $dict({@"key", @"\"Dessert\""}, {@"value", @"6.5"}, {@"seq", $object(3)}) ));
  263 +
  264 + TDQueryOptions options = kDefaultTDQueryOptions;
  265 + options.reduce = YES;
  266 + TDStatus status;
  267 + NSArray* reduced = [view queryWithOptions: &options status: &status];
  268 + CAssertEq(status, 200);
  269 + CAssertEq(reduced.count, 1u);
  270 + double result = [[[reduced objectAtIndex: 0] objectForKey: @"value"] doubleValue];
  271 + CAssert(fabs(result - 17.44) < 0.001, @"Unexpected reduced value %@", reduced);
  272 +}
  273 +
  274 +
  275 +TestCase(TDView_Grouped) {
  276 + RequireTestCase(TDView_Reduce);
  277 + TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
  278 + putDoc(db, $dict({@"_id", @"1"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
  279 + {@"track", @"Ether"}, {@"time", $object(231)}));
  280 + putDoc(db, $dict({@"_id", @"2"}, {@"artist", @"Gang Of Four"}, {@"album", @"Songs Of The Free"},
  281 + {@"track", @"I Love A Man In Uniform"}, {@"time", $object(248)}));
  282 + putDoc(db, $dict({@"_id", @"3"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
  283 + {@"track", @"Natural's Not In It"}, {@"time", $object(187)}));
  284 + putDoc(db, $dict({@"_id", @"4"}, {@"artist", @"PiL"}, {@"album", @"Metal Box"},
  285 + {@"track", @"Memories"}, {@"time", $object(309)}));
  286 + putDoc(db, $dict({@"_id", @"5"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
  287 + {@"track", @"Not Great Men"}, {@"time", $object(187)}));
  288 +
  289 + TDView* view = [db viewNamed: @"grouper"];
  290 + [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
  291 + emit($array([doc objectForKey: @"artist"],
  292 + [doc objectForKey: @"album"],
  293 + [doc objectForKey: @"track"]),
  294 + [doc objectForKey: @"time"]);
  295 + } reduceBlock:^id(NSArray *keys, NSArray *values, BOOL rereduce) {
  296 + return total(keys, values);
  297 + } version: @"1"];
  298 +
  299 + TDQueryOptions options = kDefaultTDQueryOptions;
  300 + options.reduce = YES;
  301 + TDStatus status;
  302 + NSArray* rows = [view queryWithOptions: &options status: &status];
  303 + CAssertEq(status, 200);
  304 + CAssertEqual(rows, $array($dict({@"key", $null}, {@"value", $object(1162)})));
  305 +
  306 + options.group = YES;
  307 + rows = [view queryWithOptions: &options status: &status];
  308 + CAssertEq(status, 200);
  309 + CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
  310 + @"Ether")},
  311 + {@"value", $object(231)}),
  312 + $dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
  313 + @"Natural's Not In It")},
  314 + {@"value", $object(187)}),
  315 + $dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
  316 + @"Not Great Men")},
  317 + {@"value", $object(187)}),
  318 + $dict({@"key", $array(@"Gang Of Four", @"Songs Of The Free",
  319 + @"I Love A Man In Uniform")},
  320 + {@"value", $object(248)}),
  321 + $dict({@"key", $array(@"PiL", @"Metal Box",
  322 + @"Memories")},
  323 + {@"value", $object(309)})));
  324 +
  325 + options.groupLevel = 1;
  326 + rows = [view queryWithOptions: &options status: &status];
  327 + CAssertEq(status, 200);
  328 + CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four")}, {@"value", $object(853)}),
  329 + $dict({@"key", $array(@"PiL")}, {@"value", $object(309)})));
  330 +
  331 + options.groupLevel = 2;
  332 + rows = [view queryWithOptions: &options status: &status];
  333 + CAssertEq(status, 200);
  334 + CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four", @"Entertainment!")},
  335 + {@"value", $object(605)}),
  336 + $dict({@"key", $array(@"Gang Of Four", @"Songs Of The Free")},
  337 + {@"value", $object(248)}),
  338 + $dict({@"key", $array(@"PiL", @"Metal Box")},
  339 + {@"value", $object(309)})));
  340 +}
  341 +
  342 +
239 343
240 344 #endif
2  TouchDB.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
@@ -82,7 +82,7 @@
82 82 </CommandLineArgument>
83 83 <CommandLineArgument
84 84 argument = "Test_All"
85   - isEnabled = "NO">
  85 + isEnabled = "YES">
86 86 </CommandLineArgument>
87 87 </CommandLineArguments>
88 88 <AdditionalOptions>
1  vendor/MYUtilities/CollectionUtils.h
@@ -60,6 +60,7 @@ BOOL kvRemoveFromSet( id owner, NSString *property, NSMutableSet *set, id objToR
60 60
61 61 #define $true ((NSNumber*)kCFBooleanTrue)
62 62 #define $false ((NSNumber*)kCFBooleanFalse)
  63 +#define $null [NSNull null]
63 64
64 65
65 66 @interface NSObject (MYUtils)

0 comments on commit 7725b25

Please sign in to comment.
Something went wrong with that request. Please try again.