thinking-sphinx 1.2.12

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 (95) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +157 -0
  3. data/VERSION.yml +4 -0
  4. data/lib/thinking_sphinx.rb +211 -0
  5. data/lib/thinking_sphinx/active_record.rb +307 -0
  6. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  7. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  8. data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
  9. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  10. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  11. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  12. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  13. data/lib/thinking_sphinx/association.rb +164 -0
  14. data/lib/thinking_sphinx/attribute.rb +342 -0
  15. data/lib/thinking_sphinx/class_facet.rb +15 -0
  16. data/lib/thinking_sphinx/configuration.rb +282 -0
  17. data/lib/thinking_sphinx/core/array.rb +7 -0
  18. data/lib/thinking_sphinx/core/string.rb +15 -0
  19. data/lib/thinking_sphinx/deltas.rb +30 -0
  20. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  21. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  22. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  23. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  24. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  25. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  26. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  27. data/lib/thinking_sphinx/excerpter.rb +22 -0
  28. data/lib/thinking_sphinx/facet.rb +125 -0
  29. data/lib/thinking_sphinx/facet_search.rb +134 -0
  30. data/lib/thinking_sphinx/field.rb +82 -0
  31. data/lib/thinking_sphinx/index.rb +99 -0
  32. data/lib/thinking_sphinx/index/builder.rb +286 -0
  33. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  34. data/lib/thinking_sphinx/property.rb +162 -0
  35. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  36. data/lib/thinking_sphinx/search.rb +707 -0
  37. data/lib/thinking_sphinx/search_methods.rb +421 -0
  38. data/lib/thinking_sphinx/source.rb +150 -0
  39. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  40. data/lib/thinking_sphinx/source/sql.rb +128 -0
  41. data/lib/thinking_sphinx/tasks.rb +165 -0
  42. data/rails/init.rb +14 -0
  43. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
  44. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
  45. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  46. data/spec/lib/thinking_sphinx/active_record_spec.rb +364 -0
  47. data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
  48. data/spec/lib/thinking_sphinx/attribute_spec.rb +500 -0
  49. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  50. data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
  51. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  52. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  53. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  54. data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
  55. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  56. data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
  57. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  58. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  59. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  60. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  61. data/spec/lib/thinking_sphinx/search_spec.rb +1092 -0
  62. data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
  63. data/spec/lib/thinking_sphinx_spec.rb +162 -0
  64. data/tasks/distribution.rb +50 -0
  65. data/tasks/rails.rake +1 -0
  66. data/tasks/testing.rb +83 -0
  67. data/vendor/after_commit/LICENSE +20 -0
  68. data/vendor/after_commit/README +16 -0
  69. data/vendor/after_commit/Rakefile +22 -0
  70. data/vendor/after_commit/init.rb +8 -0
  71. data/vendor/after_commit/lib/after_commit.rb +45 -0
  72. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  73. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  74. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  75. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  76. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  77. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  78. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  79. data/vendor/riddle/lib/riddle.rb +30 -0
  80. data/vendor/riddle/lib/riddle/client.rb +635 -0
  81. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  82. data/vendor/riddle/lib/riddle/client/message.rb +66 -0
  83. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  84. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  85. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  86. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  87. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  88. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  89. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  90. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  91. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  92. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  93. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  94. data/vendor/riddle/lib/riddle/controller.rb +53 -0
  95. metadata +172 -0
@@ -0,0 +1,48 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module AttributeUpdates
4
+ def self.included(base)
5
+ base.class_eval do
6
+ after_commit :update_attribute_values
7
+ end
8
+ end
9
+
10
+ private
11
+
12
+ def update_attribute_values
13
+ return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
14
+
15
+ config = ThinkingSphinx::Configuration.instance
16
+ client = Riddle::Client.new config.address, config.port
17
+
18
+ self.sphinx_indexes.each do |index|
19
+ attribute_pairs = attribute_values_for_index(index)
20
+ attribute_names = attribute_pairs.keys
21
+ attribute_values = attribute_names.collect { |key|
22
+ attribute_pairs[key]
23
+ }
24
+
25
+ client.update "#{index.name}_core", attribute_names, {
26
+ sphinx_document_id => attribute_values
27
+ } if in_core_index?
28
+ end
29
+ end
30
+
31
+ def updatable_attributes(index)
32
+ index.attributes.select { |attrib| attrib.updatable? }
33
+ end
34
+
35
+ def attribute_values_for_index(index)
36
+ updatable_attributes(index).inject({}) { |hash, attrib|
37
+ if attrib.type == :datetime
38
+ hash[attrib.unique_name.to_s] = attrib.live_value(self).to_time.to_i
39
+ else
40
+ hash[attrib.unique_name.to_s] = attrib.live_value self
41
+ end
42
+
43
+ hash
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,87 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ # This module contains all the delta-related code for models. There isn't
4
+ # really anything you need to call manually in here - except perhaps
5
+ # index_delta, but not sure what reason why.
6
+ #
7
+ module Delta
8
+ # Code for after_commit callback is written by Eli Miller:
9
+ # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
10
+ # with slight modification from Joost Hietbrink.
11
+ #
12
+ def self.included(base)
13
+ base.class_eval do
14
+ class << self
15
+ # Temporarily disable delta indexing inside a block, then perform a single
16
+ # rebuild of index at the end.
17
+ #
18
+ # Useful when performing updates to batches of models to prevent
19
+ # the delta index being rebuilt after each individual update.
20
+ #
21
+ # In the following example, the delta index will only be rebuilt once,
22
+ # not 10 times.
23
+ #
24
+ # SomeModel.suspended_delta do
25
+ # 10.times do
26
+ # SomeModel.create( ... )
27
+ # end
28
+ # end
29
+ #
30
+ def suspended_delta(reindex_after = true, &block)
31
+ original_setting = ThinkingSphinx.deltas_enabled?
32
+ ThinkingSphinx.deltas_enabled = false
33
+ begin
34
+ yield
35
+ ensure
36
+ ThinkingSphinx.deltas_enabled = original_setting
37
+ self.index_delta if reindex_after
38
+ end
39
+ end
40
+
41
+ # Build the delta index for the related model. This won't be called
42
+ # if running in the test environment.
43
+ #
44
+ def index_delta(instance = nil)
45
+ delta_object.index(self, instance)
46
+ end
47
+
48
+ def delta_object
49
+ self.sphinx_indexes.first.delta_object
50
+ end
51
+ end
52
+
53
+ def toggled_delta?
54
+ self.class.delta_object.toggled(self)
55
+ end
56
+
57
+ private
58
+
59
+ # Set the delta value for the model to be true.
60
+ def toggle_delta
61
+ self.class.delta_object.toggle(self) if should_toggle_delta?
62
+ end
63
+
64
+ # Build the delta index for the related model. This won't be called
65
+ # if running in the test environment.
66
+ #
67
+ def index_delta
68
+ self.class.index_delta(self) if self.class.delta_object.toggled(self)
69
+ end
70
+
71
+ def should_toggle_delta?
72
+ self.new_record? || indexed_data_changed?
73
+ end
74
+
75
+ def indexed_data_changed?
76
+ sphinx_indexes.any? { |index|
77
+ index.fields.any? { |field| field.changed?(self) } ||
78
+ index.attributes.any? { |attrib|
79
+ attrib.public? && attrib.changed?(self) && !attrib.updatable?
80
+ }
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,28 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module HasManyAssociation
4
+ def search(*args)
5
+ foreign_key = @reflection.primary_key_name
6
+ stack = [@reflection.options[:through]].compact
7
+
8
+ attribute = nil
9
+ (@reflection.klass.sphinx_indexes || []).each do |index|
10
+ attribute = index.attributes.detect { |attrib|
11
+ attrib.columns.length == 1 &&
12
+ attrib.columns.first.__name == foreign_key.to_sym
13
+ }
14
+ break if attribute
15
+ end
16
+
17
+ raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute
18
+
19
+ options = args.extract_options!
20
+ options[:with] ||= {}
21
+ options[:with][attribute.unique_name] = @owner.id
22
+
23
+ args << options
24
+ @reflection.klass.search(*args)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module Scopes
4
+ def self.included(base)
5
+ base.class_eval do
6
+ extend ThinkingSphinx::ActiveRecord::Scopes::ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def sphinx_scope(method, &block)
12
+ @sphinx_scopes ||= []
13
+ @sphinx_scopes << method
14
+
15
+ metaclass.instance_eval do
16
+ define_method(method) do |*args|
17
+ options = {:classes => classes_option}
18
+ options.merge! block.call(*args)
19
+
20
+ ThinkingSphinx::Search.new(options)
21
+ end
22
+ end
23
+ end
24
+
25
+ def sphinx_scopes
26
+ @sphinx_scopes || []
27
+ end
28
+
29
+ def remove_sphinx_scopes
30
+ sphinx_scopes.each do |scope|
31
+ metaclass.send(:undef_method, scope)
32
+ end
33
+
34
+ sphinx_scopes.clear
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ module ThinkingSphinx
2
+ class AbstractAdapter
3
+ def initialize(model)
4
+ @model = model
5
+ end
6
+
7
+ def setup
8
+ # Deliberately blank - subclasses should do something though. Well, if
9
+ # they need to.
10
+ end
11
+
12
+ def self.detect(model)
13
+ case model.connection.class.name
14
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
15
+ "ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
16
+ ThinkingSphinx::MysqlAdapter.new model
17
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
18
+ ThinkingSphinx::PostgreSQLAdapter.new model
19
+ when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
20
+ if model.connection.config[:adapter] == "jdbcmysql"
21
+ ThinkingSphinx::MysqlAdapter.new model
22
+ elsif model.connection.config[:adapter] == "jdbcpostgresql"
23
+ ThinkingSphinx::PostgreSQLAdapter.new model
24
+ else
25
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
26
+ end
27
+ else
28
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
29
+ end
30
+ end
31
+
32
+ def quote_with_table(column)
33
+ "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
34
+ end
35
+
36
+ protected
37
+
38
+ def connection
39
+ @connection ||= @model.connection
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ module ThinkingSphinx
2
+ class MysqlAdapter < AbstractAdapter
3
+ def setup
4
+ # Does MySQL actually need to do anything?
5
+ end
6
+
7
+ def sphinx_identifier
8
+ "mysql"
9
+ end
10
+
11
+ def concatenate(clause, separator = ' ')
12
+ "CONCAT_WS('#{separator}', #{clause})"
13
+ end
14
+
15
+ def group_concatenate(clause, separator = ' ')
16
+ "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')"
17
+ end
18
+
19
+ def cast_to_string(clause)
20
+ "CAST(#{clause} AS CHAR)"
21
+ end
22
+
23
+ def cast_to_datetime(clause)
24
+ "UNIX_TIMESTAMP(#{clause})"
25
+ end
26
+
27
+ def cast_to_unsigned(clause)
28
+ "CAST(#{clause} AS UNSIGNED)"
29
+ end
30
+
31
+ def convert_nulls(clause, default = '')
32
+ default = "'#{default}'" if default.is_a?(String)
33
+
34
+ "IFNULL(#{clause}, #{default})"
35
+ end
36
+
37
+ def boolean(value)
38
+ value ? 1 : 0
39
+ end
40
+
41
+ def crc(clause, blank_to_null = false)
42
+ clause = "NULLIF(#{clause},'')" if blank_to_null
43
+ "CRC32(#{clause})"
44
+ end
45
+
46
+ def utf8_query_pre
47
+ "SET NAMES utf8"
48
+ end
49
+
50
+ def time_difference(diff)
51
+ "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,136 @@
1
+ module ThinkingSphinx
2
+ class PostgreSQLAdapter < AbstractAdapter
3
+ def setup
4
+ create_array_accum_function
5
+ create_crc32_function
6
+ end
7
+
8
+ def sphinx_identifier
9
+ "pgsql"
10
+ end
11
+
12
+ def concatenate(clause, separator = ' ')
13
+ clause.split(', ').collect { |field|
14
+ case field
15
+ when /COALESCE/, "'')"
16
+ field
17
+ else
18
+ "COALESCE(CAST(#{field} as varchar), '')"
19
+ end
20
+ }.join(" || '#{separator}' || ")
21
+ end
22
+
23
+ def group_concatenate(clause, separator = ' ')
24
+ "array_to_string(array_accum(#{clause}), '#{separator}')"
25
+ end
26
+
27
+ def cast_to_string(clause)
28
+ clause
29
+ end
30
+
31
+ def cast_to_datetime(clause)
32
+ "cast(extract(epoch from #{clause}) as int)"
33
+ end
34
+
35
+ def cast_to_unsigned(clause)
36
+ clause
37
+ end
38
+
39
+ def convert_nulls(clause, default = '')
40
+ default = "'#{default}'" if default.is_a?(String)
41
+ default = 'NULL' if default.nil?
42
+
43
+ "COALESCE(#{clause}, #{default})"
44
+ end
45
+
46
+ def boolean(value)
47
+ value ? 'TRUE' : 'FALSE'
48
+ end
49
+
50
+ def crc(clause, blank_to_null = false)
51
+ clause = "NULLIF(#{clause},'')" if blank_to_null
52
+ "crc32(#{clause})"
53
+ end
54
+
55
+ def utf8_query_pre
56
+ nil
57
+ end
58
+
59
+ def time_difference(diff)
60
+ "current_timestamp - interval '#{diff} seconds'"
61
+ end
62
+
63
+ private
64
+
65
+ def execute(command, output_error = false)
66
+ connection.execute "begin"
67
+ connection.execute "savepoint ts"
68
+ begin
69
+ connection.execute command
70
+ rescue StandardError => err
71
+ puts err if output_error
72
+ connection.execute "rollback to savepoint ts"
73
+ end
74
+ connection.execute "release savepoint ts"
75
+ connection.execute "commit"
76
+ end
77
+
78
+ def create_array_accum_function
79
+ if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
80
+ execute <<-SQL
81
+ CREATE AGGREGATE array_accum (anyelement)
82
+ (
83
+ sfunc = array_append,
84
+ stype = anyarray,
85
+ initcond = '{}'
86
+ );
87
+ SQL
88
+ else
89
+ execute <<-SQL
90
+ CREATE AGGREGATE array_accum
91
+ (
92
+ basetype = anyelement,
93
+ sfunc = array_append,
94
+ stype = anyarray,
95
+ initcond = '{}'
96
+ );
97
+ SQL
98
+ end
99
+ end
100
+
101
+ def create_crc32_function
102
+ execute "CREATE LANGUAGE 'plpgsql';"
103
+ function = <<-SQL
104
+ CREATE OR REPLACE FUNCTION crc32(word text)
105
+ RETURNS bigint AS $$
106
+ DECLARE tmp bigint;
107
+ DECLARE i int;
108
+ DECLARE j int;
109
+ DECLARE word_array bytea;
110
+ BEGIN
111
+ i = 0;
112
+ tmp = 4294967295;
113
+ word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
114
+ LOOP
115
+ tmp = (tmp # get_byte(word_array, i))::bigint;
116
+ i = i + 1;
117
+ j = 0;
118
+ LOOP
119
+ tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
120
+ j = j + 1;
121
+ IF j >= 8 THEN
122
+ EXIT;
123
+ END IF;
124
+ END LOOP;
125
+ IF i >= char_length(word) THEN
126
+ EXIT;
127
+ END IF;
128
+ END LOOP;
129
+ return (tmp # 4294967295);
130
+ END
131
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
132
+ SQL
133
+ execute function, true
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,164 @@
1
+ module ThinkingSphinx
2
+ # Association tracks a specific reflection and join to reference data that
3
+ # isn't in the base model. Very much an internal class for Thinking Sphinx -
4
+ # perhaps because I feel it's not as strong (or simple) as most of the rest.
5
+ #
6
+ class Association
7
+ attr_accessor :parent, :reflection, :join
8
+
9
+ # Create a new association by passing in the parent association, and the
10
+ # corresponding reflection instance. If there is no parent, pass in nil.
11
+ #
12
+ # top = Association.new nil, top_reflection
13
+ # child = Association.new top, child_reflection
14
+ #
15
+ def initialize(parent, reflection)
16
+ @parent, @reflection = parent, reflection
17
+ @children = {}
18
+ end
19
+
20
+ # Get the children associations for a given association name. The only time
21
+ # that there'll actually be more than one association is when the
22
+ # relationship is polymorphic. To keep things simple though, it will always
23
+ # be an Array that gets returned (an empty one if no matches).
24
+ #
25
+ # # where pages is an association on the class tied to the reflection.
26
+ # association.children(:pages)
27
+ #
28
+ def children(assoc)
29
+ @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
30
+ end
31
+
32
+ # Get the children associations for a given class, association name and
33
+ # parent association. Much like the instance method of the same name, it
34
+ # will return an empty array if no associations have the name, and only
35
+ # have multiple association instances if the underlying relationship is
36
+ # polymorphic.
37
+ #
38
+ # Association.children(User, :pages, user_association)
39
+ #
40
+ def self.children(klass, assoc, parent=nil)
41
+ ref = klass.reflect_on_association(assoc)
42
+
43
+ return [] if ref.nil?
44
+ return [Association.new(parent, ref)] unless ref.options[:polymorphic]
45
+
46
+ # association is polymorphic - create associations for each
47
+ # non-polymorphic reflection.
48
+ polymorphic_classes(ref).collect { |klass|
49
+ Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
50
+ ref.macro,
51
+ "#{ref.name}_#{klass.name}".to_sym,
52
+ casted_options(klass, ref),
53
+ ref.active_record
54
+ )
55
+ }
56
+ end
57
+
58
+ # Link up the join for this model from a base join - and set parent
59
+ # associations' joins recursively.
60
+ #
61
+ def join_to(base_join)
62
+ parent.join_to(base_join) if parent && parent.join.nil?
63
+
64
+ @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
65
+ @reflection, base_join, parent ? parent.join : base_join.joins.first
66
+ )
67
+ end
68
+
69
+ # Returns the association's join SQL statements - and it replaces
70
+ # ::ts_join_alias:: with the aliased table name so the generated reflection
71
+ # join conditions avoid column name collisions.
72
+ #
73
+ def to_sql
74
+ @join.association_join.gsub(/::ts_join_alias::/,
75
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76
+ )
77
+ end
78
+
79
+ # Returns true if the association - or a parent - is a has_many or
80
+ # has_and_belongs_to_many.
81
+ #
82
+ def is_many?
83
+ case @reflection.macro
84
+ when :has_many, :has_and_belongs_to_many
85
+ true
86
+ else
87
+ @parent ? @parent.is_many? : false
88
+ end
89
+ end
90
+
91
+ # Returns an array of all the associations that lead to this one - starting
92
+ # with the top level all the way to the current association object.
93
+ #
94
+ def ancestors
95
+ (parent ? parent.ancestors : []) << self
96
+ end
97
+
98
+ def has_column?(column)
99
+ @reflection.klass.column_names.include?(column.to_s)
100
+ end
101
+
102
+ def primary_key_from_reflection
103
+ if @reflection.options[:through]
104
+ @reflection.source_reflection.options[:foreign_key] ||
105
+ @reflection.source_reflection.primary_key_name
106
+ elsif @reflection.macro == :has_and_belongs_to_many
107
+ @reflection.association_foreign_key
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ def table
114
+ if @reflection.options[:through] ||
115
+ @reflection.macro == :has_and_belongs_to_many
116
+ @join.aliased_join_table_name
117
+ else
118
+ @join.aliased_table_name
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Returns all the objects that could be currently instantiated from a
125
+ # polymorphic association. This is pretty damn fast if there's an index on
126
+ # the foreign type column - but if there isn't, it can take a while if you
127
+ # have a lot of data.
128
+ #
129
+ def self.polymorphic_classes(ref)
130
+ ref.active_record.connection.select_all(
131
+ "SELECT DISTINCT #{ref.options[:foreign_type]} " +
132
+ "FROM #{ref.active_record.table_name} " +
133
+ "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
134
+ ).collect { |row|
135
+ row[ref.options[:foreign_type]].constantize
136
+ }
137
+ end
138
+
139
+ # Returns a new set of options for an association that mimics an existing
140
+ # polymorphic relationship for a specific class. It adds a condition to
141
+ # filter by the appropriate object.
142
+ #
143
+ def self.casted_options(klass, ref)
144
+ options = ref.options.clone
145
+ options[:polymorphic] = nil
146
+ options[:class_name] = klass.name
147
+ options[:foreign_key] ||= "#{ref.name}_id"
148
+
149
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
150
+ case options[:conditions]
151
+ when nil
152
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
153
+ when Array
154
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
155
+ when Hash
156
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
157
+ else
158
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
159
+ end
160
+
161
+ options
162
+ end
163
+ end
164
+ end