yoomee-searchlogic 2.4.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/.gitignore +6 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +308 -0
  4. data/Rakefile +35 -0
  5. data/VERSION.yml +5 -0
  6. data/init.rb +1 -0
  7. data/lib/searchlogic.rb +56 -0
  8. data/lib/searchlogic/active_record/association_proxy.rb +19 -0
  9. data/lib/searchlogic/active_record/consistency.rb +49 -0
  10. data/lib/searchlogic/active_record/named_scope_tools.rb +101 -0
  11. data/lib/searchlogic/core_ext/object.rb +43 -0
  12. data/lib/searchlogic/core_ext/proc.rb +17 -0
  13. data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
  14. data/lib/searchlogic/named_scopes/association_conditions.rb +132 -0
  15. data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
  16. data/lib/searchlogic/named_scopes/conditions.rb +232 -0
  17. data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
  18. data/lib/searchlogic/named_scopes/ordering.rb +48 -0
  19. data/lib/searchlogic/rails_helpers.rb +79 -0
  20. data/lib/searchlogic/search.rb +26 -0
  21. data/lib/searchlogic/search/base.rb +26 -0
  22. data/lib/searchlogic/search/conditions.rb +58 -0
  23. data/lib/searchlogic/search/date_parts.rb +23 -0
  24. data/lib/searchlogic/search/implementation.rb +14 -0
  25. data/lib/searchlogic/search/method_missing.rb +123 -0
  26. data/lib/searchlogic/search/ordering.rb +10 -0
  27. data/lib/searchlogic/search/scopes.rb +19 -0
  28. data/lib/searchlogic/search/to_yaml.rb +38 -0
  29. data/lib/searchlogic/search/unknown_condition_error.rb +15 -0
  30. data/rails/init.rb +1 -0
  31. data/searchlogic.gemspec +98 -0
  32. data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
  33. data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
  34. data/spec/searchlogic/core_ext/object_spec.rb +9 -0
  35. data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
  36. data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
  37. data/spec/searchlogic/named_scopes/association_conditions_spec.rb +203 -0
  38. data/spec/searchlogic/named_scopes/association_ordering_spec.rb +27 -0
  39. data/spec/searchlogic/named_scopes/conditions_spec.rb +319 -0
  40. data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
  41. data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
  42. data/spec/searchlogic/search_spec.rb +497 -0
  43. data/spec/spec_helper.rb +132 -0
  44. metadata +136 -0
@@ -0,0 +1,49 @@
1
+ module Searchlogic
2
+ module ActiveRecord
3
+ # Active Record is pretty inconsistent with how their SQL is constructed. This
4
+ # method attempts to close the gap between the various inconsistencies.
5
+ module Consistency
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ alias_method_chain :merge_joins, :singularity
9
+ alias_method_chain :merge_joins, :consistent_conditions
10
+ alias_method_chain :merge_joins, :merged_duplicates
11
+ end
12
+ end
13
+
14
+ # In AR multiple joins are sometimes in a single join query, and other times they
15
+ # are not. The merge_joins method in AR should account for this, but it doesn't.
16
+ # This fixes that problem. This way there is one join per string, which allows
17
+ # the merge_joins method to delete duplicates.
18
+ def merge_joins_with_singularity(*args)
19
+ joins = merge_joins_without_singularity(*args)
20
+ joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
21
+ end
22
+
23
+ # This method ensures that the order of the conditions in the joins are the same.
24
+ # The strings of the joins MUST be exactly the same for AR to remove the duplicates.
25
+ # AR is not consistent in this approach, resulting in duplicate joins errors when
26
+ # combining scopes.
27
+ def merge_joins_with_consistent_conditions(*args)
28
+ joins = merge_joins_without_consistent_conditions(*args)
29
+ joins.collect do |j|
30
+ if j.is_a?(String) && (j =~ / (AND|OR) /i).nil?
31
+ j.gsub(/(.*) ON (.*) = (.*)/) do |m|
32
+ join, cond1, cond2 = $1, $2, $3
33
+ sorted = [cond1.gsub(/\(|\)/, ""), cond2.gsub(/\(|\)/, "")].sort
34
+ "#{join} ON #{sorted[0]} = #{sorted[1]}"
35
+ end
36
+ else
37
+ j
38
+ end
39
+ end.uniq
40
+ end
41
+
42
+
43
+ def merge_joins_with_merged_duplicates(*args)
44
+ args << "" if !Thread.current["searchlogic_delegation"]
45
+ joins = merge_joins_without_merged_duplicates(*args)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,101 @@
1
+ module Searchlogic
2
+ module ActiveRecord
3
+ # Adds methods that give extra information about a classes named scopes.
4
+ module NamedScopeTools
5
+ # Retrieves the options passed when creating the respective named scope. Ex:
6
+ #
7
+ # named_scope :whatever, :conditions => {:column => value}
8
+ #
9
+ # This method will return:
10
+ #
11
+ # :conditions => {:column => value}
12
+ #
13
+ # ActiveRecord hides this internally in a Proc, so we have to try and pull it out with this
14
+ # method.
15
+ def named_scope_options(name)
16
+ key = scopes.key?(name.to_sym) ? name.to_sym : condition_scope_name(name)
17
+
18
+ if key
19
+ eval("options", scopes[key].binding)
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ # The arity for a named scope's proc is important, because we use the arity
26
+ # to determine if the condition should be ignored when calling the search method.
27
+ # If the condition is false and the arity is 0, then we skip it all together. Ex:
28
+ #
29
+ # User.named_scope :age_is_4, :conditions => {:age => 4}
30
+ # User.search(:age_is_4 => false) == User.all
31
+ # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
32
+ #
33
+ # We also use it when trying to "copy" the underlying named scope for association
34
+ # conditions. This way our aliased scope accepts the same number of parameters for
35
+ # the underlying scope.
36
+ def named_scope_arity(name)
37
+ options = named_scope_options(name)
38
+ options.respond_to?(:arity) ? options.arity : nil
39
+ end
40
+
41
+ # When searchlogic calls a named_scope on a foreigh model it will execute that scope and then call scope(:find).
42
+ # When we get these options we want this to be in an exclusive scope, especially if we are calling a condition on
43
+ # the same originating model:
44
+ #
45
+ # Company.users_company_name_equals("name")
46
+ #
47
+ # If we aren't in an exclusive scope we will get unexpected results for the :joins option. Lastly, we want the named_scopes
48
+ # generated by searchlogic to be symbols whenever possible. The reason for this is so that we can allow
49
+ # ActiveRecord to leverage its joins library that automatically aliases joins if they appear more than once in a query.
50
+ # If the joins are strings, AtiveRecord can't do anything. Because the code that does this in ActiveRecord is pretty bad
51
+ # when it comes to being consisitent, searchlogic had to fix this in Searchloigc::ActiveRecord::Consistency. That said,
52
+ # part of this fix is to normalize joins into strings. We do not want to do this if we are calling scopes on foreigh models.
53
+ # Only when we are performing an action on it. This is what the searchlogic_delegation thread variable is all about. A
54
+ # flag to let search logic know not to convert joins to strings.
55
+ def in_searchlogic_delegation(&block)
56
+ old = Thread.current["searchlogic_delegation"]
57
+ Thread.current["searchlogic_delegation"] = true
58
+ with_exclusive_scope(&block)
59
+ Thread.current["searchlogic_delegation"] = old
60
+ end
61
+
62
+ # A convenience method for creating inner join sql to that your inner joins
63
+ # are consistent with how Active Record creates them. Basically a tool for
64
+ # you to use when writing your own named scopes. This way you know for sure
65
+ # that duplicate joins will be removed when chaining scopes together that
66
+ # use the same join.
67
+ #
68
+ # Also, don't worry about breaking up the joins or retriving multiple joins.
69
+ # ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
70
+ # breaking up your joins so that they are unique.
71
+ def inner_joins(association_name)
72
+ ::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
73
+ end
74
+
75
+ # A convenience methods to create a join on a polymorphic associations target.
76
+ # Ex:
77
+ #
78
+ # Audit.belong_to :auditable, :polymorphic => true
79
+ # User.has_many :audits, :as => :auditable
80
+ #
81
+ # Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
82
+ # "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
83
+ #
84
+ # This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
85
+ def inner_polymorphic_join(target, options = {})
86
+ options[:on] ||= table_name
87
+ options[:on_table_name] ||= connection.quote_table_name(options[:on])
88
+ options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
89
+ options[:as] ||= "owner"
90
+ postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
91
+ "INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
92
+ "#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
93
+ end
94
+
95
+ # See inner_joins. Does the same thing except creates LEFT OUTER joins.
96
+ def left_outer_joins(association_name)
97
+ ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ # Contains extensions for the Object class that Searchlogic uses.
4
+ module Object
5
+ # Searchlogic needs to know the expected type of the condition value so that it can properly cast
6
+ # the value in the Searchlogic::Search object. For example:
7
+ #
8
+ # search = User.search(:id_gt => "1")
9
+ #
10
+ # You would expect this:
11
+ #
12
+ # search.id_gt => 1
13
+ #
14
+ # Not this:
15
+ #
16
+ # search.id_gt => "1"
17
+ #
18
+ # Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
19
+ # does when you instantiate a new User object.
20
+ #
21
+ # The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
22
+ # for your named scope. So instead of this:
23
+ #
24
+ # named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
25
+ #
26
+ # You need to do this:
27
+ #
28
+ # named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
29
+ #
30
+ # If you are wanting a string, you don't have to do anything, because Searchlogic assumes you want a string.
31
+ # If you want something else, you need to specify it as I did in the above example. Comments are appreciated
32
+ # on this, if you know of a better solution please let me know. But this is the best I could come up with,
33
+ # without being intrusive and altering default behavior.
34
+ def searchlogic_lambda(type = :string, options = {}, &block)
35
+ proc = lambda(&block)
36
+ proc.searchlogic_options ||= {}
37
+ proc.searchlogic_options[:type] = type
38
+ proc.searchlogic_options.merge!(options)
39
+ proc
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ module Searchlogic
2
+ module CoreExt
3
+ module Proc # :nodoc:
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ attr_accessor :searchlogic_options
7
+
8
+ def searchlogic_options
9
+ @searchlogic_options ||= {}
10
+ @searchlogic_options[:type] ||= :string
11
+ @searchlogic_options
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Adds the ability to create alias scopes that allow you to alias a named
4
+ # scope or create a named scope procedure. See the alias_scope method for a more
5
+ # detailed explanation.
6
+ module AliasScope
7
+ # In some instances you might create a class method that essentially aliases a named scope
8
+ # or represents a named scope procedure. Ex:
9
+ #
10
+ # class User
11
+ # def teenager
12
+ # age_gte(13).age_lte(19)
13
+ # end
14
+ # end
15
+ #
16
+ # This is obviously a very basic example, but notice how we are utilizing already existing named
17
+ # scopes so that we do not have to repeat ourself. This method makes a lot more sense when you are
18
+ # dealing with complicated named scope.
19
+ #
20
+ # There is a problem though. What if you want to use this in your controller's via the 'search' method:
21
+ #
22
+ # User.search(:teenager => true)
23
+ #
24
+ # You would expect that to work, but how does Searchlogic::Search tell the difference between your
25
+ # 'teenager' method and the 'destroy_all' method. It can't, there is no way to tell unless we actually
26
+ # call the method, which we obviously can not do.
27
+ #
28
+ # The being said, we need a way to tell searchlogic that this is method is safe. Here's how you do that:
29
+ #
30
+ # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
31
+ #
32
+ # This feels better, it feels like our other scopes, and it provides a way to tell Searchlogic that this
33
+ # is a safe method.
34
+ def alias_scope(name, options = nil)
35
+ alias_scopes[name.to_sym] = options
36
+ (class << self; self end).instance_eval do
37
+ define_method name do |*args|
38
+ case options
39
+ when Symbol
40
+ send(options)
41
+ else
42
+ options.call(*args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ alias_method :scope_procedure, :alias_scope
48
+
49
+ def alias_scopes # :nodoc:
50
+ read_inheritable_attribute(:alias_scopes) || write_inheritable_attribute(:alias_scopes, {})
51
+ end
52
+
53
+ def alias_scope?(name) # :nodoc:
54
+ return false if name.blank?
55
+ alias_scopes.key?(name.to_sym)
56
+ end
57
+
58
+ def condition?(name) # :nodoc:
59
+ super || alias_scope?(name)
60
+ end
61
+
62
+ def named_scope_options(name) # :nodoc:
63
+ super || alias_scopes[name.to_sym]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,132 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
4
+ module AssociationConditions
5
+ def condition?(name) # :nodoc:
6
+ super || association_condition?(name)
7
+ end
8
+
9
+ private
10
+ def association_condition?(name)
11
+ !association_condition_details(name).nil? unless name.to_s.downcase.match("_or_")
12
+ end
13
+
14
+ def method_missing(name, *args, &block)
15
+ if !local_condition?(name) && details = association_condition_details(name)
16
+ create_association_condition(details[:association], details[:condition], args, details[:poly_class])
17
+ send(name, *args)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def association_condition_details(name, last_condition = nil)
24
+ non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
25
+ poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
26
+ return nil if non_poly_assocs.empty? && poly_assocs.empty?
27
+
28
+ name_with_condition = [name, last_condition].compact.join('_')
29
+
30
+ association_name = nil
31
+ poly_type = nil
32
+ condition = nil
33
+
34
+ if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
35
+ association_name = $1
36
+ condition = $2
37
+ elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+?)_type_(\w+)$/
38
+ association_name = $1
39
+ poly_type = $2
40
+ condition = $3
41
+ end
42
+
43
+ if association_name && condition
44
+ association = reflect_on_association(association_name.to_sym)
45
+ klass = poly_type ? poly_type.camelcase.constantize : association.klass
46
+ if klass.condition?(condition)
47
+ {:association => association, :poly_class => poly_type && klass, :condition => condition}
48
+ else
49
+ nil
50
+ end
51
+ end
52
+ end
53
+
54
+ def create_association_condition(association, condition_name, args, poly_class = nil)
55
+ name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
56
+ named_scope(name, association_condition_options(association, condition_name, args, poly_class))
57
+ end
58
+
59
+ def association_condition_options(association, association_condition, args, poly_class = nil)
60
+ klass = poly_class ? poly_class : association.klass
61
+ scope = klass.send(association_condition, *args)
62
+ scope_options = klass.named_scope_options(association_condition)
63
+ arity = klass.named_scope_arity(association_condition)
64
+
65
+ if !arity || arity == 0
66
+ # The underlying condition doesn't require any parameters, so let's just create a simple
67
+ # named scope that is based on a hash.
68
+ options = {}
69
+ in_searchlogic_delegation { options = scope.scope(:find) }
70
+ prepare_named_scope_options(options, association, poly_class)
71
+ options
72
+ else
73
+ proc_args = arity_args(arity)
74
+ scope_options = scope_options.respond_to?(:searchlogic_options) ? scope_options.searchlogic_options.clone : {}
75
+ arg_type = scope_options.delete(:type) || :string
76
+
77
+ eval <<-"end_eval"
78
+ searchlogic_lambda(:#{arg_type}, #{scope_options.inspect}) { |#{proc_args.join(",")}|
79
+ options = {}
80
+
81
+ in_searchlogic_delegation do
82
+ scope = klass.send(association_condition, #{proc_args.join(",")})
83
+ options = scope.scope(:find) if scope
84
+ end
85
+
86
+
87
+ prepare_named_scope_options(options, association, poly_class)
88
+ options
89
+ }
90
+ end_eval
91
+ end
92
+ end
93
+
94
+ # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
95
+ # new scope as best as possible instead of taking the easy way out and using *args.
96
+ def arity_args(arity)
97
+ args = []
98
+ if arity > 0
99
+ arity.times { |i| args << "arg#{i}" }
100
+ else
101
+ positive_arity = arity * -1
102
+ positive_arity.times do |i|
103
+ if i == (positive_arity - 1)
104
+ args << "*arg#{i}"
105
+ else
106
+ args << "arg#{i}"
107
+ end
108
+ end
109
+ end
110
+ args
111
+ end
112
+
113
+ def prepare_named_scope_options(options, association, poly_class = nil)
114
+ options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
115
+
116
+ klass = poly_class || association.klass
117
+ # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
118
+ options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
119
+
120
+ poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
121
+
122
+ if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
123
+ options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
124
+ elsif poly_class
125
+ options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
126
+ else
127
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,44 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating order named scopes for associations:
4
+ #
5
+ # User.has_many :orders
6
+ # Order.has_many :line_items
7
+ # LineItem
8
+ #
9
+ # User.ascend_by_orders_line_items_id
10
+ #
11
+ # See the README for a more detailed explanation.
12
+ module AssociationOrdering
13
+ def condition?(name) # :nodoc:
14
+ super || association_ordering_condition?(name)
15
+ end
16
+
17
+ private
18
+ def association_ordering_condition?(name)
19
+ !association_ordering_condition_details(name).nil?
20
+ end
21
+
22
+ def method_missing(name, *args, &block)
23
+ if details = association_ordering_condition_details(name)
24
+ create_association_ordering_condition(details[:association], details[:order_as], details[:condition], args)
25
+ send(name, *args)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def association_ordering_condition_details(name)
32
+ associations = reflect_on_all_associations
33
+ association_names = associations.collect { |assoc| assoc.name }
34
+ if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
35
+ {:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
36
+ end
37
+ end
38
+
39
+ def create_association_ordering_condition(association, order_as, condition, args)
40
+ named_scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
41
+ end
42
+ end
43
+ end
44
+ end