ryanb-thinking_sphinx 0.9.8
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.
- data/LICENCE +20 -0
- data/README +60 -0
- data/lib/riddle.rb +26 -0
- data/lib/riddle/client.rb +639 -0
- data/lib/riddle/client/filter.rb +44 -0
- data/lib/riddle/client/message.rb +65 -0
- data/lib/riddle/client/response.rb +84 -0
- data/lib/test.rb +46 -0
- data/lib/thinking_sphinx.rb +102 -0
- data/lib/thinking_sphinx/active_record.rb +141 -0
- data/lib/thinking_sphinx/active_record/delta.rb +97 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/search.rb +50 -0
- data/lib/thinking_sphinx/association.rb +144 -0
- data/lib/thinking_sphinx/attribute.rb +284 -0
- data/lib/thinking_sphinx/configuration.rb +283 -0
- data/lib/thinking_sphinx/field.rb +200 -0
- data/lib/thinking_sphinx/index.rb +340 -0
- data/lib/thinking_sphinx/index/builder.rb +195 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/rails_additions.rb +56 -0
- data/lib/thinking_sphinx/search.rb +482 -0
- data/lib/thinking_sphinx/tasks.rb +86 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +207 -0
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
- data/spec/unit/thinking_sphinx/active_record_spec.rb +236 -0
- data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
- data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +493 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +219 -0
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +277 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +190 -0
- data/spec/unit/thinking_sphinx_spec.rb +129 -0
- data/tasks/thinking_sphinx_tasks.rake +1 -0
- metadata +103 -0
@@ -0,0 +1,97 @@
|
|
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
|
+
# The define_callbacks method was added post Rails 2.0.2 - if it
|
15
|
+
# doesn't exist, we define the callback manually
|
16
|
+
#
|
17
|
+
if respond_to?(:define_callbacks)
|
18
|
+
define_callbacks :after_commit
|
19
|
+
else
|
20
|
+
class << self
|
21
|
+
# Handle after_commit callbacks - call all the registered callbacks.
|
22
|
+
#
|
23
|
+
def after_commit(*callbacks, &block)
|
24
|
+
callbacks << block if block_given?
|
25
|
+
write_inheritable_array(:after_commit, callbacks)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def after_commit
|
31
|
+
# Deliberately blank.
|
32
|
+
end
|
33
|
+
|
34
|
+
# Normal boolean save wrapped in a handler for the after_commit
|
35
|
+
# callback.
|
36
|
+
#
|
37
|
+
def save_with_after_commit_callback(*args)
|
38
|
+
value = save_without_after_commit_callback(*args)
|
39
|
+
callback(:after_commit) if value
|
40
|
+
return value
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method_chain :save, :after_commit_callback
|
44
|
+
|
45
|
+
# Forceful save wrapped in a handler for the after_commit callback.
|
46
|
+
#
|
47
|
+
def save_with_after_commit_callback!(*args)
|
48
|
+
value = save_without_after_commit_callback!(*args)
|
49
|
+
callback(:after_commit) if value
|
50
|
+
return value
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method_chain :save!, :after_commit_callback
|
54
|
+
|
55
|
+
# Normal destroy wrapped in a handler for the after_commit callback.
|
56
|
+
#
|
57
|
+
def destroy_with_after_commit_callback
|
58
|
+
value = destroy_without_after_commit_callback
|
59
|
+
callback(:after_commit) if value
|
60
|
+
return value
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method_chain :destroy, :after_commit_callback
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Set the delta value for the model to be true.
|
68
|
+
def toggle_delta
|
69
|
+
self.delta = true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Build the delta index for the related model. This won't be called
|
73
|
+
# if running in the test environment.
|
74
|
+
#
|
75
|
+
def index_delta
|
76
|
+
return true unless ThinkingSphinx.updates_enabled? &&
|
77
|
+
ThinkingSphinx.deltas_enabled?
|
78
|
+
|
79
|
+
config = ThinkingSphinx::Configuration.new
|
80
|
+
client = Riddle::Client.new config.address, config.port
|
81
|
+
|
82
|
+
client.update(
|
83
|
+
"#{self.class.indexes.first.name}_core",
|
84
|
+
['sphinx_deleted'],
|
85
|
+
{self.id => 1}
|
86
|
+
) if self.in_core_index?
|
87
|
+
|
88
|
+
configuration = ThinkingSphinx::Configuration.new
|
89
|
+
system "indexer --config #{configuration.config_file} --rotate #{self.class.indexes.first.name}_delta"
|
90
|
+
|
91
|
+
true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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.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
|
+
attrib.columns.first.__stack == stack
|
14
|
+
}
|
15
|
+
break if attribute
|
16
|
+
end
|
17
|
+
|
18
|
+
raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute
|
19
|
+
|
20
|
+
options = args.extract_options!
|
21
|
+
options[:with] ||= {}
|
22
|
+
options[:with][attribute.unique_name] = @owner.id
|
23
|
+
|
24
|
+
args << options
|
25
|
+
@reflection.klass.search(*args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module ActiveRecord
|
3
|
+
# This module covers the specific model searches - but the syntax is
|
4
|
+
# exactly the same as the core Search class - so use that as your refence
|
5
|
+
# point.
|
6
|
+
#
|
7
|
+
module Search
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
class << self
|
11
|
+
# Searches for results that match the parameters provided. Will only
|
12
|
+
# return the ids for the matching objects. See
|
13
|
+
# ThinkingSphinx::Search#search for syntax examples.
|
14
|
+
#
|
15
|
+
def search_for_ids(*args)
|
16
|
+
options = args.extract_options!
|
17
|
+
options[:class] = self
|
18
|
+
args << options
|
19
|
+
ThinkingSphinx::Search.search_for_ids(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Searches for results limited to a single model. See
|
23
|
+
# ThinkingSphinx::Search#search for syntax examples.
|
24
|
+
#
|
25
|
+
def search(*args)
|
26
|
+
options = args.extract_options!
|
27
|
+
options[:class] = self
|
28
|
+
args << options
|
29
|
+
ThinkingSphinx::Search.search(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def search_count(*args)
|
33
|
+
options = args.extract_options!
|
34
|
+
options[:class] = self
|
35
|
+
args << options
|
36
|
+
ThinkingSphinx::Search.count(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def search_for_id(*args)
|
40
|
+
options = args.extract_options!
|
41
|
+
options[:class] = self
|
42
|
+
args << options
|
43
|
+
ThinkingSphinx::Search.search_for_id(*args)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,144 @@
|
|
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
|
+
private
|
103
|
+
|
104
|
+
# Returns all the objects that could be currently instantiated from a
|
105
|
+
# polymorphic association. This is pretty damn fast if there's an index on
|
106
|
+
# the foreign type column - but if there isn't, it can take a while if you
|
107
|
+
# have a lot of data.
|
108
|
+
#
|
109
|
+
def self.polymorphic_classes(ref)
|
110
|
+
ref.active_record.connection.select_all(
|
111
|
+
"SELECT DISTINCT #{ref.options[:foreign_type]} " +
|
112
|
+
"FROM #{ref.active_record.table_name} " +
|
113
|
+
"WHERE #{ref.options[:foreign_type]} IS NOT NULL"
|
114
|
+
).collect { |row|
|
115
|
+
row[ref.options[:foreign_type]].constantize
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a new set of options for an association that mimics an existing
|
120
|
+
# polymorphic relationship for a specific class. It adds a condition to
|
121
|
+
# filter by the appropriate object.
|
122
|
+
#
|
123
|
+
def self.casted_options(klass, ref)
|
124
|
+
options = ref.options.clone
|
125
|
+
options[:polymorphic] = nil
|
126
|
+
options[:class_name] = klass.name
|
127
|
+
options[:foreign_key] ||= "#{ref.name}_id"
|
128
|
+
|
129
|
+
quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
|
130
|
+
case options[:conditions]
|
131
|
+
when nil
|
132
|
+
options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
|
133
|
+
when Array
|
134
|
+
options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
|
135
|
+
when Hash
|
136
|
+
options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
|
137
|
+
else
|
138
|
+
options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
|
139
|
+
end
|
140
|
+
|
141
|
+
options
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
# Attributes - eternally useful when it comes to filtering, sorting or
|
3
|
+
# grouping. This class isn't really useful to you unless you're hacking
|
4
|
+
# around with the internals of Thinking Sphinx - but hey, don't let that
|
5
|
+
# stop you.
|
6
|
+
#
|
7
|
+
# One key thing to remember - if you're using the attribute manually to
|
8
|
+
# generate SQL statements, you'll need to set the base model, and all the
|
9
|
+
# associations. Which can get messy. Use Index.link!, it really helps.
|
10
|
+
#
|
11
|
+
class Attribute
|
12
|
+
attr_accessor :alias, :columns, :associations, :model
|
13
|
+
|
14
|
+
# To create a new attribute, you'll need to pass in either a single Column
|
15
|
+
# or an array of them, and some (optional) options.
|
16
|
+
#
|
17
|
+
# Valid options are:
|
18
|
+
# - :as => :alias_name
|
19
|
+
# - :type => :attribute_type
|
20
|
+
#
|
21
|
+
# Alias is only required in three circumstances: when there's
|
22
|
+
# another attribute or field with the same name, when the column name is
|
23
|
+
# 'id', or when there's more than one column.
|
24
|
+
#
|
25
|
+
# Type is not required, unless you want to force a column to be a certain
|
26
|
+
# type (but keep in mind the value will not be CASTed in the SQL
|
27
|
+
# statements). The only time you really need to use this is when the type
|
28
|
+
# can't be figured out by the column - ie: when not actually using a
|
29
|
+
# database column as your source.
|
30
|
+
#
|
31
|
+
# Example usage:
|
32
|
+
#
|
33
|
+
# Attribute.new(
|
34
|
+
# Column.new(:created_at)
|
35
|
+
# )
|
36
|
+
#
|
37
|
+
# Attribute.new(
|
38
|
+
# Column.new(:posts, :id),
|
39
|
+
# :as => :post_ids
|
40
|
+
# )
|
41
|
+
#
|
42
|
+
# Attribute.new(
|
43
|
+
# [Column.new(:pages, :id), Column.new(:articles, :id)],
|
44
|
+
# :as => :content_ids
|
45
|
+
# )
|
46
|
+
#
|
47
|
+
# Attribute.new(
|
48
|
+
# Column.new("NOW()"),
|
49
|
+
# :as => :indexed_at,
|
50
|
+
# :type => :datetime
|
51
|
+
# )
|
52
|
+
#
|
53
|
+
# If you're creating attributes for latitude and longitude, don't forget
|
54
|
+
# that Sphinx expects these values to be in radians.
|
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
|
+
@type = options[:type]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get the part of the SELECT clause related to this attribute. Don't forget
|
67
|
+
# to set your model and associations first though.
|
68
|
+
#
|
69
|
+
# This will concatenate strings and arrays of integers, and convert
|
70
|
+
# datetimes to timestamps, as needed.
|
71
|
+
#
|
72
|
+
def to_select_sql
|
73
|
+
clause = @columns.collect { |column|
|
74
|
+
column_with_prefix(column)
|
75
|
+
}.join(', ')
|
76
|
+
|
77
|
+
separator = all_ints? ? ',' : ' '
|
78
|
+
|
79
|
+
clause = concatenate(clause, separator) if concat_ws?
|
80
|
+
clause = group_concatenate(clause, separator) if is_many?
|
81
|
+
clause = cast_to_datetime(clause) if type == :datetime
|
82
|
+
clause = convert_nulls(clause) if type == :string
|
83
|
+
|
84
|
+
"#{clause} AS #{quote_column(unique_name)}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the part of the GROUP BY clause related to this attribute - if one is
|
88
|
+
# needed. If not, all you'll get back is nil. The latter will happen if
|
89
|
+
# there isn't actually a real column to get data from, or if there's
|
90
|
+
# multiple data values (read: a has_many or has_and_belongs_to_many
|
91
|
+
# association).
|
92
|
+
#
|
93
|
+
def to_group_sql
|
94
|
+
case
|
95
|
+
when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
|
96
|
+
nil
|
97
|
+
else
|
98
|
+
@columns.collect { |column|
|
99
|
+
column_with_prefix(column)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Generates the appropriate attribute statement for a Sphinx configuration
|
105
|
+
# file, depending on the attribute's type.
|
106
|
+
#
|
107
|
+
def to_sphinx_clause
|
108
|
+
case type
|
109
|
+
when :multi
|
110
|
+
"sql_attr_multi = uint #{unique_name} from field"
|
111
|
+
when :datetime
|
112
|
+
"sql_attr_timestamp = #{unique_name}"
|
113
|
+
when :string
|
114
|
+
"sql_attr_str2ordinal = #{unique_name}"
|
115
|
+
when :float
|
116
|
+
"sql_attr_float = #{unique_name}"
|
117
|
+
when :boolean
|
118
|
+
"sql_attr_bool = #{unique_name}"
|
119
|
+
else
|
120
|
+
"sql_attr_uint = #{unique_name}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the unique name of the attribute - which is either the alias of
|
125
|
+
# the attribute, or the name of the only column - if there is only one. If
|
126
|
+
# there isn't, there should be an alias. Else things probably won't work.
|
127
|
+
# Consider yourself warned.
|
128
|
+
#
|
129
|
+
def unique_name
|
130
|
+
if @columns.length == 1
|
131
|
+
@alias || @columns.first.__name
|
132
|
+
else
|
133
|
+
@alias
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def concatenate(clause, separator = ' ')
|
140
|
+
case @model.connection.class.name
|
141
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
142
|
+
"CONCAT_WS('#{separator}', #{clause})"
|
143
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
144
|
+
clause.split(', ').join(" || '#{separator}' || ")
|
145
|
+
else
|
146
|
+
clause
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def group_concatenate(clause, separator = ' ')
|
151
|
+
case @model.connection.class.name
|
152
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
153
|
+
"GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')"
|
154
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
155
|
+
"array_to_string(array_accum(#{clause}), '#{separator}')"
|
156
|
+
else
|
157
|
+
clause
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def cast_to_string(clause)
|
162
|
+
case @model.connection.class.name
|
163
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
164
|
+
"CAST(#{clause} AS CHAR)"
|
165
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
166
|
+
clause
|
167
|
+
else
|
168
|
+
clause
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def cast_to_datetime(clause)
|
173
|
+
case @model.connection.class.name
|
174
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
175
|
+
"UNIX_TIMESTAMP(#{clause})"
|
176
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
177
|
+
clause # Rails' datetimes are timestamps in PostgreSQL
|
178
|
+
else
|
179
|
+
clause
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def convert_nulls(clause)
|
184
|
+
case @model.connection.class.name
|
185
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
186
|
+
"IFNULL(#{clause}, '')"
|
187
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
188
|
+
"COALESCE(#{clause}, '')"
|
189
|
+
else
|
190
|
+
clause
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def quote_column(column)
|
195
|
+
@model.connection.quote_column_name(column)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Indication of whether the columns should be concatenated with a space
|
199
|
+
# between each value. True if there's either multiple sources or multiple
|
200
|
+
# associations.
|
201
|
+
#
|
202
|
+
def concat_ws?
|
203
|
+
multiple_associations? || @columns.length > 1
|
204
|
+
end
|
205
|
+
|
206
|
+
# Checks the association tree for each column - if they're all the same,
|
207
|
+
# returns false.
|
208
|
+
#
|
209
|
+
def multiple_sources?
|
210
|
+
first = associations[@columns.first]
|
211
|
+
|
212
|
+
!@columns.all? { |col| associations[col] == first }
|
213
|
+
end
|
214
|
+
|
215
|
+
# Checks whether any column requires multiple associations (which only
|
216
|
+
# happens for polymorphic situations).
|
217
|
+
#
|
218
|
+
def multiple_associations?
|
219
|
+
associations.any? { |col,assocs| assocs.length > 1 }
|
220
|
+
end
|
221
|
+
|
222
|
+
# Builds a column reference tied to the appropriate associations. This
|
223
|
+
# dives into the associations hash and their corresponding joins to
|
224
|
+
# figure out how to correctly reference a column in SQL.
|
225
|
+
#
|
226
|
+
def column_with_prefix(column)
|
227
|
+
if column.is_string?
|
228
|
+
column.__name
|
229
|
+
elsif associations[column].empty?
|
230
|
+
"#{@model.quoted_table_name}.#{quote_column(column.__name)}"
|
231
|
+
else
|
232
|
+
associations[column].collect { |assoc|
|
233
|
+
assoc.has_column?(column.__name) ?
|
234
|
+
"#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
|
235
|
+
".#{quote_column(column.__name)}" :
|
236
|
+
nil
|
237
|
+
}.compact.join(', ')
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Could there be more than one value related to the parent record? If so,
|
242
|
+
# then this will return true. If not, false. It's that simple.
|
243
|
+
#
|
244
|
+
def is_many?
|
245
|
+
associations.values.flatten.any? { |assoc| assoc.is_many? }
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns true if any of the columns are string values, instead of database
|
249
|
+
# column references.
|
250
|
+
def is_string?
|
251
|
+
columns.all? { |col| col.is_string? }
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns the type of the column. If that's not already set, it returns
|
255
|
+
# :multi if there's the possibility of more than one value, :string if
|
256
|
+
# there's more than one association, otherwise it figures out what the
|
257
|
+
# actual column's datatype is and returns that.
|
258
|
+
def type
|
259
|
+
@type ||= case
|
260
|
+
when is_many?
|
261
|
+
:multi
|
262
|
+
when @associations.values.flatten.length > 1
|
263
|
+
:string
|
264
|
+
else
|
265
|
+
klass = @associations.values.flatten.first ?
|
266
|
+
@associations.values.flatten.first.reflection.klass : @model
|
267
|
+
klass.columns.detect { |col|
|
268
|
+
@columns.collect { |c| c.__name.to_s }.include? col.name
|
269
|
+
}.type
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def all_ints?
|
274
|
+
@columns.all? { |col|
|
275
|
+
klasses = @associations[col].empty? ? [@model] :
|
276
|
+
@associations[col].collect { |assoc| assoc.reflection.klass }
|
277
|
+
klasses.all? { |klass|
|
278
|
+
column = klass.columns.detect { |column| column.name == col.__name.to_s }
|
279
|
+
!column.nil? && column.type == :integer
|
280
|
+
}
|
281
|
+
}
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|