Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implemented reduce and grouping of views.

  • Loading branch information...
commit 7725b257bb3bb80502b7826f0d6709bf1bc4c3f0 1 parent ec35a7a
@snej snej authored
View
2  Demo-Mac/DemoAppController.m
@@ -64,7 +64,7 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n {
[[tdb viewNamed: @"byDate"] setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
id date = [doc objectForKey: @"created_at"];
if (date) emit(date, doc);
- } version: @"1"];
+ } reduceBlock: NULL version: @"1"];
// ...and a validation function requiring parseable dates:
[tdb addValidation: ^(TDRevision* newRevision, id<TDValidationContext>context) {
View
2  Demo-iOS/RootViewController.m
@@ -107,7 +107,7 @@ - (void)useDatabase:(CouchDatabase*)theDatabase {
[[delegate.touchDatabase viewNamed: @"byDate"] setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
id date = [doc objectForKey: @"created_at"];
if (date) emit(date, doc);
- } version: @"1"];
+ } reduceBlock: NULL version: @"1"];
// ...and a validation function requiring parseable dates:
[delegate.touchDatabase addValidation: ^(TDRevision* newRevision,
View
10 Source/TDRouter.m
@@ -588,10 +588,14 @@ - (TDStatus) do_GET_design: (TDDatabase*)db docID: (NSString*)docID {
TDView* view = [db viewNamed: viewName];
TDStatus status;
- NSDictionary* result = [view queryWithOptions: &options status: &status];
- if (!result)
+ NSArray* rows = [view queryWithOptions: &options status: &status];
+ if (!rows)
return status;
- _response.bodyObject = result;
+ id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
+ _response.bodyObject = $dict({@"rows", rows},
+ {@"total_rows", $object(rows.count)},
+ {@"offset", $object(options.skip)},
+ {@"update_seq", updateSeq});;
return 200;
}
View
30 Source/TDView.h
@@ -17,17 +17,27 @@ typedef void (^TDMapEmitBlock)(id key, id value);
@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. */
typedef void (^TDMapBlock)(NSDictionary* doc, TDMapEmitBlock emit);
+/** A "reduce" function called to summarize the results of a view.
+ @param keys An array of keys to be reduced (or nil if this is a rereduce).
+ @param values A parallel array of values to be reduced, corresponding 1::1 with the keys.
+ @param rereduce YES if the input values are the results of previous reductions.
+ @return The reduced value; almost always a scalar or small fixed-size object. */
+typedef id (^TDReduceBlock)(NSArray* keys, NSArray* values, BOOL rereduce);
+
/** Standard query options for views. */
typedef struct TDQueryOptions {
__unsafe_unretained id startKey;
__unsafe_unretained id endKey;
- int skip;
- int limit;
+ unsigned skip;
+ unsigned limit;
+ unsigned groupLevel;
BOOL descending;
BOOL includeDocs;
BOOL updateSeq;
BOOL inclusiveEnd;
+ BOOL reduce;
+ BOOL group;
} TDQueryOptions;
extern const TDQueryOptions kDefaultTDQueryOptions;
@@ -41,6 +51,7 @@ extern const TDQueryOptions kDefaultTDQueryOptions;
NSString* _name;
int _viewID;
TDMapBlock _mapBlock;
+ TDReduceBlock _reduceBlock;
}
- (void) deleteView;
@@ -49,12 +60,21 @@ extern const TDQueryOptions kDefaultTDQueryOptions;
@property (readonly) NSString* name;
@property (readonly) TDMapBlock mapBlock;
-- (BOOL) setMapBlock: (TDMapBlock)mapBlock version: (NSString*)version;
+@property (readonly) TDReduceBlock reduceBlock;
+
+- (BOOL) setMapBlock: (TDMapBlock)mapBlock
+ reduceBlock: (TDReduceBlock)reduceBlock
+ version: (NSString*)version;
- (void) removeIndex;
- (TDStatus) updateIndex;
-- (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
- status: (TDStatus*)outStatus;
+@property (readonly) SequenceNumber lastSequenceIndexed;
+
+/** Queries the view.
+ @param options The options to use.
+ @return An array of result rows -- each is a dictionary with "key" and "value" keys, and possibly "id" and "doc". */
+- (NSArray*) queryWithOptions: (const TDQueryOptions*)options
+ status: (TDStatus*)outStatus;
@end
View
163 Source/TDView.m
@@ -21,8 +21,13 @@
#import "FMResultSet.h"
+#define kReduceBatchSize 100
+
+
const TDQueryOptions kDefaultTDQueryOptions = {
- nil, nil, 0, INT_MAX, NO, NO, NO, YES
+ nil, nil, 0,
+ UINT_MAX, 0,
+ NO, NO, NO, YES, NO, NO
};
@@ -45,11 +50,13 @@ - (id) initWithDatabase: (TDDatabase*)db name: (NSString*)name {
- (void)dealloc {
[_db release];
[_name release];
+ [_mapBlock release];
+ [_reduceBlock release];
[super dealloc];
}
-@synthesize database=_db, name=_name, mapBlock=_mapBlock;
+@synthesize database=_db, name=_name, mapBlock=_mapBlock, reduceBlock=_reduceBlock;
- (int) viewID {
@@ -64,11 +71,16 @@ - (SequenceNumber) lastSequenceIndexed {
}
-- (BOOL) setMapBlock: (TDMapBlock)mapBlock version:(NSString *)version {
+- (BOOL) setMapBlock: (TDMapBlock)mapBlock
+ reduceBlock: (TDReduceBlock)reduceBlock
+ version: (NSString *)version
+{
Assert(mapBlock);
Assert(version);
- [_mapBlock release];
+ [_mapBlock autorelease];
_mapBlock = [mapBlock copy];
+ [_reduceBlock autorelease];
+ _reduceBlock = [reduceBlock copy];
// Update the version column in the db. This is a little weird looking because we want to
// avoid modifying the db if the version didn't change, and because the row might not exist yet.
@@ -245,21 +257,26 @@ - (TDStatus) updateIndex {
#pragma mark - QUERYING:
-//FIX: This has a lot of code in common with -[TDDatabase getAllDocs:]. Unify the two!
-- (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
- status: (TDStatus*)outStatus
+- (FMResultSet*) resultSetWithOptions: (const TDQueryOptions*)options
+ status: (TDStatus*)outStatus
{
if (!options)
options = &kDefaultTDQueryOptions;
+ if (!options->group) {
+ if (options->reduce && !_reduceBlock) {
+ Warn(@"Cannot use reduce option in view %@ which has no reduce block defined", _name);
+ *outStatus = 400;
+ return nil;
+ }
+ if (options->groupLevel > 0)
+ Warn(@"Setting groupLevel without group makes no sense");
+ }
+
*outStatus = [self updateIndex];
if (*outStatus >= 300)
return nil;
- SequenceNumber update_seq = 0;
- if (options->updateSeq)
- update_seq = self.lastSequenceIndexed; // TODO: needs to be atomic with the following SELECT
-
NSMutableString* sql = [NSMutableString stringWithString: @"SELECT key, value, docid"];
if (options->includeDocs)
[sql appendString: @", revid, json, revs.sequence"];
@@ -287,44 +304,120 @@ - (NSDictionary*) queryWithOptions: (const TDQueryOptions*)options
"ORDER BY key"];
if (options->descending)
[sql appendString: @" DESC"];
- [sql appendString: @" LIMIT ? OFFSET ?"];
- [args addObject: $object(options->limit)];
- [args addObject: $object(options->skip)];
+ if (options->limit != kDefaultTDQueryOptions.limit) {
+ [sql appendString: @" LIMIT ?"];
+ [args addObject: $object(options->limit)];
+ }
+ if (options->skip > 0) {
+ [sql appendString: @" OFFSET ?"];
+ [args addObject: $object(options->skip)];
+ }
FMResultSet* r = [_db.fmdb executeQuery: sql withArgumentsInArray: args];
- if (!r) {
+ if (!r)
*outStatus = 500;
- return nil;
+ return r;
+}
+
+
+// Are key1 and key2 grouped together at this groupLevel?
+static bool groupTogether(id key1, id key2, unsigned groupLevel) {
+ if (groupLevel == 0 || ![key1 isKindOfClass: [NSArray class]]
+ || ![key2 isKindOfClass: [NSArray class]])
+ return [key1 isEqual: key2];
+ unsigned end = MIN(groupLevel, MIN([key1 count], [key2 count]));
+ for (unsigned i = 0; i< end; ++i) {
+ if (![[key1 objectAtIndex: i] isEqual: [key2 objectAtIndex: i]])
+ return false;
}
+ return true;
+}
+
+// Returns the prefix of the key to use in the result row, at this groupLevel
+static id groupKey(id key, unsigned groupLevel) {
+ if (groupLevel > 0 && [key isKindOfClass: [NSArray class]] && [key count] > groupLevel)
+ return [key subarrayWithRange: NSMakeRange(0, groupLevel)];
+ else
+ return key;
+}
+
+
+- (NSArray*) queryWithOptions: (const TDQueryOptions*)options
+ status: (TDStatus*)outStatus
+{
+ if (!options)
+ options = &kDefaultTDQueryOptions;
+
+ FMResultSet* r = [self resultSetWithOptions: options status: outStatus];
+ if (!r)
+ return nil;
+ bool reduce = options->reduce;
+ bool group = options->group;
+ unsigned groupLevel = options->groupLevel;
NSMutableArray* rows = $marray();
+ NSMutableArray* keysToReduce=nil, *valuesToReduce=nil;
+ id lastKey = nil;
+ if (reduce) {
+ keysToReduce = [[NSMutableArray alloc] initWithCapacity: 100];
+ valuesToReduce = [[NSMutableArray alloc] initWithCapacity: 100];
+ }
+
while ([r next]) {
- NSData* key = fromJSON([r dataForColumnIndex: 0]);
- NSData* value = fromJSON([r dataForColumnIndex: 1]);
- NSString* docID = [r stringForColumnIndex: 2];
- NSDictionary* docContents = nil;
- if (options->includeDocs) {
- docContents = [_db documentPropertiesFromJSON: [r dataForColumnIndex: 4]
- docID: docID
- revID: [r stringForColumnIndex: 3]
- sequence: [r longLongIntForColumnIndex: 5]];
+ @autoreleasepool {
+ id key = fromJSON([r dataForColumnIndex: 0]);
+ id value = fromJSON([r dataForColumnIndex: 1]);
+ Assert(key);
+ if (reduce) {
+ // Reduced or grouped query:
+ if (group && !groupTogether(key, lastKey, groupLevel) && lastKey) {
+ // This pair starts a new group, so reduce & record the last one:
+ id reduced = _reduceBlock(keysToReduce, valuesToReduce, NO) ?: $null;
+ [rows addObject: $dict({@"key", groupKey(lastKey, groupLevel)},
+ {@"value", reduced})];
+ [keysToReduce removeAllObjects];
+ [valuesToReduce removeAllObjects];
+ }
+ [keysToReduce addObject: key];
+ [valuesToReduce addObject: value ?: $null];
+ lastKey = key;
+
+ } else {
+ // Regular query:
+ NSString* docID = [r stringForColumnIndex: 2];
+ NSDictionary* docContents = nil;
+ if (options->includeDocs) {
+ docContents = [_db documentPropertiesFromJSON: [r dataNoCopyForColumnIndex: 4]
+ docID: docID
+ revID: [r stringForColumnIndex: 3]
+ sequence: [r longLongIntForColumnIndex:5]];
+ }
+ [rows addObject: $dict({@"id", docID},
+ {@"key", key},
+ {@"value", value},
+ {@"doc", docContents})];
+ }
}
- NSDictionary* change = $dict({@"id", docID},
- {@"key", key},
- {@"value", value},
- {@"doc", docContents});
- [rows addObject: change];
}
+
+ if (reduce) {
+ if (keysToReduce.count > 0) {
+ // Finish the last group (or the entire list, if no grouping):
+ id key = group ? groupKey(lastKey, groupLevel) : $null;
+ id reduced = _reduceBlock(keysToReduce, valuesToReduce, NO) ?: $null;
+ [rows addObject: $dict({@"key", key}, {@"value", reduced})];
+ }
+ [keysToReduce release];
+ [valuesToReduce release];
+ }
+
[r close];
*outStatus = 200;
- NSUInteger totalRows = rows.count; //??? Is this true, or does it ignore limit/offset?
- return $dict({@"rows", rows},
- {@"total_rows", $object(totalRows)},
- {@"offset", $object(options->skip)},
- {@"update_seq", update_seq ? $object(update_seq) : nil});
+ return rows;
}
+// This is really just for unit tests & debugging
- (NSArray*) dump {
if (self.viewID <= 0)
return nil;
View
164 Source/TDView_Tests.m
@@ -30,15 +30,18 @@
CAssertEqual(view.name, @"aview");
CAssertNull(view.mapBlock);
- BOOL changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"1"];
+ BOOL changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
+ reduceBlock: NULL version: @"1"];
CAssert(changed);
CAssertEqual(db.allViews, $array(view));
- changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"1"];
+ changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
+ reduceBlock: NULL version: @"1"];
CAssert(!changed);
- changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { } version: @"2"];
+ changed = [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) { }
+ reduceBlock: NULL version: @"2"];
CAssert(changed);
[db close];
@@ -71,11 +74,19 @@
CAssert([doc objectForKey: @"_id"] != nil, @"Missing _id in %@", doc);
CAssert([doc objectForKey: @"_rev"] != nil, @"Missing _rev in %@", doc);
emit([doc objectForKey: @"key"], nil);
- } version: @"1"];
+ } reduceBlock: NULL version: @"1"];
return view;
}
+static id total(NSArray* keys, NSArray* values) {
+ double total = 0;
+ for (NSNumber* value in values)
+ total += value.doubleValue;
+ return $object(total);
+}
+
+
TestCase(TDView_Index) {
RequireTestCase(TDView_Create);
TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
@@ -120,14 +131,11 @@
$dict({@"key", @"\"one\""}, {@"seq", $object(1)}) ));
// Now do a real query:
- NSDictionary* query = [view queryWithOptions: NULL status: &status];
+ NSArray* rows = [view queryWithOptions: NULL status: &status];
CAssertEq(status, 200);
- CAssertEqual([query objectForKey: @"rows"], $array(
- $dict({@"key", @"3hree"}, {@"id", rev3.docID}),
+ CAssertEqual(rows, $array( $dict({@"key", @"3hree"}, {@"id", rev3.docID}),
$dict({@"key", @"four"}, {@"id", rev4.docID}),
$dict({@"key", @"one"}, {@"id", rev1.docID}) ));
- CAssertEqual([query objectForKey: @"total_rows"], $object(3));
- CAssertEqual([query objectForKey: @"offset"], $object(0));
[view removeIndex];
@@ -145,56 +153,46 @@
// Query all rows:
TDQueryOptions options = kDefaultTDQueryOptions;
TDStatus status;
- NSDictionary* query = [view queryWithOptions: &options status: &status];
+ NSArray* rows = [view queryWithOptions: &options status: &status];
NSArray* expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
$dict({@"id", @"44444"}, {@"key", @"four"}),
$dict({@"id", @"11111"}, {@"key", @"one"}),
$dict({@"id", @"33333"}, {@"key", @"three"}),
$dict({@"id", @"22222"}, {@"key", @"two"}));
- CAssertEqual(query, $dict({@"rows", expectedRows},
- {@"total_rows", $object(5)},
- {@"offset", $object(0)}));
+ CAssertEqual(rows, expectedRows);
// Start/end key query:
options = kDefaultTDQueryOptions;
options.startKey = @"a";
options.endKey = @"one";
- query = [view queryWithOptions: &options status: &status];
+ rows = [view queryWithOptions: &options status: &status];
expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
$dict({@"id", @"44444"}, {@"key", @"four"}),
$dict({@"id", @"11111"}, {@"key", @"one"}));
- CAssertEqual(query, $dict({@"rows", expectedRows},
- {@"total_rows", $object(3)},
- {@"offset", $object(0)}));
+ CAssertEqual(rows, expectedRows);
// Start/end query without inclusive end:
options.inclusiveEnd = NO;
- query = [view queryWithOptions: &options status: &status];
+ rows = [view queryWithOptions: &options status: &status];
expectedRows = $array($dict({@"id", @"55555"}, {@"key", @"five"}),
$dict({@"id", @"44444"}, {@"key", @"four"}));
- CAssertEqual(query, $dict({@"rows", expectedRows},
- {@"total_rows", $object(2)},
- {@"offset", $object(0)}));
+ CAssertEqual(rows, expectedRows);
// Reversed:
options.descending = YES;
options.startKey = @"o";
options.endKey = @"five";
options.inclusiveEnd = YES;
- query = [view queryWithOptions: &options status: &status];
+ rows = [view queryWithOptions: &options status: &status];
expectedRows = $array($dict({@"id", @"44444"}, {@"key", @"four"}),
$dict({@"id", @"55555"}, {@"key", @"five"}));
- CAssertEqual(query, $dict({@"rows", expectedRows},
- {@"total_rows", $object(2)},
- {@"offset", $object(0)}));
+ CAssertEqual(rows, expectedRows);
// Reversed, no inclusive end:
options.inclusiveEnd = NO;
- query = [view queryWithOptions: &options status: &status];
+ rows = [view queryWithOptions: &options status: &status];
expectedRows = $array($dict({@"id", @"44444"}, {@"key", @"four"}));
- CAssertEqual(query, $dict({@"rows", expectedRows},
- {@"total_rows", $object(1)},
- {@"offset", $object(0)}));
+ CAssertEqual(rows, expectedRows);
}
@@ -235,6 +233,112 @@
CAssertEqual(query, $dict({@"rows", expectedRows},
{@"total_rows", $object(2)},
{@"offset", $object(0)}));
-}
+}
+
+
+TestCase(TDView_Reduce) {
+ RequireTestCase(TDView_Query);
+ TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
+ putDoc(db, $dict({@"_id", @"CD"}, {@"cost", $object(8.99)}));
+ putDoc(db, $dict({@"_id", @"App"}, {@"cost", $object(1.95)}));
+ putDoc(db, $dict({@"_id", @"Dessert"}, {@"cost", $object(6.50)}));
+
+ TDView* view = [db viewNamed: @"totaler"];
+ [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
+ CAssert([doc objectForKey: @"_id"] != nil, @"Missing _id in %@", doc);
+ CAssert([doc objectForKey: @"_rev"] != nil, @"Missing _rev in %@", doc);
+ id cost = [doc objectForKey: @"cost"];
+ if (cost)
+ emit([doc objectForKey: @"_id"], cost);
+ } reduceBlock: ^(NSArray* keys, NSArray* values, BOOL rereduce) {
+ return total(keys, values);
+ } version: @"1"];
+
+ CAssertEq([view updateIndex], 200);
+ NSArray* dump = [view dump];
+ Log(@"View dump: %@", dump);
+ CAssertEqual(dump, $array($dict({@"key", @"\"App\""}, {@"value", @"1.95"}, {@"seq", $object(2)}),
+ $dict({@"key", @"\"CD\""}, {@"value", @"8.99"}, {@"seq", $object(1)}),
+ $dict({@"key", @"\"Dessert\""}, {@"value", @"6.5"}, {@"seq", $object(3)}) ));
+
+ TDQueryOptions options = kDefaultTDQueryOptions;
+ options.reduce = YES;
+ TDStatus status;
+ NSArray* reduced = [view queryWithOptions: &options status: &status];
+ CAssertEq(status, 200);
+ CAssertEq(reduced.count, 1u);
+ double result = [[[reduced objectAtIndex: 0] objectForKey: @"value"] doubleValue];
+ CAssert(fabs(result - 17.44) < 0.001, @"Unexpected reduced value %@", reduced);
+}
+
+
+TestCase(TDView_Grouped) {
+ RequireTestCase(TDView_Reduce);
+ TDDatabase *db = [TDDatabase createEmptyDBAtPath: @"/tmp/TouchDB_ViewTest.touchdb"];
+ putDoc(db, $dict({@"_id", @"1"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
+ {@"track", @"Ether"}, {@"time", $object(231)}));
+ putDoc(db, $dict({@"_id", @"2"}, {@"artist", @"Gang Of Four"}, {@"album", @"Songs Of The Free"},
+ {@"track", @"I Love A Man In Uniform"}, {@"time", $object(248)}));
+ putDoc(db, $dict({@"_id", @"3"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
+ {@"track", @"Natural's Not In It"}, {@"time", $object(187)}));
+ putDoc(db, $dict({@"_id", @"4"}, {@"artist", @"PiL"}, {@"album", @"Metal Box"},
+ {@"track", @"Memories"}, {@"time", $object(309)}));
+ putDoc(db, $dict({@"_id", @"5"}, {@"artist", @"Gang Of Four"}, {@"album", @"Entertainment!"},
+ {@"track", @"Not Great Men"}, {@"time", $object(187)}));
+
+ TDView* view = [db viewNamed: @"grouper"];
+ [view setMapBlock: ^(NSDictionary* doc, TDMapEmitBlock emit) {
+ emit($array([doc objectForKey: @"artist"],
+ [doc objectForKey: @"album"],
+ [doc objectForKey: @"track"]),
+ [doc objectForKey: @"time"]);
+ } reduceBlock:^id(NSArray *keys, NSArray *values, BOOL rereduce) {
+ return total(keys, values);
+ } version: @"1"];
+
+ TDQueryOptions options = kDefaultTDQueryOptions;
+ options.reduce = YES;
+ TDStatus status;
+ NSArray* rows = [view queryWithOptions: &options status: &status];
+ CAssertEq(status, 200);
+ CAssertEqual(rows, $array($dict({@"key", $null}, {@"value", $object(1162)})));
+
+ options.group = YES;
+ rows = [view queryWithOptions: &options status: &status];
+ CAssertEq(status, 200);
+ CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
+ @"Ether")},
+ {@"value", $object(231)}),
+ $dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
+ @"Natural's Not In It")},
+ {@"value", $object(187)}),
+ $dict({@"key", $array(@"Gang Of Four", @"Entertainment!",
+ @"Not Great Men")},
+ {@"value", $object(187)}),
+ $dict({@"key", $array(@"Gang Of Four", @"Songs Of The Free",
+ @"I Love A Man In Uniform")},
+ {@"value", $object(248)}),
+ $dict({@"key", $array(@"PiL", @"Metal Box",
+ @"Memories")},
+ {@"value", $object(309)})));
+
+ options.groupLevel = 1;
+ rows = [view queryWithOptions: &options status: &status];
+ CAssertEq(status, 200);
+ CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four")}, {@"value", $object(853)}),
+ $dict({@"key", $array(@"PiL")}, {@"value", $object(309)})));
+
+ options.groupLevel = 2;
+ rows = [view queryWithOptions: &options status: &status];
+ CAssertEq(status, 200);
+ CAssertEqual(rows, $array($dict({@"key", $array(@"Gang Of Four", @"Entertainment!")},
+ {@"value", $object(605)}),
+ $dict({@"key", $array(@"Gang Of Four", @"Songs Of The Free")},
+ {@"value", $object(248)}),
+ $dict({@"key", $array(@"PiL", @"Metal Box")},
+ {@"value", $object(309)})));
+}
+
+
#endif
View
2  TouchDB.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
@@ -82,7 +82,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "Test_All"
- isEnabled = "NO">
+ isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<AdditionalOptions>
View
1  vendor/MYUtilities/CollectionUtils.h
@@ -60,6 +60,7 @@ BOOL kvRemoveFromSet( id owner, NSString *property, NSMutableSet *set, id objToR
#define $true ((NSNumber*)kCFBooleanTrue)
#define $false ((NSNumber*)kCFBooleanFalse)
+#define $null [NSNull null]
@interface NSObject (MYUtils)
Please sign in to comment.
Something went wrong with that request. Please try again.