sherpa99-thinking-sphinx 1.1.4

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 (145) hide show
  1. data/LICENCE +20 -0
  2. data/README +107 -0
  3. data/README.textile +107 -0
  4. data/Rakefile +4 -0
  5. data/contribute.rb +328 -0
  6. data/cucumber.yml +1 -0
  7. data/features/a.rb +17 -0
  8. data/features/attribute_transformation.feature +22 -0
  9. data/features/datetime_deltas.feature +55 -0
  10. data/features/delayed_delta_indexing.feature +37 -0
  11. data/features/deleting_instances.feature +52 -0
  12. data/features/facets.feature +26 -0
  13. data/features/handling_edits.feature +67 -0
  14. data/features/retry_stale_indexes.feature +24 -0
  15. data/features/searching_across_models.feature +20 -0
  16. data/features/searching_by_model.feature +118 -0
  17. data/features/searching_with_find_arguments.feature +56 -0
  18. data/features/sphinx_detection.feature +16 -0
  19. data/features/step_definitions/alpha_steps.rb +3 -0
  20. data/features/step_definitions/beta_steps.rb +11 -0
  21. data/features/step_definitions/cat_steps.rb +3 -0
  22. data/features/step_definitions/common_steps.rb +154 -0
  23. data/features/step_definitions/datetime_delta_steps.rb +11 -0
  24. data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
  25. data/features/step_definitions/facet_steps.rb +30 -0
  26. data/features/step_definitions/find_arguments_steps.rb +36 -0
  27. data/features/step_definitions/gamma_steps.rb +15 -0
  28. data/features/step_definitions/search_steps.rb +66 -0
  29. data/features/step_definitions/sphinx_steps.rb +23 -0
  30. data/features/support/db/active_record.rb +40 -0
  31. data/features/support/db/database.example.yml +4 -0
  32. data/features/support/db/migrations/create_alphas.rb +18 -0
  33. data/features/support/db/migrations/create_animals.rb +9 -0
  34. data/features/support/db/migrations/create_betas.rb +15 -0
  35. data/features/support/db/migrations/create_boxes.rb +13 -0
  36. data/features/support/db/migrations/create_comments.rb +13 -0
  37. data/features/support/db/migrations/create_delayed_betas.rb +28 -0
  38. data/features/support/db/migrations/create_developers.rb +39 -0
  39. data/features/support/db/migrations/create_gammas.rb +14 -0
  40. data/features/support/db/migrations/create_people.rb +1014 -0
  41. data/features/support/db/migrations/create_posts.rb +6 -0
  42. data/features/support/db/migrations/create_thetas.rb +16 -0
  43. data/features/support/db/mysql.rb +4 -0
  44. data/features/support/db/postgresql.rb +4 -0
  45. data/features/support/env.rb +6 -0
  46. data/features/support/models/alpha.rb +9 -0
  47. data/features/support/models/animal.rb +5 -0
  48. data/features/support/models/beta.rb +7 -0
  49. data/features/support/models/box.rb +8 -0
  50. data/features/support/models/cat.rb +3 -0
  51. data/features/support/models/comment.rb +3 -0
  52. data/features/support/models/delayed_beta.rb +7 -0
  53. data/features/support/models/developer.rb +8 -0
  54. data/features/support/models/gamma.rb +5 -0
  55. data/features/support/models/person.rb +8 -0
  56. data/features/support/models/post.rb +8 -0
  57. data/features/support/models/theta.rb +7 -0
  58. data/features/support/post_database.rb +37 -0
  59. data/features/support/z.rb +19 -0
  60. data/ginger_scenarios.rb +24 -0
  61. data/init.rb +12 -0
  62. data/lib/thinking_sphinx.rb +144 -0
  63. data/lib/thinking_sphinx/active_record.rb +245 -0
  64. data/lib/thinking_sphinx/active_record/delta.rb +74 -0
  65. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  66. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  67. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
  68. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
  69. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
  70. data/lib/thinking_sphinx/association.rb +144 -0
  71. data/lib/thinking_sphinx/attribute.rb +258 -0
  72. data/lib/thinking_sphinx/collection.rb +142 -0
  73. data/lib/thinking_sphinx/configuration.rb +236 -0
  74. data/lib/thinking_sphinx/core/string.rb +22 -0
  75. data/lib/thinking_sphinx/deltas.rb +22 -0
  76. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  77. data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
  78. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  79. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  80. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  81. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  82. data/lib/thinking_sphinx/facet.rb +58 -0
  83. data/lib/thinking_sphinx/facet_collection.rb +44 -0
  84. data/lib/thinking_sphinx/field.rb +172 -0
  85. data/lib/thinking_sphinx/index.rb +414 -0
  86. data/lib/thinking_sphinx/index/builder.rb +233 -0
  87. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  88. data/lib/thinking_sphinx/rails_additions.rb +133 -0
  89. data/lib/thinking_sphinx/search.rb +638 -0
  90. data/lib/thinking_sphinx/tasks.rb +128 -0
  91. data/rails/init.rb +6 -0
  92. data/spec/fixtures/data.sql +32 -0
  93. data/spec/fixtures/database.yml.default +3 -0
  94. data/spec/fixtures/models.rb +81 -0
  95. data/spec/fixtures/structure.sql +84 -0
  96. data/spec/spec_helper.rb +54 -0
  97. data/spec/sphinx_helper.rb +109 -0
  98. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  99. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  100. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  101. data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
  102. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  103. data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
  104. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  105. data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
  106. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  107. data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
  108. data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
  109. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  110. data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
  111. data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
  112. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  113. data/tasks/distribution.rb +48 -0
  114. data/tasks/rails.rake +1 -0
  115. data/tasks/testing.rb +86 -0
  116. data/thinking-sphinx.gemspec +232 -0
  117. data/vendor/after_commit/LICENSE +20 -0
  118. data/vendor/after_commit/README +16 -0
  119. data/vendor/after_commit/Rakefile +22 -0
  120. data/vendor/after_commit/init.rb +5 -0
  121. data/vendor/after_commit/lib/after_commit.rb +42 -0
  122. data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
  123. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  124. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  125. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  126. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  127. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  128. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  129. data/vendor/riddle/lib/riddle.rb +30 -0
  130. data/vendor/riddle/lib/riddle/client.rb +619 -0
  131. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  132. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  133. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  134. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  135. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  136. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  137. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  138. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  139. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  140. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  141. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  142. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  143. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  144. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  145. metadata +248 -0
@@ -0,0 +1,22 @@
1
+ module ThinkingSphinx
2
+ module Core
3
+ module String
4
+
5
+ def to_crc32
6
+ result = 0xFFFFFFFF
7
+ self.each_byte do |byte|
8
+ result ^= byte
9
+ 8.times do
10
+ result = (result >> 1) ^ (0xEDB88320 * (result & 1))
11
+ end
12
+ end
13
+ result ^ 0xFFFFFFFF
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+
20
+ class String
21
+ include ThinkingSphinx::Core::String
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'thinking_sphinx/deltas/default_delta'
2
+ require 'thinking_sphinx/deltas/delayed_delta'
3
+ require 'thinking_sphinx/deltas/datetime_delta'
4
+
5
+ module ThinkingSphinx
6
+ module Deltas
7
+ def self.parse(index, options)
8
+ case options.delete(:delta)
9
+ when TrueClass, :default
10
+ DefaultDelta.new index, options
11
+ when :delayed
12
+ DelayedDelta.new index, options
13
+ when :datetime
14
+ DatetimeDelta.new index, options
15
+ when FalseClass, nil
16
+ nil
17
+ else
18
+ raise "Unknown delta type"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,50 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta
4
+ attr_accessor :column, :threshold
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :updated_at
9
+ @threshold = options.delete(:threshold) || 1.day
10
+ end
11
+
12
+ def index(model, instance = nil)
13
+ # do nothing
14
+ true
15
+ end
16
+
17
+ def delayed_index(model)
18
+ config = ThinkingSphinx::Configuration.instance
19
+ rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
20
+
21
+ output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
22
+ output += `#{config.bin_path}indexer --config #{config.config_file} #{rotate} --merge #{core_index_name model} #{delta_index_name model} --merge-dst-range sphinx_deleted 0 0`
23
+ puts output unless ThinkingSphinx.suppress_delta_output?
24
+
25
+ true
26
+ end
27
+
28
+ def toggle(instance)
29
+ # do nothing
30
+ end
31
+
32
+ def toggled(instance)
33
+ instance.send(@column) > @threshold.ago
34
+ end
35
+
36
+ def reset_query(model)
37
+ nil
38
+ end
39
+
40
+ def clause(model, toggled)
41
+ if toggled
42
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
43
+ " > #{adapter.time_difference(@threshold)}"
44
+ else
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DefaultDelta
4
+ attr_accessor :column
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :delta
9
+ end
10
+
11
+ def index(model, instance = nil)
12
+ return true unless ThinkingSphinx.updates_enabled? &&
13
+ ThinkingSphinx.deltas_enabled?
14
+
15
+ config = ThinkingSphinx::Configuration.instance
16
+ client = Riddle::Client.new config.address, config.port
17
+
18
+ client.update(
19
+ core_index_name(model),
20
+ ['sphinx_deleted'],
21
+ {instance.sphinx_document_id => [1]}
22
+ ) if instance && ThinkingSphinx.sphinx_running? && instance.in_core_index?
23
+
24
+ output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{delta_index_name model}`
25
+ puts output unless ThinkingSphinx.suppress_delta_output?
26
+
27
+ true
28
+ end
29
+
30
+ def toggle(instance)
31
+ instance.delta = true
32
+ end
33
+
34
+ def toggled(instance)
35
+ instance.delta
36
+ end
37
+
38
+ def reset_query(model)
39
+ "UPDATE #{model.quoted_table_name} SET " +
40
+ "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
41
+ end
42
+
43
+ def clause(model, toggled)
44
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
45
+ " = #{adapter.boolean(toggled)}"
46
+ end
47
+
48
+ protected
49
+
50
+ def core_index_name(model)
51
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
52
+ end
53
+
54
+ def delta_index_name(model)
55
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
56
+ end
57
+
58
+ private
59
+
60
+ def adapter
61
+ @adapter = @index.model.sphinx_database_adapter
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ require 'delayed/job'
2
+
3
+ require 'thinking_sphinx/deltas/delayed_delta/delta_job'
4
+ require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
5
+ require 'thinking_sphinx/deltas/delayed_delta/job'
6
+
7
+ module ThinkingSphinx
8
+ module Deltas
9
+ class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
10
+ def index(model, instance = nil)
11
+ ThinkingSphinx::Deltas::Job.enqueue(
12
+ ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
13
+ )
14
+
15
+ Delayed::Job.enqueue(
16
+ ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
17
+ core_index_name(model), instance.sphinx_document_id
18
+ )
19
+ ) if instance
20
+
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DeltaJob
4
+ attr_accessor :index
5
+
6
+ def initialize(index)
7
+ @index = index
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled? &&
12
+ ThinkingSphinx.deltas_enabled?
13
+
14
+ config = ThinkingSphinx::Configuration.instance
15
+ client = Riddle::Client.new config.address, config.port
16
+
17
+ output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}`
18
+ puts output unless ThinkingSphinx.suppress_delta_output?
19
+
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class FlagAsDeletedJob
4
+ attr_accessor :index, :document_id
5
+
6
+ def initialize(index, document_id)
7
+ @index, @document_id = index, document_id
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled?
12
+
13
+ config = ThinkingSphinx::Configuration.instance
14
+ client = Riddle::Client.new config.address, config.port
15
+
16
+ client.update(
17
+ @index,
18
+ ['sphinx_deleted'],
19
+ {@document_id => [1]}
20
+ ) if ThinkingSphinx.sphinx_running? &&
21
+ ThinkingSphinx::Search.search_for_id(@document_id, @index)
22
+
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class Job < Delayed::Job
4
+ def self.enqueue(object, priority = 0)
5
+ super unless duplicates_exist(object)
6
+ end
7
+
8
+ def self.cancel_thinking_sphinx_jobs
9
+ if connection.tables.include?("delayed_jobs")
10
+ delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'")
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def self.duplicates_exist(object)
17
+ count(
18
+ :conditions => {
19
+ :handler => object.to_yaml,
20
+ :locked_at => nil
21
+ }
22
+ ) > 0
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,58 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :reference
4
+
5
+ def initialize(reference)
6
+ @reference = reference
7
+
8
+ if reference.columns.length != 1
9
+ raise "Can't translate Facets on multiple-column field or attribute"
10
+ end
11
+ end
12
+
13
+ def name
14
+ reference.unique_name
15
+ end
16
+
17
+ def attribute_name
18
+ @attribute_name ||= case @reference
19
+ when Attribute
20
+ @reference.unique_name.to_s
21
+ when Field
22
+ @reference.unique_name.to_s + "_sort"
23
+ end
24
+ end
25
+
26
+ def value(object, attribute_value)
27
+ return translate(object, attribute_value) if @reference.is_a?(Field)
28
+
29
+ case @reference.type
30
+ when :string, :multi
31
+ translate(object, attribute_value)
32
+ when :datetime
33
+ Time.at(attribute_value)
34
+ when :boolean
35
+ attribute_value > 0
36
+ else
37
+ attribute_value
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+
45
+ private
46
+
47
+ def translate(object, attribute_value)
48
+ column.__stack.each { |method|
49
+ object = object.send(method)
50
+ }
51
+ object.send(column.__name)
52
+ end
53
+
54
+ def column
55
+ @reference.columns.first
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ module ThinkingSphinx
2
+ class FacetCollection < Hash
3
+ attr_accessor :arguments
4
+
5
+ def initialize(arguments)
6
+ @arguments = arguments.clone
7
+ @attribute_values = {}
8
+ @facets = []
9
+ end
10
+
11
+ def add_from_results(facet, results)
12
+ self[facet.name] = {}
13
+ @attribute_values[facet.name] = {}
14
+ @facets << facet
15
+
16
+ results.each_with_groupby_and_count { |result, group, count|
17
+ facet_value = facet.value(result, group)
18
+
19
+ self[facet.name][facet_value] = count
20
+ @attribute_values[facet.name][facet_value] = group
21
+ }
22
+ end
23
+
24
+ def for(hash = {})
25
+ arguments = @arguments.clone
26
+ options = arguments.extract_options!
27
+ options[:with] ||= {}
28
+
29
+ hash.each do |key, value|
30
+ attrib = facet_for_key(key).attribute_name
31
+ options[:with][attrib] = @attribute_values[key][value]
32
+ end
33
+
34
+ arguments << options
35
+ ThinkingSphinx::Search.search *arguments
36
+ end
37
+
38
+ private
39
+
40
+ def facet_for_key(key)
41
+ @facets.detect { |facet| facet.name == key }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,172 @@
1
+ module ThinkingSphinx
2
+ # Fields - holding the string data which Sphinx indexes for your searches.
3
+ # This class isn't really useful to you unless you're hacking around with the
4
+ # internals of Thinking Sphinx - but hey, don't let that stop you.
5
+ #
6
+ # One key thing to remember - if you're using the field manually to
7
+ # generate SQL statements, you'll need to set the base model, and all the
8
+ # associations. Which can get messy. Use Index.link!, it really helps.
9
+ #
10
+ class Field
11
+ attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
12
+ :prefixes, :faceted
13
+
14
+ # To create a new field, you'll need to pass in either a single Column
15
+ # or an array of them, and some (optional) options. The columns are
16
+ # references to the data that will make up the field.
17
+ #
18
+ # Valid options are:
19
+ # - :as => :alias_name
20
+ # - :sortable => true
21
+ # - :infixes => true
22
+ # - :prefixes => true
23
+ #
24
+ # Alias is only required in three circumstances: when there's
25
+ # another attribute or field with the same name, when the column name is
26
+ # 'id', or when there's more than one column.
27
+ #
28
+ # Sortable defaults to false - but is quite useful when set to true, as
29
+ # it creates an attribute with the same string value (which Sphinx converts
30
+ # to an integer value), which can be sorted by. Thinking Sphinx is smart
31
+ # enough to realise that when you specify fields in sort statements, you
32
+ # mean their respective attributes.
33
+ #
34
+ # If you have partial matching enabled (ie: enable_star), then you can
35
+ # specify certain fields to have their prefixes and infixes indexed. Keep
36
+ # in mind, though, that Sphinx's default is _all_ fields - so once you
37
+ # highlight a particular field, no other fields in the index will have
38
+ # these partial indexes.
39
+ #
40
+ # Here's some examples:
41
+ #
42
+ # Field.new(
43
+ # Column.new(:name)
44
+ # )
45
+ #
46
+ # Field.new(
47
+ # [Column.new(:first_name), Column.new(:last_name)],
48
+ # :as => :name, :sortable => true
49
+ # )
50
+ #
51
+ # Field.new(
52
+ # [Column.new(:posts, :subject), Column.new(:posts, :content)],
53
+ # :as => :posts, :prefixes => true
54
+ # )
55
+ #
56
+ def initialize(columns, options = {})
57
+ @columns = Array(columns)
58
+ @associations = {}
59
+
60
+ raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
61
+
62
+ @alias = options[:as]
63
+ @sortable = options[:sortable] || false
64
+ @infixes = options[:infixes] || false
65
+ @prefixes = options[:prefixes] || false
66
+ @faceted = options[:facet] || false
67
+ end
68
+
69
+ # Get the part of the SELECT clause related to this field. Don't forget
70
+ # to set your model and associations first though.
71
+ #
72
+ # This will concatenate strings if there's more than one data source or
73
+ # multiple data values (has_many or has_and_belongs_to_many associations).
74
+ #
75
+ def to_select_sql
76
+ clause = @columns.collect { |column|
77
+ column_with_prefix(column)
78
+ }.join(', ')
79
+
80
+ clause = adapter.concatenate(clause) if concat_ws?
81
+ clause = adapter.group_concatenate(clause) if is_many?
82
+
83
+ "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
84
+ end
85
+
86
+ # Get the part of the GROUP BY clause related to this field - if one is
87
+ # needed. If not, all you'll get back is nil. The latter will happen if
88
+ # there's multiple data values (read: a has_many or has_and_belongs_to_many
89
+ # association).
90
+ #
91
+ def to_group_sql
92
+ case
93
+ when is_many?, ThinkingSphinx.use_group_by_shortcut?
94
+ nil
95
+ else
96
+ @columns.collect { |column|
97
+ column_with_prefix(column)
98
+ }
99
+ end
100
+ end
101
+
102
+ # Returns the unique name of the field - which is either the alias of
103
+ # the field, or the name of the only column - if there is only one. If
104
+ # there isn't, there should be an alias. Else things probably won't work.
105
+ # Consider yourself warned.
106
+ #
107
+ def unique_name
108
+ if @columns.length == 1
109
+ @alias || @columns.first.__name
110
+ else
111
+ @alias
112
+ end
113
+ end
114
+
115
+ def to_facet
116
+ return nil unless @faceted
117
+
118
+ ThinkingSphinx::Facet.new(self)
119
+ end
120
+
121
+ private
122
+
123
+ def adapter
124
+ @adapter ||= @model.sphinx_database_adapter
125
+ end
126
+
127
+ def quote_column(column)
128
+ @model.connection.quote_column_name(column)
129
+ end
130
+
131
+ # Indication of whether the columns should be concatenated with a space
132
+ # between each value. True if there's either multiple sources or multiple
133
+ # associations.
134
+ #
135
+ def concat_ws?
136
+ @columns.length > 1 || multiple_associations?
137
+ end
138
+
139
+ # Checks whether any column requires multiple associations (which only
140
+ # happens for polymorphic situations).
141
+ #
142
+ def multiple_associations?
143
+ associations.any? { |col,assocs| assocs.length > 1 }
144
+ end
145
+
146
+ # Builds a column reference tied to the appropriate associations. This
147
+ # dives into the associations hash and their corresponding joins to
148
+ # figure out how to correctly reference a column in SQL.
149
+ #
150
+ def column_with_prefix(column)
151
+ if column.is_string?
152
+ column.__name
153
+ elsif associations[column].empty?
154
+ "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
155
+ else
156
+ associations[column].collect { |assoc|
157
+ assoc.has_column?(column.__name) ?
158
+ "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
159
+ ".#{quote_column(column.__name)}" :
160
+ nil
161
+ }.compact.join(', ')
162
+ end
163
+ end
164
+
165
+ # Could there be more than one value related to the parent record? If so,
166
+ # then this will return true. If not, false. It's that simple.
167
+ #
168
+ def is_many?
169
+ associations.values.flatten.any? { |assoc| assoc.is_many? }
170
+ end
171
+ end
172
+ end