module CDQ # # CDQ Queries are the primary way of describing a set of objects. # class CDQQuery < CDQObject # @private # # This is a singleton object needed to represent "empty" in limit and # offset, because they need to be able to accept nil as a real value. # EMPTY = Object.new attr_reader :predicate, :sort_descriptors def initialize(opts = {}) @predicate = opts[:predicate] @limit = opts[:limit] @offset = opts[:offset] @sort_descriptors = opts[:sort_descriptors] || [] @saved_key = opts[:saved_key] end # Return or set the fetch limit. If passed an argument, return a new # query with the specified limit value. Otherwise, return the current # value. # def limit(value = EMPTY) if value == EMPTY @limit else clone(limit: value) end end # Return or set the fetch offset. If passed an argument, return a new # query with the specified offset value. Otherwise, return the current # value. # def offset(value = EMPTY) if value == EMPTY @offset else clone(offset: value) end end # Combine this query with others in an intersection ("and") relationship. Can be # used to begin a new query as well, especially when called in its where # variant. # # The query passed in can be a wide variety of types: # # Symbol: This is by far the most common, and it is also a special # case -- the return value when passing a symbol is a CDQPartialPredicate, # rather than CDQQuery. Methods on CDQPartialPredicate are then comparison # operators against the attribute indicated by the symbol itself, which take # a value operand. For example: # # query.where(:name).equal("Chuck").and(:title).not_equal("Manager") # # @see CDQPartialPredicate # # String: Interpreted as an NSPredicate format string. Additional arguments are # the positional parameters. # # NilClass: If the argument is nil (most likely because it was omitted), and there # was a previous use of a symbol, then reuse that last symbol. For example: # # query.where(:name).contains("Chuck").and.contains("Norris") # # CDQQuery: If you have another CDQQuery from somewhere else, you can pass it in directly. # # NSPredicate: You can pass in a raw NSPredicate and it will work as you'd expect. # # Hash: Each key/value pair is treated as equality and anded together. # def and(query = nil, *args) merge_query(query, :and, *args) do |left, right| NSCompoundPredicate.andPredicateWithSubpredicates([left, right]) end end alias_method :where, :and # Combine this query with others in a union ("or") relationship. Accepts # all the same argument types as and. def or(query = nil, *args) merge_query(query, :or, *args) do |left, right| NSCompoundPredicate.orPredicateWithSubpredicates([left, right]) end end # Add a new sort key. Multiple invocations add additional sort keys rather than replacing # old ones. # # @param key The attribute to sort on # @param dir The sort direction (default = :ascending) # def sort_by(key, dir = :ascending) if dir.to_s[0,4].downcase == 'desc' ascending = false else ascending = true end clone(sort_descriptors: @sort_descriptors + [NSSortDescriptor.sortDescriptorWithKey(key, ascending: ascending)]) end # Return an NSFetchRequest that will implement this query def fetch_request NSFetchRequest.new.tap do |req| req.predicate = predicate req.fetchLimit = limit if limit req.fetchOffset = offset if offset req.sortDescriptors = sort_descriptors unless sort_descriptors.empty? end end private # Create a new query with the same values as this one, optionally overriding # any of them in the options def clone(opts = {}) self.class.new(locals.merge(opts)) end def locals { sort_descriptors: sort_descriptors, predicate: predicate, limit: limit, offset: offset } end def merge_query(query, operation, *args, &block) key_to_save = nil case query when Hash subquery = query.inject(CDQQuery.new) do |memo, (key, value)| memo.and(key).eq(value) end other_predicate = subquery.predicate new_limit = limit new_offset = offset new_sort_descriptors = sort_descriptors when Symbol return CDQPartialPredicate.new(query, self, operation) when NilClass if @saved_key return CDQPartialPredicate.new(@saved_key, self, operation) else raise "Zero-argument 'and' and 'or' can only be used if there is a key in the preceding predicate" end when CDQQuery new_limit = [limit, query.limit].compact.last new_offset = [offset, query.offset].compact.last new_sort_descriptors = sort_descriptors + query.sort_descriptors other_predicate = query.predicate when NSPredicate other_predicate = query new_limit = limit new_offset = offset new_sort_descriptors = sort_descriptors key_to_save = args.first when String other_predicate = NSPredicate.predicateWithFormat(query, argumentArray: args) new_limit = limit new_offset = offset new_sort_descriptors = sort_descriptors end if predicate new_predicate = block.call(predicate, other_predicate) else new_predicate = other_predicate end clone(predicate: new_predicate, limit: new_limit, offset: new_offset, sort_descriptors: new_sort_descriptors, saved_key: key_to_save) end end end