module CDQ #:nodoc: class CDQTargetedQuery < CDQQuery include Enumerable attr_reader :entity_description # Create a new CDQTargetedContext. Takes an entity description, an optional # implementation class, and a hash of options that will be passed to the CDQQuery # constructor. # def initialize(entity_description, target_class = nil, opts = {}) @entity_description = entity_description @target_class = target_class || NSClassFromString(entity_description.managedObjectClassName) || CDQManagedObject @context = opts.delete(:context) super(opts) end # The current context, taken from the environment or overriden by in_context # def context @context || contexts.current end # Return the number of matching entities. # # Causes execution. # def count raise("No context has been set. Probably need to run cdq.setup") unless context with_error_object(0) do |error| context.countForFetchRequest(fetch_request, error:error) end end alias :length :count alias :size :count # Return all matching entities. # # Causes execution. # def array raise("No context has been set. Probably need to run cdq.setup") unless context with_error_object([]) do |error| context.executeFetchRequest(fetch_request, error:error) end end alias_method :to_a, :array # Convenience method for referring to all matching entities. No-op. You must # still call array or another executing method # def all self end # Return the first entity matching the query. # # Causes execution. # def first(n = 1) result = limit(n).array n == 1 ? result.first : result end # Return the last entity matching the query. # # Causes execution. # def last(n = 1) result = offset(count - n).limit(n).array n == 1 ? result.first : result end # Fetch a single entity from the query by index. If the optional # length parameter is supplied, fetch a range of length length # starting at index # # Causes execution. # def [](index, length = nil) if length offset(index).limit(length).array else offset(index).first end end # Iterate over each entity matched by the query. You can also use any method from the # Enumerable module in the standard library that does not depend on ordering. # # Causes execution. # def each(*args, &block) array.each(*args, &block) end # Calculation method based on core data aggregate functions. def calculate(operation, column_name) raise("No context has been set. Probably need to run cdq.setup") unless context raise("Cannot find attribute #{column_name} while calculating #{operation}") unless @entity_description.attributesByName[column_name.to_s] desc_name = operation.to_s + '_of_' + column_name.to_s fr = fetch_request.tap do |req| req.propertiesToFetch = [ NSExpressionDescription.alloc.init.tap do |desc| desc.name = desc_name desc.expression = NSExpression.expressionForFunction(operation.to_s + ':', arguments: [ NSExpression.expressionForKeyPath(column_name.to_s) ]) desc.expressionResultType = @entity_description.attributesByName[column_name.to_s].attributeType end ] req.resultType = NSDictionaryResultType end with_error_object([]) do |error| r = context.executeFetchRequest(fr, error:error) r.first[desc_name] end end # Calculates the sum of values on a given column. # # Author.sum(:fee) # => 6.0 def sum(column_name) calculate(:sum, column_name) end # Calculates the average of values on a given column. # # Author.average(:fee) # => 2.0 def average(column_name) calculate(:average, column_name) end # Calculates the minimum of values on a given column. # # Author.min(:fee) # => 1.0 def min(column_name) calculate(:min, column_name) end alias :minimum :min # Calculates the maximum of values on a given column. # # Author.max(:fee) # => 3.0 def max(column_name) calculate(:max, column_name) end alias :maximum :max # Returns the fully-contstructed fetch request, which can be executed outside of CDQ. # def fetch_request super.tap do |req| req.entity = @entity_description req.predicate ||= NSPredicate.predicateWithValue(true) end end # Create a new entity in the current context. Accepts a hash of attributes that will be assigned to # the newly-created entity. Does not save the context. # def new(opts = {}) @target_class.alloc.initWithEntity(@entity_description, insertIntoManagedObjectContext: context).tap do |entity| opts.each { |k, v| entity.send("#{k}=", v) } end end # Create a new entity in the current context. Accepts a hash of attributes that will be assigned to # the newly-created entity. Does not save the context. # def create(*args) new(*args) end # Create a named scope. The query is any valid CDQ query. # # Example: # # cdq('Author').scope(:first_published, cdq(:published).eq(true).sort_by(:published_at).limit(1)) # # cdq('Author').first_published.first => # # def scope(name, query = nil, &block) if query.nil? && block_given? named_scopes[name] = block elsif query named_scopes[name] = query else raise ArgumentError.new("You must supply a query OR a block that returns a query to scope") end end # Override the context in which to perform this query. This forever forces the # specified context for this particular query, so if you save the it for later # use (such as defining a scope) bear in mind that changes in the default context # will have no effect when running this. # def in_context(context) clone(context: context) end # Any unknown method will be checked against the list of named scopes. # def method_missing(name, *args) scope = named_scopes[name] case scope when CDQQuery where(scope) when Proc where(scope.call(*args)) else super(name, *args) end end def log(log_type = nil) out = "\n\n ATTRIBUTES" out << " \n Name | type | default |" line = " \n- - - - - - - - - - - | - - - - - - - - - - | - - - - - - - - - - - - - - - |" out << line entity_description.attributesByName.each do |name, desc| out << " \n #{name.ljust(21)}|" out << " #{desc.attributeValueClassName.ljust(20)}|" out << " #{desc.defaultValue.to_s.ljust(30)}|" end out << line out << "\n\n" self.each do |o| out << o.log(:string) end if log_type == :string out else NSLog out end end private def oid(obj) obj ? obj.oid : "nil" end def named_scopes @@named_scopes ||= {} @@named_scopes[@entity_description] ||= {} end def clone(opts = {}) CDQTargetedQuery.new(@entity_description, @target_class, locals.merge(opts)) end end end