thinking-sphinx 3.0.3 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/HISTORY +25 -0
  4. data/lib/thinking_sphinx.rb +1 -0
  5. data/lib/thinking_sphinx/active_record/association_proxy.rb +13 -55
  6. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +47 -0
  7. data/lib/thinking_sphinx/active_record/base.rb +16 -15
  8. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +1 -12
  9. data/lib/thinking_sphinx/active_record/database_adapters.rb +41 -42
  10. data/lib/thinking_sphinx/active_record/polymorpher.rb +7 -2
  11. data/lib/thinking_sphinx/active_record/property_query.rb +23 -19
  12. data/lib/thinking_sphinx/active_record/sql_builder.rb +108 -129
  13. data/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +28 -0
  14. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +43 -0
  15. data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +110 -0
  16. data/lib/thinking_sphinx/active_record/sql_source.rb +143 -138
  17. data/lib/thinking_sphinx/capistrano.rb +11 -8
  18. data/lib/thinking_sphinx/configuration.rb +57 -35
  19. data/lib/thinking_sphinx/connection.rb +15 -6
  20. data/lib/thinking_sphinx/core.rb +1 -0
  21. data/lib/thinking_sphinx/core/index.rb +18 -10
  22. data/lib/thinking_sphinx/core/settings.rb +9 -0
  23. data/lib/thinking_sphinx/deletion.rb +48 -0
  24. data/lib/thinking_sphinx/errors.rb +7 -0
  25. data/lib/thinking_sphinx/excerpter.rb +1 -0
  26. data/lib/thinking_sphinx/facet_search.rb +42 -19
  27. data/lib/thinking_sphinx/masks/scopes_mask.rb +7 -0
  28. data/lib/thinking_sphinx/middlewares.rb +27 -33
  29. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +18 -18
  30. data/lib/thinking_sphinx/middlewares/geographer.rb +49 -16
  31. data/lib/thinking_sphinx/middlewares/sphinxql.rb +128 -58
  32. data/lib/thinking_sphinx/panes/excerpts_pane.rb +7 -3
  33. data/lib/thinking_sphinx/rake_interface.rb +10 -0
  34. data/lib/thinking_sphinx/real_time.rb +7 -1
  35. data/lib/thinking_sphinx/real_time/attribute.rb +4 -0
  36. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +14 -10
  37. data/lib/thinking_sphinx/real_time/index.rb +20 -12
  38. data/lib/thinking_sphinx/search/glaze.rb +5 -0
  39. data/lib/thinking_sphinx/search/query.rb +4 -8
  40. data/lib/thinking_sphinx/tasks.rb +8 -0
  41. data/spec/acceptance/excerpts_spec.rb +22 -0
  42. data/spec/acceptance/remove_deleted_records_spec.rb +10 -0
  43. data/spec/acceptance/searching_across_models_spec.rb +10 -0
  44. data/spec/acceptance/searching_with_filters_spec.rb +15 -0
  45. data/spec/acceptance/specifying_sql_spec.rb +3 -3
  46. data/spec/acceptance/sphinx_scopes_spec.rb +11 -0
  47. data/spec/internal/app/indices/product_index.rb +2 -0
  48. data/spec/internal/app/models/categorisation.rb +6 -0
  49. data/spec/internal/app/models/category.rb +3 -0
  50. data/spec/internal/app/models/product.rb +4 -1
  51. data/spec/internal/db/schema.rb +10 -0
  52. data/spec/thinking_sphinx/active_record/base_spec.rb +33 -0
  53. data/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +4 -35
  54. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +5 -10
  55. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +4 -3
  56. data/spec/thinking_sphinx/configuration_spec.rb +26 -6
  57. data/spec/thinking_sphinx/connection_spec.rb +4 -1
  58. data/spec/thinking_sphinx/deletion_spec.rb +76 -0
  59. data/spec/thinking_sphinx/facet_search_spec.rb +54 -5
  60. data/spec/thinking_sphinx/panes/excerpts_pane_spec.rb +4 -6
  61. data/spec/thinking_sphinx/rake_interface_spec.rb +35 -0
  62. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +68 -28
  63. data/spec/thinking_sphinx/search/glaze_spec.rb +19 -0
  64. data/spec/thinking_sphinx/search/query_spec.rb +39 -2
  65. data/thinking-sphinx.gemspec +2 -2
  66. metadata +31 -45
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e4abfc7a0ce650e92e51a842336ec1028f12e9ea
4
+ data.tar.gz: 2c51520fcd002486f51e127f981f5e7c60000706
5
+ SHA512:
6
+ metadata.gz: 384ba739298c2dc12431cf9c74e79a3f15191098d84b41afab0e94383ac9403384e9290e38c834ef17a1675eedf5835477f63c12aeeb440144421dab97e82959
7
+ data.tar.gz: 787f2a640087f91d7ffd7d39d1b092fb027551e6df58d2ab19062a78bfebe3b7bbbfe8ad4c65858d84fc590403eb0515296c6a5192fc3aaf6937e2f595b1ec3a
data/.gitignore CHANGED
@@ -5,4 +5,5 @@ Gemfile.lock
5
5
  *.sublime-*
6
6
  pkg/*
7
7
  spec/internal/config/test.sphinx.conf
8
- spec/internal/db/sphinx
8
+ spec/internal/db/sphinx
9
+ _site
data/HISTORY CHANGED
@@ -1,3 +1,28 @@
1
+ 2013-07-09: 3.0.4
2
+ * [CHANGE] Updating Riddle dependency to be >= 1.5.7.
3
+ * [FEATURE] ts:regenerate rake task for rebuilding Sphinx when realtime indices are involved.
4
+ * [FEATURE] ts:clear task removes all Sphinx index and binlog files.
5
+ * [CHANGE] Glaze now responds to respond_to? (@groe).
6
+ * [FEATURE] Facet search calls now respect the limit option (which otherwise defaults to max_matches) (Demian Ferreiro).
7
+ * [FEATURE] Excerpts words can be overwritten with the words option (@groe).
8
+ * [FIX] Empty queries with the star option set to true are handled gracefully.
9
+ * [CHANGE] Deleted ActiveRecord objects are deleted in realtime indices as well.
10
+ * [CHANGE] Realtime callbacks are no longer automatically added, but they're now more flexible (for association situations).
11
+ * [CHANGE] Cleaning and refactoring so Code Climate ranks this as A-level code (Philip Arndt, Shevaun Coker, Garrett Heinlen).
12
+ * [FIX] Excerpts are now wildcard-friendly.
13
+ * [FIX] Facet searches now use max_matches value (with a default of 1000) to ensure as many results as possible are returned.
14
+ * [CHANGE] Exceptions raised when communicating with Sphinx are now mentioned in the logs when queries are retried (instead of STDOUT).
15
+ * [CHANGE] Excerpts now use just the query and standard conditions, instead of parsing Sphinx's keyword metadata (which had model names in it).
16
+ * [FIX] The settings cache is now cleared when the configuration singleton is reset (Pedro Cunha).
17
+ * [FEATURE] The :facets option can be used in facet searches to limit which facets are queried.
18
+ * [FIX] Escaped @'s in queries are considered part of each word, instead of word separators.
19
+ * [FIX] Internal class name conditions are ignored with auto-starred queries.
20
+ * [FEATURE] A separate role can be set for Sphinx actions with Capistrano (Andrey Chernih).
21
+ * [FIX] RDoc doesn't like constant hierarchies split over multiple lines.
22
+ * [CHANGE] Get database connection details from ActiveRecord::Base, not each model, as this is where changes are reflected.
23
+ * [CHANGE] Default Sphinx scopes are applied to new facet searches.
24
+ * [FEATURE] Facet searches can now be called from Sphinx scopes.
25
+
1
26
  2013-05-07: 3.0.3
2
27
  * [CHANGE] Updating Riddle dependency to be >= 1.5.6
3
28
  * [FEATURE] INDEX_ONLY environment flag is passed through when invoked through Capistrano (Demian Ferreiro).
@@ -42,6 +42,7 @@ require 'thinking_sphinx/callbacks'
42
42
  require 'thinking_sphinx/core'
43
43
  require 'thinking_sphinx/configuration'
44
44
  require 'thinking_sphinx/connection'
45
+ require 'thinking_sphinx/deletion'
45
46
  require 'thinking_sphinx/errors'
46
47
  require 'thinking_sphinx/excerpter'
47
48
  require 'thinking_sphinx/facet'
@@ -2,73 +2,31 @@ module ThinkingSphinx::ActiveRecord::AssociationProxy
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def search(query = nil, options = {})
5
- query, options = nil, query if query.is_a?(Hash)
6
- options[:ignore_scopes] = true
7
-
8
- ThinkingSphinx::Search::Merger.new(super).merge! nil,
9
- :with => association_filter
5
+ perform_search super(*normalise_search_arguments(query, options))
10
6
  end
11
7
 
12
8
  def search_for_ids(query = nil, options = {})
9
+ perform_search super(*normalise_search_arguments(query, options))
10
+ end
11
+
12
+ private
13
+ def normalise_search_arguments(query, options)
13
14
  query, options = nil, query if query.is_a?(Hash)
14
15
  options[:ignore_scopes] = true
15
16
 
16
- ThinkingSphinx::Search::Merger.new(super).merge! nil,
17
- :with => association_filter
17
+ [query, options]
18
18
  end
19
19
 
20
- private
20
+ def perform_search(searcher)
21
+ ThinkingSphinx::Search::Merger.new(searcher).merge! nil,
22
+ :with => association_filter
23
+ end
21
24
 
22
25
  def association_filter
23
26
  attribute = AttributeFinder.new(proxy_association).attribute
24
27
 
25
28
  {attribute.name.to_sym => proxy_association.owner.id}
26
29
  end
27
-
28
- class AttributeFinder
29
- def initialize(association)
30
- @association = association
31
- end
32
-
33
- def attribute
34
- attributes.detect { |attribute|
35
- # Don't bother with attributes built from multiple columns
36
- next unless attribute.columns.length == 1
37
-
38
- attribute.columns.first.__name == foreign_key.to_sym ||
39
- attribute.name == foreign_key.to_s
40
- } or raise "Missing Attribute for Foreign Key #{foreign_key}"
41
- end
42
-
43
- private
44
-
45
- def attributes
46
- sources.collect(&:attributes).flatten
47
- end
48
-
49
- def configuration
50
- ThinkingSphinx::Configuration.instance
51
- end
52
-
53
- def foreign_key
54
- @foreign_key ||= if @association.reflection.through_reflection
55
- @association.reflection.through_reflection.foreign_key
56
- else
57
- @association.reflection.foreign_key
58
- end
59
- end
60
-
61
- def indices
62
- @indices ||= begin
63
- configuration.preload_indices
64
- configuration.indices_for_references(
65
- *@association.klass.name.underscore.to_sym
66
- )
67
- end
68
- end
69
-
70
- def sources
71
- indices.collect(&:sources).flatten
72
- end
73
- end
74
30
  end
31
+
32
+ require 'thinking_sphinx/active_record/association_proxy/attribute_finder'
@@ -0,0 +1,47 @@
1
+ class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeFinder
2
+ def initialize(association)
3
+ @association = association
4
+ end
5
+
6
+ def attribute
7
+ attributes.detect { |attribute|
8
+ # Don't bother with attributes built from multiple columns
9
+ next if attribute.columns.many?
10
+
11
+ attribute.columns.first.__name == foreign_key.to_sym ||
12
+ attribute.name == foreign_key.to_s
13
+ } or raise "Missing Attribute for Foreign Key #{foreign_key}"
14
+ end
15
+
16
+ private
17
+ def attributes
18
+ sources.collect(&:attributes).flatten
19
+ end
20
+
21
+ def configuration
22
+ ThinkingSphinx::Configuration.instance
23
+ end
24
+
25
+ def foreign_key
26
+ @foreign_key ||= reflection_target.foreign_key
27
+ end
28
+
29
+ def indices
30
+ @indices ||= begin
31
+ configuration.preload_indices
32
+ configuration.indices_for_references(
33
+ *@association.klass.name.underscore.to_sym
34
+ )
35
+ end
36
+ end
37
+
38
+ def reflection_target
39
+ target = @association.reflection
40
+ target = target.through_reflection if target.through_reflection
41
+ target
42
+ end
43
+
44
+ def sources
45
+ indices.collect(&:sources).flatten
46
+ end
47
+ end
@@ -7,30 +7,17 @@ module ThinkingSphinx::ActiveRecord::Base
7
7
  after_update ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks
8
8
  after_commit ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks
9
9
 
10
- after_save ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
11
-
12
10
  ::ActiveRecord::Associations::CollectionProxy.send :include,
13
11
  ThinkingSphinx::ActiveRecord::AssociationProxy
14
12
  end
15
13
 
16
14
  module ClassMethods
17
15
  def facets(query = nil, options = {})
18
- search = ThinkingSphinx.facets query, options
19
- ThinkingSphinx::Search::Merger.new(search).merge! nil, :classes => [self]
16
+ merge_search ThinkingSphinx.facets, query, options
20
17
  end
21
18
 
22
19
  def search(query = nil, options = {})
23
- merger = ThinkingSphinx::Search::Merger.new ThinkingSphinx.search
24
-
25
- merger.merge! *default_sphinx_scope_response if default_sphinx_scope?
26
- merger.merge! query, options
27
-
28
- if current_scope && !merger.search.options[:ignore_scopes]
29
- raise ThinkingSphinx::MixedScopesError,
30
- 'You cannot search with Sphinx through ActiveRecord scopes'
31
- end
32
-
33
- merger.merge! nil, :classes => [self]
20
+ merge_search ThinkingSphinx.search, query, options
34
21
  end
35
22
 
36
23
  def search_count(query = nil, options = {})
@@ -51,5 +38,19 @@ module ThinkingSphinx::ActiveRecord::Base
51
38
  def default_sphinx_scope_response
52
39
  [sphinx_scopes[default_sphinx_scope].call].flatten
53
40
  end
41
+
42
+ def merge_search(search, query, options)
43
+ merger = ThinkingSphinx::Search::Merger.new search
44
+
45
+ merger.merge! *default_sphinx_scope_response if default_sphinx_scope?
46
+ merger.merge! query, options
47
+
48
+ if current_scope && !merger.search.options[:ignore_scopes]
49
+ raise ThinkingSphinx::MixedScopesError,
50
+ 'You cannot search with Sphinx through ActiveRecord scopes'
51
+ end
52
+
53
+ merger.merge! nil, :classes => [self]
54
+ end
54
55
  end
55
56
  end
@@ -4,14 +4,7 @@ class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks <
4
4
  callbacks :after_destroy
5
5
 
6
6
  def after_destroy
7
- indices.each do |index|
8
- connection.execute Riddle::Query.update(
9
- index.name, index.document_id_for_key(instance.id),
10
- :sphinx_deleted => true
11
- )
12
- end
13
- rescue Mysql2::Error => error
14
- # This isn't vital, so don't raise the error.
7
+ indices.each { |index| ThinkingSphinx::Deletion.perform index, instance }
15
8
  end
16
9
 
17
10
  private
@@ -20,10 +13,6 @@ class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks <
20
13
  ThinkingSphinx::Configuration.instance
21
14
  end
22
15
 
23
- def connection
24
- @connection ||= ThinkingSphinx::Connection.new
25
- end
26
-
27
16
  def indices
28
17
  config.preload_indices
29
18
  config.indices_for_references instance.class.name.underscore.to_sym
@@ -1,55 +1,54 @@
1
1
  module ThinkingSphinx::ActiveRecord::DatabaseAdapters
2
- def self.adapter_for(model)
3
- return default.new(model) unless default.nil?
4
-
5
- adapter = adapter_type_for(model)
6
- klass = case adapter
7
- when :mysql
8
- ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter
9
- when :postgresql
10
- ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter
11
- else
12
- raise "Invalid Database Adapter '#{adapter}': Thinking Sphinx only supports MySQL and PostgreSQL."
13
- end
2
+ class << self
3
+ attr_accessor :default
14
4
 
15
- klass.new model
16
- end
5
+ def adapter_for(model)
6
+ return default.new(model) if default
7
+
8
+ adapter = adapter_type_for(model)
9
+ klass = case adapter
10
+ when :mysql
11
+ MySQLAdapter
12
+ when :postgresql
13
+ PostgreSQLAdapter
14
+ else
15
+ raise "Invalid Database Adapter '#{adapter}': Thinking Sphinx only supports MySQL and PostgreSQL."
16
+ end
17
+
18
+ klass.new model
19
+ end
17
20
 
18
- def self.adapter_type_for(model)
19
- case model.connection.class.name
20
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
21
- "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
22
- :mysql
23
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
24
- :postgresql
25
- when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
26
- case model.connection.config[:adapter]
27
- when "jdbcmysql"
21
+ def adapter_type_for(model)
22
+ class_name = model.connection.class.name
23
+ case class_name.split('::').last
24
+ when 'MysqlAdapter', 'Mysql2Adapter'
28
25
  :mysql
29
- when "jdbcpostgresql"
26
+ when 'PostgreSQLAdapter'
30
27
  :postgresql
31
- when "jdbc"
32
- match = /^jdbc:(?<adapter>mysql|postgresql):\/\//.match(model.connection.config[:url])
33
- if match
34
- match[:adapter].to_sym
35
- else
36
- model.connection.config[:adapter]
37
- end
28
+ when 'JdbcAdapter'
29
+ adapter_type_for_jdbc(model)
38
30
  else
39
- model.connection.config[:adapter]
31
+ class_name
40
32
  end
41
- else
42
- model.connection.class.name
43
33
  end
44
- end
45
34
 
46
- @default = nil
47
- def self.default
48
- @default
49
- end
35
+ def adapter_type_for_jdbc(model)
36
+ case adapter = model.connection.config[:adapter]
37
+ when 'jdbcmysql'
38
+ :mysql
39
+ when 'jdbcpostgresql'
40
+ :postgresql
41
+ when 'jdbc'
42
+ adapter_type_for_jdbc_plain(adapter, model.connection.config[:url])
43
+ else adapter
44
+ end
45
+ end
46
+
47
+ def adapter_type_for_jdbc_plain(adapter, url)
48
+ return adapter unless match = /^jdbc:(?<adapter>mysql|postgresql):\/\//.match(url)
50
49
 
51
- def self.default=(default)
52
- @default = default
50
+ match[:adapter].to_sym
51
+ end
53
52
  end
54
53
  end
55
54
 
@@ -14,11 +14,16 @@ class ThinkingSphinx::ActiveRecord::Polymorpher
14
14
 
15
15
  def append_reflections
16
16
  mappings.each do |class_name, name|
17
- klass.reflections[name] ||= ThinkingSphinx::ActiveRecord::
18
- FilteredReflection.clone_with_filter(reflection, name, class_name)
17
+ klass.reflections[name] ||= clone_with name, class_name
19
18
  end
20
19
  end
21
20
 
21
+ def clone_with(name, class_name)
22
+ ThinkingSphinx::ActiveRecord::FilteredReflection.clone_with_filter(
23
+ reflection, name, class_name
24
+ )
25
+ end
26
+
22
27
  def mappings
23
28
  @mappings ||= class_names.inject({}) do |hash, class_name|
24
29
  hash[class_name] = "#{column.__name}_#{class_name.downcase}".to_sym
@@ -5,6 +5,13 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
5
5
 
6
6
  def to_s
7
7
  identifier = [type, property.name].compact.join(' ')
8
+
9
+ "#{identifier} from #{source_type}; #{queries.join('; ')}"
10
+ end
11
+
12
+ private
13
+
14
+ def queries
8
15
  queries = []
9
16
  if column.string?
10
17
  queries << column.__name.strip.gsub(/\n/, "\\\n")
@@ -12,18 +19,20 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
12
19
  queries << to_sql
13
20
  queries << range_sql if ranged?
14
21
  end
15
-
16
- "#{identifier} from #{source_type}; #{queries.join('; ')}"
22
+ queries
17
23
  end
18
24
 
19
- private
20
-
21
25
  attr_reader :property, :source, :type
22
26
 
23
27
  def base_association
24
28
  reflections.first
25
29
  end
26
30
 
31
+ def base_association_class
32
+ base_association.klass
33
+ end
34
+ delegate :relation, :to => :base_association_class, :prefix => true
35
+
27
36
  def column
28
37
  @column ||= property.columns.first
29
38
  end
@@ -40,7 +49,7 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
40
49
 
41
50
  column.__stack.collect { |key|
42
51
  reflection = base.reflections[key]
43
- base = reflection.klass
52
+ base = reflection.klass
44
53
 
45
54
  extend_reflection reflection
46
55
  }.flatten
@@ -64,12 +73,11 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
64
73
  end
65
74
 
66
75
  def quoted_foreign_key
67
- quote_with_table base_association.klass.table_name,
68
- base_association.foreign_key
76
+ quote_with_table(base_association_class.table_name, base_association.foreign_key)
69
77
  end
70
78
 
71
79
  def quoted_primary_key
72
- quote_with_table reflections.last.klass.table_name, column.__name
80
+ quote_with_table(reflections.last.klass.table_name, column.__name)
73
81
  end
74
82
 
75
83
  def quote_with_table(table, column)
@@ -85,8 +93,9 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
85
93
  end
86
94
 
87
95
  def range_sql
88
- base_association.klass.unscoped.
89
- select("MIN(#{quoted_foreign_key}), MAX(#{quoted_foreign_key})").to_sql
96
+ base_association_class_relation.select(
97
+ "MIN(#{quoted_foreign_key}), MAX(#{quoted_foreign_key})"
98
+ ).to_sql
90
99
  end
91
100
 
92
101
  def source_type
@@ -96,15 +105,10 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
96
105
  def to_sql
97
106
  raise "Could not determine SQL for MVA" if reflections.empty?
98
107
 
99
- relation = base_association.klass.unscoped
100
- relation = relation.joins joins unless joins.blank?
101
- relation = relation.select "#{quoted_foreign_key} #{offset} AS #{quote_column('id')}, #{quoted_primary_key} AS #{quote_column(property.name)}"
102
-
103
- if ranged?
104
- relation = relation.where("#{quoted_foreign_key} >= $start")
105
- relation = relation.where("#{quoted_foreign_key} <= $end")
106
- end
107
-
108
+ relation = base_association_class_relation.select("#{quoted_foreign_key} #{offset} AS #{quote_column('id')}, #{quoted_primary_key} AS #{quote_column(property.name)}"
109
+ )
110
+ relation = relation.joins(joins) if joins.present?
111
+ relation = relation.where("#{quoted_foreign_key} BETWEEN $start AND $end") if ranged?
108
112
  relation = relation.order("#{quoted_foreign_key} ASC") if type.nil?
109
113
 
110
114
  relation.to_sql