require 'mcfly' module Mcfly module Model def self.included(base) base.send :extend, ClassMethods end module ClassMethods def clear_cache @LOOKUP_CACHE.clear if @LOOKUP_CACHE end # Implements a VERY HACKY class-based caching mechanism for # database lookup results. Issues include: cached values are # ActiveRecord objects. Not sure if these should be shared # across connections. Query results can potentially be very # large lists which we simply count as one item in the cache. # Caching mechanism will result in large processes. Caches are # not sharable across different Ruby processes. def cached_delorean_fn(name, options = {}, &block) @LOOKUP_CACHE ||= {} delorean_fn(name, options) do |ts, *args| cache_key = [name, ts] + args.map{ |a| a.is_a?(ActiveRecord::Base) ? a.id : a } unless Mcfly.is_infinity(ts) next @LOOKUP_CACHE[cache_key] if cache_key && @LOOKUP_CACHE.has_key?(cache_key) res = block.call(ts, *args) if cache_key # Cache has >1000 items, clear out the oldest 200. FIXME: # hard-coded, should be configurable. Cache # size/invalidation should be per lookup and not class. # We're invalidating cache items simply based on age and # not usage. This is faster but not as fair. if @LOOKUP_CACHE.count > 1000 @LOOKUP_CACHE.keys[0..200].each{|k| @LOOKUP_CACHE.delete(k)} end @LOOKUP_CACHE[cache_key] = res # Since we're caching this object and don't want anyone # changing it. FIXME: ideally should freeze this object # recursively. res.freeze unless res.is_a?(ActiveRecord::Relation) end res end end # FIXME: duplicate code from Mcfly's mcfly_lookup. def cached_mcfly_lookup(name, options = {}, &block) cached_delorean_fn(name, options) do |ts, *args| raise "time cannot be nil" if ts.nil? ts = Mcfly.normalize_infinity(ts) where("obsoleted_dt >= ? AND created_dt < ?", ts, ts).scoping do block.call(ts, *args) end end end # FIXME: for validation purposes, this mechanism should make # sure that the allable attrs are not required. def gen_mcfly_lookup(name, attrs, options={}) raise "bad options" unless options.is_a?(Hash) # FIXME: mode should be sent later, not as a part of # gen_mcfly_lookup. i.e. we just generate the search and the # mode is applied at runtime by delorean code. That would # allow lookups to be used in either mode dynamically. mode = options.fetch(:mode, :first) assoc = Set.new(self.reflect_on_all_associations.map(&:name)) attr_names = attrs.keys allables = attrs.select {|k, v| v} order = allables.keys.reverse.map { |k| k = "#{k}_id" if assoc.member?(k) "#{k} NULLS LAST" }.join(", ") qstr = attrs.map {|k, v| k = "#{k}_id" if assoc.member?(k) v ? "(#{k} = ? OR #{k} IS NULL)" : "(#{k} = ?)" }.join(" AND ") cached_mcfly_lookup(name, sig: attrs.length+1) do |t, *attr_list| attr_list_ids = attr_list.each_with_index.map {|x, i| assoc.member?(attr_names[i]) ? (attr_list[i] && attr_list[i].id) : attr_list[i] } q = self.where(qstr, *attr_list_ids) q = q.order(order) unless order.empty? mode ? q.send(mode) : q end end ###################################################################### # Generates Gemini categorization lookups. For instance, # suppose we have the following in class GFee: # # gen_mcfly_lookup_cat :lookup_q, # [:security_instrument, # 'Gemini::SecurityInstrumentCategorization', # :g_fee_category], # { # entity: true, # security_instrument: true, # coupon: true, # }, # nil # In the above case, # rel_attr = :security_instrument # cat_assoc_klass = Gemini::SecurityInstrumentCategorization # cat_attr = :g_fee_category # name = :lookup_q # pc_name = :pc_lookup_q # pc_attrs = {entity: true, security_instrument: true, # g_fee_category: true, coupon: true} def gen_mcfly_lookup_cat(name, catrel, attrs, options={}) rel_attr, cat_assoc_name, cat_attr = catrel raise "#{rel_attr} should be mapped in attrs" if attrs[rel_attr].nil? cat_assoc_klass = cat_assoc_name.constantize raise "need lookup method on #{cat_assoc_klass}" unless cat_assoc_klass.respond_to? :lookup # replace rel_attr with cat_attr in attrs pc_attrs = attrs.each_with_object({}) {|(k, v), h| h[k == rel_attr ? cat_attr : k] = v } pc_name = "pc_#{name}".to_sym gen_mcfly_lookup(pc_name, pc_attrs, options) lpi = attrs.keys.index rel_attr raise "should not include #{cat_attr}" if attrs.member?(cat_attr) raise "need #{rel_attr} argument" unless lpi delorean_fn(name, sig: attrs.length+1) do |ts, *args| # Example: rel is a Gemini::SecurityInstrument instance. rel = args[lpi] raise "#{rel_attr} can't be nil" unless rel # Assumes there's a mcfly :lookup function on # cat_assoc_klass. categorizing_obj = cat_assoc_klass.lookup(ts, rel) raise "no categorization #{cat_assoc_klass} for #{rel}" unless categorizing_obj pc = categorizing_obj.send(cat_attr) raise ("#{categorizing_obj} must have assoc." + " #{cat_attr}/#{rel.inspect}") unless pc args[lpi] = pc self.send(pc_name, ts, *args) end end end end end module Mcfly::Controller # define mcfly user to be Flowscape's current_user. def user_for_mcfly find_current_user rescue nil end end