Module: LowCardTables::ActiveRecord::Scoping::ClassMethods

Defined in:
lib/low_card_tables/active_record/scoping.rb

Instance Method Summary collapse

Instance Method Details

#scope(name, scope_options = {}, &block) ⇒ Object

Overrides #scope to check for statically-defined scopes against low-card attributes, as discussed in the comment for LowCardTables::ActiveRecord::Scoping.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/low_card_tables/active_record/scoping.rb', line 31

def scope(name, scope_options = {}, &block)
  # First, go invoke the superclass method.
  out = super(name, scope_options, &block)

  # We're safe if it's not a statically-defined scope.
  return out if block
  # We're also safe if this class doesn't refer to any low-card tables, because then it could not possibly
  # have been constraining on any low-card columns.
  return out if (! self.has_any_low_card_tables?)
  # If you defined a scope that isn't an actual ::ActiveRecord::Relation, you're fine.
  return out unless scope_options.kind_of?(::ActiveRecord::Relation)

  # ::ActiveRecord::Relation#where_values gets you a list of the 'where clauses' applied in the relation.
  used_associations = scope_options.where_values.map do |where_value|
    # Let's grab the SQL...
    sql = if where_value.respond_to?(:to_sql)
      where_value.to_sql
    elsif where_value.kind_of?(String)
      where_value
    end

    # ...and just search it for the foreign-key name. Is this foolproof? No; it's possible that you'll get some
    # false positives. Is this a big deal? No -- because changing a static scope to dynamic really has no
    # drawbacks at all, so there's a trivial fix for any false positives.
    self._low_card_associations_manager.associations.select do |association|
      foreign_key = association.foreign_key_column_name
      sql =~ /#{foreign_key}/i
    end
  end.flatten

  # Here's where we check for our problem and blow up if it's there.
  if used_associations.length > 0
    raise LowCardTables::Errors::LowCardStaticScopeError, %{You defined a named scope, #{name.inspect}, on model #{self.name}. This scope
appears to constrain on the following foreign keys, which point to low-card tables.
Because this scope is defined statically (e.g., 'scope :foo, where(...)' rather than
'scope :foo { where(...) }'), these conditions will only be evaluated a single time,
at startup.

This means that if additional low-card rows get created that match the criteria for
this scope, they will never be picked up no matter what (as the WHERE clause is
frozen in time forever), and you will miss critical data.

The fix for this is simple: define this scope dynamically (i.e., enclose the
call to #where in a block). This will cause the conditions to be evaluated every
time you use it, thus updating the set of IDs used on every call, properly.

The foreign keys you appear to be constraining on are:

#{used_associations.map(&:foreign_key_column_name).sort.join(", ")}}
  end

  out
end