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.
- data/LICENCE +20 -0
- data/README +107 -0
- data/README.textile +107 -0
- data/Rakefile +4 -0
- data/contribute.rb +328 -0
- data/cucumber.yml +1 -0
- data/features/a.rb +17 -0
- data/features/attribute_transformation.feature +22 -0
- data/features/datetime_deltas.feature +55 -0
- data/features/delayed_delta_indexing.feature +37 -0
- data/features/deleting_instances.feature +52 -0
- data/features/facets.feature +26 -0
- data/features/handling_edits.feature +67 -0
- data/features/retry_stale_indexes.feature +24 -0
- data/features/searching_across_models.feature +20 -0
- data/features/searching_by_model.feature +118 -0
- data/features/searching_with_find_arguments.feature +56 -0
- data/features/sphinx_detection.feature +16 -0
- data/features/step_definitions/alpha_steps.rb +3 -0
- data/features/step_definitions/beta_steps.rb +11 -0
- data/features/step_definitions/cat_steps.rb +3 -0
- data/features/step_definitions/common_steps.rb +154 -0
- data/features/step_definitions/datetime_delta_steps.rb +11 -0
- data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
- data/features/step_definitions/facet_steps.rb +30 -0
- data/features/step_definitions/find_arguments_steps.rb +36 -0
- data/features/step_definitions/gamma_steps.rb +15 -0
- data/features/step_definitions/search_steps.rb +66 -0
- data/features/step_definitions/sphinx_steps.rb +23 -0
- data/features/support/db/active_record.rb +40 -0
- data/features/support/db/database.example.yml +4 -0
- data/features/support/db/migrations/create_alphas.rb +18 -0
- data/features/support/db/migrations/create_animals.rb +9 -0
- data/features/support/db/migrations/create_betas.rb +15 -0
- data/features/support/db/migrations/create_boxes.rb +13 -0
- data/features/support/db/migrations/create_comments.rb +13 -0
- data/features/support/db/migrations/create_delayed_betas.rb +28 -0
- data/features/support/db/migrations/create_developers.rb +39 -0
- data/features/support/db/migrations/create_gammas.rb +14 -0
- data/features/support/db/migrations/create_people.rb +1014 -0
- data/features/support/db/migrations/create_posts.rb +6 -0
- data/features/support/db/migrations/create_thetas.rb +16 -0
- data/features/support/db/mysql.rb +4 -0
- data/features/support/db/postgresql.rb +4 -0
- data/features/support/env.rb +6 -0
- data/features/support/models/alpha.rb +9 -0
- data/features/support/models/animal.rb +5 -0
- data/features/support/models/beta.rb +7 -0
- data/features/support/models/box.rb +8 -0
- data/features/support/models/cat.rb +3 -0
- data/features/support/models/comment.rb +3 -0
- data/features/support/models/delayed_beta.rb +7 -0
- data/features/support/models/developer.rb +8 -0
- data/features/support/models/gamma.rb +5 -0
- data/features/support/models/person.rb +8 -0
- data/features/support/models/post.rb +8 -0
- data/features/support/models/theta.rb +7 -0
- data/features/support/post_database.rb +37 -0
- data/features/support/z.rb +19 -0
- data/ginger_scenarios.rb +24 -0
- data/init.rb +12 -0
- data/lib/thinking_sphinx.rb +144 -0
- data/lib/thinking_sphinx/active_record.rb +245 -0
- data/lib/thinking_sphinx/active_record/delta.rb +74 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/search.rb +57 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
- data/lib/thinking_sphinx/association.rb +144 -0
- data/lib/thinking_sphinx/attribute.rb +258 -0
- data/lib/thinking_sphinx/collection.rb +142 -0
- data/lib/thinking_sphinx/configuration.rb +236 -0
- data/lib/thinking_sphinx/core/string.rb +22 -0
- data/lib/thinking_sphinx/deltas.rb +22 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
- data/lib/thinking_sphinx/facet.rb +58 -0
- data/lib/thinking_sphinx/facet_collection.rb +44 -0
- data/lib/thinking_sphinx/field.rb +172 -0
- data/lib/thinking_sphinx/index.rb +414 -0
- data/lib/thinking_sphinx/index/builder.rb +233 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/rails_additions.rb +133 -0
- data/lib/thinking_sphinx/search.rb +638 -0
- data/lib/thinking_sphinx/tasks.rb +128 -0
- data/rails/init.rb +6 -0
- data/spec/fixtures/data.sql +32 -0
- data/spec/fixtures/database.yml.default +3 -0
- data/spec/fixtures/models.rb +81 -0
- data/spec/fixtures/structure.sql +84 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/sphinx_helper.rb +109 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -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 +256 -0
- data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
- data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
- data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
- data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
- data/spec/unit/thinking_sphinx_spec.rb +129 -0
- data/tasks/distribution.rb +48 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +86 -0
- data/thinking-sphinx.gemspec +232 -0
- data/vendor/after_commit/LICENSE +20 -0
- data/vendor/after_commit/README +16 -0
- data/vendor/after_commit/Rakefile +22 -0
- data/vendor/after_commit/init.rb +5 -0
- data/vendor/after_commit/lib/after_commit.rb +42 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
- data/vendor/after_commit/test/after_commit_test.rb +53 -0
- data/vendor/delayed_job/lib/delayed/job.rb +251 -0
- data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
- data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
- data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
- data/vendor/riddle/lib/riddle.rb +30 -0
- data/vendor/riddle/lib/riddle/client.rb +619 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
- data/vendor/riddle/lib/riddle/client/message.rb +65 -0
- data/vendor/riddle/lib/riddle/client/response.rb +84 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
- data/vendor/riddle/lib/riddle/controller.rb +44 -0
- metadata +248 -0
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
private
|
|
54
|
+
|
|
55
|
+
# Set the delta value for the model to be true.
|
|
56
|
+
def toggle_delta
|
|
57
|
+
self.class.delta_object.toggle(self) if should_toggle_delta?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Build the delta index for the related model. This won't be called
|
|
61
|
+
# if running in the test environment.
|
|
62
|
+
#
|
|
63
|
+
def index_delta
|
|
64
|
+
self.class.index_delta(self) if self.class.delta_object.toggled(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def should_toggle_delta?
|
|
68
|
+
!self.respond_to?(:changed?) || self.changed? || self.new_record?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
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.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
|
+
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,57 @@
|
|
|
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
|
+
|
|
46
|
+
def facets(*args)
|
|
47
|
+
options = args.extract_options!
|
|
48
|
+
options[:class] = self
|
|
49
|
+
args << options
|
|
50
|
+
ThinkingSphinx::Search.facets(*args)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
else
|
|
20
|
+
raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def quote_with_table(column)
|
|
25
|
+
"#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def connection
|
|
31
|
+
@connection ||= @model.connection
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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(#{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)
|
|
42
|
+
"CRC32(#{clause})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def utf8_query_pre
|
|
46
|
+
"SET NAMES utf8"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def time_difference(diff)
|
|
50
|
+
"DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
"COALESCE(CAST(#{field} as varchar), '')"
|
|
15
|
+
}.join(" || '#{separator}' || ")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def group_concatenate(clause, separator = ' ')
|
|
19
|
+
"array_to_string(array_accum(#{clause}), '#{separator}')"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cast_to_string(clause)
|
|
23
|
+
clause
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cast_to_datetime(clause)
|
|
27
|
+
"cast(extract(epoch from #{clause}) as int)"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cast_to_unsigned(clause)
|
|
31
|
+
clause
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def convert_nulls(clause, default = '')
|
|
35
|
+
default = "'#{default}'" if default.is_a?(String)
|
|
36
|
+
|
|
37
|
+
"COALESCE(#{clause}, #{default})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def boolean(value)
|
|
41
|
+
value ? 'TRUE' : 'FALSE'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def crc(clause)
|
|
45
|
+
"crc32(#{clause})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def utf8_query_pre
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def time_difference(diff)
|
|
53
|
+
"current_timestamp - interval '#{diff} seconds'"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def execute(command, output_error = false)
|
|
59
|
+
connection.execute "begin"
|
|
60
|
+
connection.execute "savepoint ts"
|
|
61
|
+
begin
|
|
62
|
+
connection.execute command
|
|
63
|
+
rescue StandardError => err
|
|
64
|
+
puts err if output_error
|
|
65
|
+
connection.execute "rollback to savepoint ts"
|
|
66
|
+
end
|
|
67
|
+
connection.execute "release savepoint ts"
|
|
68
|
+
connection.execute "commit"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_array_accum_function
|
|
72
|
+
if connection.raw_connection.server_version > 80200
|
|
73
|
+
execute <<-SQL
|
|
74
|
+
CREATE AGGREGATE array_accum (anyelement)
|
|
75
|
+
(
|
|
76
|
+
sfunc = array_append,
|
|
77
|
+
stype = anyarray,
|
|
78
|
+
initcond = '{}'
|
|
79
|
+
);
|
|
80
|
+
SQL
|
|
81
|
+
else
|
|
82
|
+
execute <<-SQL
|
|
83
|
+
CREATE AGGREGATE array_accum
|
|
84
|
+
(
|
|
85
|
+
basetype = anyelement,
|
|
86
|
+
sfunc = array_append,
|
|
87
|
+
stype = anyarray,
|
|
88
|
+
initcond = '{}'
|
|
89
|
+
);
|
|
90
|
+
SQL
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_crc32_function
|
|
95
|
+
execute "CREATE LANGUAGE 'plpgsql';"
|
|
96
|
+
function = <<-SQL
|
|
97
|
+
CREATE OR REPLACE FUNCTION crc32(word text)
|
|
98
|
+
RETURNS bigint AS $$
|
|
99
|
+
DECLARE tmp bigint;
|
|
100
|
+
DECLARE i int;
|
|
101
|
+
DECLARE j int;
|
|
102
|
+
DECLARE word_array bytea;
|
|
103
|
+
BEGIN
|
|
104
|
+
i = 0;
|
|
105
|
+
tmp = 4294967295;
|
|
106
|
+
word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
|
|
107
|
+
LOOP
|
|
108
|
+
tmp = (tmp # get_byte(word_array, i))::bigint;
|
|
109
|
+
i = i + 1;
|
|
110
|
+
j = 0;
|
|
111
|
+
LOOP
|
|
112
|
+
tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
|
|
113
|
+
j = j + 1;
|
|
114
|
+
IF j >= 8 THEN
|
|
115
|
+
EXIT;
|
|
116
|
+
END IF;
|
|
117
|
+
END LOOP;
|
|
118
|
+
IF i >= char_length(word) THEN
|
|
119
|
+
EXIT;
|
|
120
|
+
END IF;
|
|
121
|
+
END LOOP;
|
|
122
|
+
return (tmp # 4294967295);
|
|
123
|
+
END
|
|
124
|
+
$$ IMMUTABLE STRICT LANGUAGE plpgsql;
|
|
125
|
+
SQL
|
|
126
|
+
execute function, true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
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
|