From 4b1fbf1effa7d7b6f64ff50a361417c86fce391b Mon Sep 17 00:00:00 2001 From: cacaodev Date: Fri, 29 Oct 2010 16:53:42 +0200 Subject: [PATCH] Implemented subquery expressions. Added missing getters in CPExpression. Added a type: argument to FunctionExpression initializer used by subclasses to init super with their own type. Fixed evaluation with bindings dictionary in CPExpression_variable. Allow values in bindings to be a constant or an expression (per cocoa). Tests: added tests for subqueryExpressions, for variableExpression evaluation where the value in the bindings dictionary can be either a constant or another expression. Parsing a simple variable expression works (e.g. '$x = 12'), parsing a variable in a combined path (e.g. '$x.path = 12' won't yet. --- .../CPPredicate/CPComparisonPredicate.j | 13 +-- Foundation/CPPredicate/CPExpression.j | 70 ++++++++++-- .../CPPredicate/CPExpression_aggregate.j | 2 +- .../CPPredicate/CPExpression_constant.j | 9 +- .../CPPredicate/CPExpression_function.j | 26 +++-- Foundation/CPPredicate/CPExpression_keypath.j | 10 +- Foundation/CPPredicate/CPExpression_self.j | 18 +-- Foundation/CPPredicate/CPExpression_set.j | 1 + .../CPPredicate/CPExpression_subquery.j | 103 ++++++++++++++++++ .../CPPredicate/CPExpression_variable.j | 24 ++-- Tests/Foundation/CPPredicateTest.j | 43 +++++++- 11 files changed, 250 insertions(+), 69 deletions(-) create mode 100644 Foundation/CPPredicate/CPExpression_subquery.j diff --git a/Foundation/CPPredicate/CPComparisonPredicate.j b/Foundation/CPPredicate/CPComparisonPredicate.j index 5ccd47b97..2e3ada1a7 100644 --- a/Foundation/CPPredicate/CPComparisonPredicate.j +++ b/Foundation/CPPredicate/CPComparisonPredicate.j @@ -478,17 +478,8 @@ var CPComparisonPredicateModifier, - (BOOL)evaluateWithObject:(id)object substitutionVariables:(CPDictionary)variables { - var left = _left, - right = _right; - - if (variables != nil) - { - left = [left _expressionWithSubstitutionVariables:variables]; - right = [right _expressionWithSubstitutionVariables:variables]; - } - - var leftValue = [left expressionValueWithObject:object context:nil], - rightValue = [right expressionValueWithObject:object context:nil]; + var leftValue = [_left expressionValueWithObject:object context:variables], + rightValue = [_right expressionValueWithObject:object context:variables]; if (_modifier == CPDirectPredicateModifier) return [self _evaluateValue:leftValue rightValue:rightValue]; diff --git a/Foundation/CPPredicate/CPExpression.j b/Foundation/CPPredicate/CPExpression.j index 78c0dabb5..9c339ab50 100644 --- a/Foundation/CPPredicate/CPExpression.j +++ b/Foundation/CPPredicate/CPExpression.j @@ -227,13 +227,11 @@ CPMinusSetExpressionType = 9; return [[CPExpression_function alloc] initWithTarget:target selector:CPSelectorFromString(function_name) arguments:parameters]; } - -+ (CPExpression)expressionForSubquery:(CPExpression)expression usingIteratorVariable:(CPString)variable predicate:(id)predicate ++ (CPExpression)expressionForSubquery:(CPExpression)expression usingIteratorVariable:(CPString)variable predicate:(CPPredicate)predicate { - return nil; // UNIMPLEMENTED + return [[CPExpression_subquery alloc] initWithExpression:expression usingIteratorVariable:variable predicate:predicate]; } - // Getting Information About an Expression /*! Returns the expression type for the receiver. @@ -252,7 +250,7 @@ CPMinusSetExpressionType = 9; */ - (id)constantValue { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPConstantValueExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } @@ -263,7 +261,7 @@ CPMinusSetExpressionType = 9; */ - (CPString)variable { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPVariableExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } @@ -274,7 +272,7 @@ CPMinusSetExpressionType = 9; */ - (CPString)keyPath { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPKeyPathExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } @@ -285,7 +283,7 @@ CPMinusSetExpressionType = 9; */ - (CPString)function { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPFunctionExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } @@ -296,21 +294,70 @@ CPMinusSetExpressionType = 9; */ - (CPArray)arguments { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPFunctionExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } /*! Returns the collection of expressions in an aggregate expression, or the collection element of a subquery expression. - @return Returns the collection of expressions in an aggregate expression, or the collection element of a subquery expression. + @return The collection of expressions in an aggregate expression, or the collection element of a subquery expression. This method raises an exception if it is not applicable to the receiver. */ - (id)collection { - [CPException raise:CPInvalidArgumentException reason:@"self is not of CPAggregateExpressionType"]; + _CPRaiseInvalidAbstractInvocation(self, _cmd); return nil; } +/*! + Returns the predicate in a subquery expression. + @return The predicate in a subquery expression.. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPPredicate)predicate +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +/*! + Returns the operand for the receiver. + @return The operand for the receiver—that is, the object on which the selector will be invoked. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPExpression)operand +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +/*! + Returns the left expression of a set expression. + @return The left expression of a set expression. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPExpression)leftExpression +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +/*! + Returns the right expression of a set expression. + @return The right expression of a set expression. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPExpression)rightExpression +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables +{ + return self; +} + @end @import "CPExpression_constant.j" @@ -320,3 +367,4 @@ CPMinusSetExpressionType = 9; @import "CPExpression_function.j" @import "CPExpression_aggregate.j" @import "CPExpression_set.j" +@import "CPExpression_subquery.j" diff --git a/Foundation/CPPredicate/CPExpression_aggregate.j b/Foundation/CPPredicate/CPExpression_aggregate.j index 4414afffe..fed253883 100644 --- a/Foundation/CPPredicate/CPExpression_aggregate.j +++ b/Foundation/CPPredicate/CPExpression_aggregate.j @@ -89,4 +89,4 @@ var CPCollectionKey = @"CPCollection"; [coder encodeObject:_aggregate forKey:CPCollectionKey]; } -@end \ No newline at end of file +@end diff --git a/Foundation/CPPredicate/CPExpression_constant.j b/Foundation/CPPredicate/CPExpression_constant.j index 4cd38fd99..66cfa1ca0 100644 --- a/Foundation/CPPredicate/CPExpression_constant.j +++ b/Foundation/CPPredicate/CPExpression_constant.j @@ -17,7 +17,7 @@ - (BOOL)isEqual:(id)object { - if (self == object) + if (self === object) return YES; if (object.isa != self.isa || [object expressionType] != [self expressionType] || ![[object constantValue] isEqual:[self constantValue]]) @@ -31,16 +31,11 @@ return _value; } -- (id)expressionValueWithObject:object context:(CPDictionary)context +- (id)expressionValueWithObject:(id)object context:(CPDictionary)context { return _value; } -- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables -{ - return self; -} - - (CPString)description { if ([_value isKindOfClass:[CPString class]]) diff --git a/Foundation/CPPredicate/CPExpression_function.j b/Foundation/CPPredicate/CPExpression_function.j index 02209a495..31d92f1ac 100644 --- a/Foundation/CPPredicate/CPExpression_function.j +++ b/Foundation/CPPredicate/CPExpression_function.j @@ -22,14 +22,19 @@ return [self initWithTarget:operand selector:aSelector arguments:parameters]; } -- (id)initWithTarget:(CPExpression)targetExpression selector:aSelector arguments:parameters +- (id)initWithTarget:(CPExpression)operand selector:(SEL)aSelector arguments:(CPArray)parameters { - [super initWithExpressionType:CPFunctionExpressionType]; + return [self initWithTarget:operand selector:aSelector arguments:parameters type:CPFunctionExpressionType]; +} + +- (id)initWithTarget:(CPExpression)operand selector:(SEL)aSelector arguments:(CPArray)parameters type:(int)type +{ + [super initWithExpressionType:type]; // Cocoa doc: "This method throws an exception immediately if the selector is unknown" -// but targetExpression's value (selector's target) may be resolved only at runtime. +// but operand's value (the target) may be resolved only at runtime. _selector = aSelector; - _operand = targetExpression; + _operand = operand; _arguments = parameters; _argc = [parameters count]; @@ -98,7 +103,7 @@ { var array = [CPArray array], i; - + // should we also allow variables for target and selectors ? for (i = 0; i < _argc; i++) [array addObject:[[_arguments objectAtIndex:i] _expressionWithSubstitutionVariables:variables]]; @@ -109,17 +114,19 @@ var CPSelectorNameKey = @"CPSelectorName", CPArgumentsKey = @"CPArguments", - CPOperandKey = @"CPOperand"; + CPOperandKey = @"CPOperand", + CPExpressionTypeKey = @"CPExpressionType"; @implementation CPExpression_function (CPCoding) - (id)initWithCoder:(CPCoder)coder { - var target = [coder decodeObjectForKey:CPOperandKey], + var type = [coder decodeIntForKey:CPExpressionTypeKey], + operand = [coder decodeObjectForKey:CPOperandKey], selector = CPSelectorFromString([coder decodeObjectForKey:CPSelectorNameKey]), - arguments = [coder decodeObjectForKey:CPArgumentsKey]; + parameters = [coder decodeObjectForKey:CPArgumentsKey]; - return [self initWithTarget:target selector:selector arguments:arguments]; + return [self initWithTarget:operand selector:selector arguments:parameters type:type]; } - (void)encodeWithCoder:(CPCoder)coder @@ -127,6 +134,7 @@ var CPSelectorNameKey = @"CPSelectorName", [coder encodeObject:[self _function] forKey:CPSelectorNameKey]; [coder encodeObject:_arguments forKey:CPArgumentsKey]; [coder encodeObject:_operand forKey:CPOperandKey]; + [coder encodeInt:_type forKey:CPExpressionTypeKey]; } @end diff --git a/Foundation/CPPredicate/CPExpression_keypath.j b/Foundation/CPPredicate/CPExpression_keypath.j index f5302a20c..9abf3b90f 100644 --- a/Foundation/CPPredicate/CPExpression_keypath.j +++ b/Foundation/CPPredicate/CPExpression_keypath.j @@ -17,11 +17,19 @@ { var arg = [CPExpression expressionForConstantValue:keyPath]; // Cocoa: if it's a direct path selector is valueForKey: - self = [super initWithTarget:operand selector:@selector(valueForKeyPath:) arguments:[arg]]; + self = [super initWithTarget:operand selector:@selector(valueForKeyPath:) arguments:[arg] type:CPKeyPathExpressionType]; return self; } +- (BOOL)isEqual:(id)object +{ + if (object === self) + return YES; + + return ([object keyPath] == [self keyPath]); //If it appears that parsing generates nested keypaths with different targets and same keyPath, comparing -keyPath won't work. +} + - (CPExpression)pathExpression { return [[self arguments] objectAtIndex:0]; diff --git a/Foundation/CPPredicate/CPExpression_self.j b/Foundation/CPPredicate/CPExpression_self.j index 1173fe48c..5e440519d 100644 --- a/Foundation/CPPredicate/CPExpression_self.j +++ b/Foundation/CPPredicate/CPExpression_self.j @@ -4,20 +4,15 @@ @import @import -var defaultInstance = nil; @implementation CPExpression_self : CPExpression { } - (id)init { - if (defaultInstance == nil) - { - [super initWithExpressionType:CPEvaluatedObjectExpressionType]; - defaultInstance = self; - } - - return defaultInstance; + [super initWithExpressionType:CPEvaluatedObjectExpressionType]; + + return self; } - (id)initWithCoder:(CPCoder)coder @@ -31,7 +26,7 @@ var defaultInstance = nil; - (BOOL)isEqual:(id)object { - return (object === self); + return (_type == CPEvaluatedObjectExpressionType); } - (id)expressionValueWithObject:(id)object context:(CPDictionary)context @@ -39,11 +34,6 @@ var defaultInstance = nil; return object; } -- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables -{ - return self; -} - - (CPString)description { return @"SELF"; diff --git a/Foundation/CPPredicate/CPExpression_set.j b/Foundation/CPPredicate/CPExpression_set.j index 38f5a542f..2f1ddbcba 100644 --- a/Foundation/CPPredicate/CPExpression_set.j +++ b/Foundation/CPPredicate/CPExpression_set.j @@ -58,6 +58,7 @@ - (CPExpression )_expressionWithSubstitutionVariables:(CPDictionary )variables { + // UNIMPLEMENTED return self; } diff --git a/Foundation/CPPredicate/CPExpression_subquery.j b/Foundation/CPPredicate/CPExpression_subquery.j new file mode 100644 index 000000000..1e5e94171 --- /dev/null +++ b/Foundation/CPPredicate/CPExpression_subquery.j @@ -0,0 +1,103 @@ +@import "CPExpression.j" + +@implementation CPExpression_subquery : CPExpression +{ + CPExpression _collection; + CPExpression _variableExpression; + CPPredicate _subpredicate; +} + +- (id)initWithExpression:(CPExpression)collection usingIteratorVariable:(CPString)variable predicate:(CPPredicate)subpredicate +{ + [super initWithExpressionType:CPSubqueryExpressionType]; + + _subpredicate = subpredicate; + _collection = collection; + _variableExpression = [CPExpression expressionForVariable:variable]; + + return self; +} + +- (id)expressionValueWithObject:(id)object context:(id)context +{ + var collection = [_collection expressionValueWithObject:object context:context], + count = [collection count], + result = [CPArray array], + + bindings = [CPDictionary dictionaryWithObject:[CPExpression expressionForEvaluatedObject] forKey:[self variable]]; + + for (var i = 0; i < count; i++) + { + var item = [collection objectAtIndex:i]; + if ([_subpredicate evaluateWithObject:item substitutionVariables:bindings]) + [result addObject:item]; + } + + return result; +} + +- (BOOL)isEqual:(id)object; +{ + if (self === object) + return YES; + + if (![_collection isEqual:[object collection]] || ![_subpredicate isEqual:[object predicate]]) + return NO; + + return YES; +} + +- (id)collection +{ + return _collection; +} + +- (id)copy +{ + return [[CPExpression_subquery alloc] initWithExpression:[_collection copy] usingIteratorExpression:[_variableExpression copy] predicate:[_subpredicate copy]]; +} + +- (id)predicate +{ + return _subpredicate; +} + +- (id)predicateFormat +{ + return @"SUBQUERY(" + [_collection description] + ", " + [_variableExpression description] + ", " + [_subpredicate predicateFormat] + ")"; +} + +- (id)variable +{ + return [_variableExpression variable]; +} + +- (id)variableExpression +{ + return _variableExpression; +} +@end + +var CPExpressionKey = @"CPExpression", + CPSubpredicateKey = @"CPSubpredicate", + CPVariableKey = @"CPVariable"; + +@implementation CPExpression_subquery (CPCoding) + +- (id)initWithCoder:(CPCoder)coder +{ + var collection = [coder decodeObjectForKey:CPExpressionKey], + subpredicate = [coder decodeObjectForKey:CPSubpredicateKey], + variable = [coder decodeObjectForKey:CPVariableKey]; + + return [self initWithExpression:collection usingIteratorVariable:variable predicate:subpredicate]; +} + +- (void)encodeWithCoder:(CPCoder)coder +{ + [coder encodeObject:_collection forKey:CPExpressionKey]; + [coder encodeObject:_subpredicate forKey:CPSubpredicateKey]; + [coder encodeObject:[variableExpression variable] forKey:CPVariableKey]; +} + +@end \ No newline at end of file diff --git a/Foundation/CPPredicate/CPExpression_variable.j b/Foundation/CPPredicate/CPExpression_variable.j index f87c4fd4f..866c4f61a 100644 --- a/Foundation/CPPredicate/CPExpression_variable.j +++ b/Foundation/CPPredicate/CPExpression_variable.j @@ -12,7 +12,7 @@ { [super initWithExpressionType:CPVariableExpressionType]; _variable = [variable copy]; - + return self; } @@ -20,10 +20,10 @@ { if (self == object) return YES; - + if (object.isa != self.isa || [object expressionType] != [self expressionType] || ![[object variable] isEqualToString:[self variable]]) return NO; - + return YES; } @@ -34,7 +34,9 @@ - (id)expressionValueWithObject:object context:(CPDictionary)context { - return [context objectForKey:_variable]; + var expression = [self _expressionWithSubstitutionVariables:context]; + + return [expression expressionValueWithObject:object context:context]; } - (CPString)description @@ -44,12 +46,14 @@ - (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables { - var aconstant = [variables objectForKey:_variable]; - - if (aconstant != nil) - return [CPExpression expressionForConstantValue:aconstant]; - - return self; + var value = [variables objectForKey:_variable]; + if (value == nil) + [CPException raise:CPInvalidArgumentException reason:@"Can't get value for '" + _variable + "' in bindings" + variables]; + + if ([value isKindOfClass:[CPExpression class]]) + return value; + + return [CPExpression expressionForConstantValue:value]; } @end diff --git a/Tests/Foundation/CPPredicateTest.j b/Tests/Foundation/CPPredicateTest.j index 921667ba5..718cb6edf 100644 --- a/Tests/Foundation/CPPredicateTest.j +++ b/Tests/Foundation/CPPredicateTest.j @@ -62,7 +62,7 @@ [self assertNotNull:expression_aggregate message:"Aggregate Expression should not be nil"]; var expression_subquery = [CPExpression expressionForSubquery:expression_collection usingIteratorVariable:@"self" predicate:[CPPredicate predicateWithValue:YES]]; - // [self assertNotNull:expression_subquery message:"Subquery Expression should not be nil"]; + [self assertNotNull:expression_subquery message:"Subquery Expression should not be nil"]; var set = [CPExpression expressionForConstantValue:[CPSet setWithObjects:@"a",@"b",@"c"]]; var array = [CPExpression expressionForConstantValue:[CPArray arrayWithObjects:@"a",@"b",@"d"]]; @@ -112,12 +112,34 @@ - (void)testVariableExpressionEvaluation { - var variable_exp = [CPExpression expressionForVariable:@"variable"]; - var pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForKeyPath:@"Record1.Age"] rightExpression:variable_exp modifier:CPDirectPredicateModifier type:CPGreaterThanPredicateOperatorType options:0]; +// Replace with constant + var expression = [CPExpression expressionForVariable:@"variable"], + bindings = [CPDictionary dictionaryWithObject:20 forKey:@"variable"], + eval = [expression expressionValueWithObject:@"variable" context:bindings]; + [self assertTrue:(eval == 20) message:"'"+ eval + "' should be 20"]; - var variables = [CPDictionary dictionaryWithObject:20 forKey:@"variable"]; +// Replace with constant expression + bindings = [CPDictionary dictionaryWithObject:[CPExpression expressionForConstantValue:10] forKey:@"variable"]; + eval = [expression expressionValueWithObject:nil context:bindings]; + [self assertTrue:(eval == 10) message:"'"+ eval + "' should be 10"]; - [self assertTrue:[pred evaluateWithObject:dict substitutionVariables:variables] message:"'"+ [pred description] + "' should be true"]; +// Replace with keypath expression + bindings = [CPDictionary dictionaryWithObject:[CPExpression expressionForKeyPath:@"Record1.Age"] forKey:@"variable"]; + eval = [expression expressionValueWithObject:dict context:bindings]; + [self assertTrue:(eval == 34) message:"'"+ eval + "' should be 34"]; +} + +- (void)testSubqueryExpressionEvaluation +{ + var collection = [CPExpression expressionForKeyPath:@"Record1.Children"], + iteratorVariable = @"x", + predicate = [CPPredicate predicateWithFormat:@"$x BEGINSWITH 'Kid'"]; + + var expression = [CPExpression expressionForSubquery:collection usingIteratorVariable:iteratorVariable predicate:predicate]; + + var eval = [expression expressionValueWithObject:dict context:nil]; + var expected = [CPArray arrayWithObjects:"Kid1", "Kid2"]; + [self assertTrue:([eval isEqual:expected]) message:"'"+ [expression predicateFormat] + "' result is "+ eval + "but should be " + expected]; } - (void)testOptions @@ -281,8 +303,19 @@ predicate = [CPPredicate predicateWithFormat: @"a BETWEEN {%f,%f}", 20, 40]; [self assertTrue:([predicate predicateOperatorType] == CPBetweenPredicateOperatorType) message:[predicate description] + " operator should be a CPBetweenPredicateOperatorType"]; +// TEST Empty string predicate = [CPPredicate predicateWithFormat: @"a CONTAINS \"\""]; [self assertNotNull:predicate message:[predicate description] + " should not be nil"]; + +// TEST variable + predicate = [CPPredicate predicateWithFormat: @"$x CONTAINS \"\""]; + [self assertTrue:[[predicate leftExpression] expressionType] == CPVariableExpressionType message:"Left Expression should be a CPVariableExpressionType"]; +/* + Variable in multiple paths will fail. Left exp should be FUNCTION(keyPathExp"keypath", valueForKey:, variableExp"variable") + predicate = [CPPredicate predicateWithFormat: @"keypath.$variable CONTAINS \"\""]; + var type = [[predicate leftExpression] expressionType]; + [self assertTrue: type == CPFunctionExpressionType message:"Left Expression should be a CPFunctionExpressionType is " + type]; +*/ } @end