pg_search 0.2.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -2
- data/.rspec +1 -0
- data/.travis.yml +2 -0
- data/CHANGELOG +8 -5
- data/Gemfile +10 -5
- data/README.rdoc +151 -44
- data/Rakefile +9 -24
- data/lib/pg_search.rb +45 -21
- data/lib/pg_search/configuration.rb +13 -1
- data/lib/pg_search/configuration/association.rb +5 -6
- data/lib/pg_search/configuration/column.rb +1 -2
- data/lib/pg_search/document.rb +21 -0
- data/lib/pg_search/features/tsearch.rb +19 -10
- data/lib/pg_search/multisearch.rb +46 -0
- data/lib/pg_search/multisearchable.rb +28 -0
- data/lib/pg_search/railtie.rb +0 -4
- data/lib/pg_search/scope.rb +1 -1
- data/lib/pg_search/scope_options.rb +13 -17
- data/lib/pg_search/tasks.rb +37 -0
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +3 -0
- data/spec/associations_spec.rb +209 -230
- data/spec/pg_search/document_spec.rb +49 -0
- data/spec/pg_search/multisearch_spec.rb +66 -0
- data/spec/pg_search/multisearchable_spec.rb +108 -0
- data/spec/pg_search_spec.rb +148 -42
- data/spec/spec_helper.rb +6 -2
- metadata +83 -31
- checksums.yaml +0 -7
- data/TODO +0 -10
- data/gemfiles/Gemfile.common +0 -9
- data/gemfiles/rails2/Gemfile +0 -4
- data/gemfiles/rails3/Gemfile +0 -4
@@ -4,7 +4,7 @@ module PgSearch
|
|
4
4
|
class Configuration
|
5
5
|
class Association
|
6
6
|
attr_reader :columns
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(model, name, column_names)
|
9
9
|
@model = model
|
10
10
|
@name = name
|
@@ -12,11 +12,11 @@ module PgSearch
|
|
12
12
|
Column.new(column_name, weight, @model, self)
|
13
13
|
end
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def table_name
|
17
17
|
@model.reflect_on_association(@name).table_name
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def join(primary_key)
|
21
21
|
selects = columns.map do |column|
|
22
22
|
"string_agg(#{column.full_name}, ' ') AS #{column.alias}"
|
@@ -24,10 +24,9 @@ module PgSearch
|
|
24
24
|
relation = @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
|
25
25
|
"LEFT OUTER JOIN (#{relation.to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def subselect_alias
|
29
|
-
|
30
|
-
"pg_search_#{Digest::SHA2.hexdigest(subselect_name)}"
|
29
|
+
Configuration.alias(table_name, @name, "subselect")
|
31
30
|
end
|
32
31
|
end
|
33
32
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pg_search/scope"
|
2
|
+
|
3
|
+
module PgSearch
|
4
|
+
class Document < ActiveRecord::Base
|
5
|
+
include PgSearch
|
6
|
+
set_table_name :pg_search_documents
|
7
|
+
belongs_to :searchable, :polymorphic => true
|
8
|
+
|
9
|
+
before_validation :update_content
|
10
|
+
|
11
|
+
pg_search_scope :search, :against => :content
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def update_content
|
16
|
+
methods = Array.wrap(searchable.pg_search_multisearchable_options[:against])
|
17
|
+
searchable_text = methods.map { |symbol| searchable.send(symbol) }.join(" ")
|
18
|
+
self.content = searchable_text
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -46,22 +46,31 @@ module PgSearch
|
|
46
46
|
tsquery_sql = "#{tsquery_sql} || #{connection.quote(':*')}" if @options[:prefix]
|
47
47
|
|
48
48
|
"to_tsquery(:dictionary, #{tsquery_sql})"
|
49
|
-
end.join(
|
49
|
+
end.join(@options[:any_word] ? ' || ' : ' && ')
|
50
50
|
end
|
51
51
|
|
52
52
|
def tsdocument
|
53
|
-
|
54
|
-
@
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
@columns.map do |search_column|
|
54
|
+
tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
|
55
|
+
search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
|
56
|
+
end.join(" || ")
|
57
|
+
end
|
58
|
+
|
59
|
+
# From http://www.postgresql.org/docs/8.3/static/textsearch-controls.html
|
60
|
+
# 0 (the default) ignores the document length
|
61
|
+
# 1 divides the rank by 1 + the logarithm of the document length
|
62
|
+
# 2 divides the rank by the document length
|
63
|
+
# 4 divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd)
|
64
|
+
# 8 divides the rank by the number of unique words in document
|
65
|
+
# 16 divides the rank by 1 + the logarithm of the number of unique words in document
|
66
|
+
# 32 divides the rank by itself + 1
|
67
|
+
# The integer option controls several behaviors, so it is a bit mask: you can specify one or more behaviors
|
68
|
+
def normalization
|
69
|
+
@options[:normalization] || 0
|
61
70
|
end
|
62
71
|
|
63
72
|
def tsearch_rank
|
64
|
-
["ts_rank((#{tsdocument}), (#{tsquery}))", interpolations]
|
73
|
+
["ts_rank((#{tsdocument}), (#{tsquery}), #{normalization})", interpolations]
|
65
74
|
end
|
66
75
|
|
67
76
|
def dictionary
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module PgSearch
|
2
|
+
module Multisearch
|
3
|
+
REBUILD_SQL_TEMPLATE = <<-SQL
|
4
|
+
INSERT INTO :documents_table (searchable_type, searchable_id, content)
|
5
|
+
SELECT :model_name AS searchable_type,
|
6
|
+
:model_table.id AS searchable_id,
|
7
|
+
(
|
8
|
+
:content_expressions
|
9
|
+
) AS content
|
10
|
+
FROM :model_table
|
11
|
+
SQL
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def rebuild(model)
|
15
|
+
model.transaction do
|
16
|
+
PgSearch::Document.where(:searchable_type => model.name).delete_all
|
17
|
+
model.connection.execute(rebuild_sql(model))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def rebuild_sql(model)
|
22
|
+
connection = model.connection
|
23
|
+
|
24
|
+
columns = Array.wrap(
|
25
|
+
model.pg_search_multisearchable_options[:against]
|
26
|
+
)
|
27
|
+
|
28
|
+
content_expressions = columns.map do |column|
|
29
|
+
%Q{coalesce(:model_table.#{column}, '')}
|
30
|
+
end.join(" || ' ' || ")
|
31
|
+
|
32
|
+
REBUILD_SQL_TEMPLATE.gsub(
|
33
|
+
":content_expressions", content_expressions
|
34
|
+
).gsub(
|
35
|
+
":model_name", connection.quote(model.name)
|
36
|
+
).gsub(
|
37
|
+
":model_table", model.quoted_table_name
|
38
|
+
).gsub(
|
39
|
+
":documents_table", PgSearch::Document.quoted_table_name
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_support/core_ext/class/attribute"
|
3
|
+
|
4
|
+
module PgSearch
|
5
|
+
module Multisearchable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_one :pg_search_document,
|
10
|
+
:as => :searchable,
|
11
|
+
:class_name => "PgSearch::Document",
|
12
|
+
:dependent => :delete
|
13
|
+
|
14
|
+
after_create :create_pg_search_document,
|
15
|
+
:if => lambda { PgSearch.multisearch_enabled? }
|
16
|
+
|
17
|
+
after_update :update_pg_search_document,
|
18
|
+
:if => lambda { PgSearch.multisearch_enabled? }
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def update_pg_search_document
|
23
|
+
create_pg_search_document unless self.pg_search_document
|
24
|
+
self.pg_search_document.save
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/pg_search/railtie.rb
CHANGED
data/lib/pg_search/scope.rb
CHANGED
@@ -19,13 +19,8 @@ module PgSearch
|
|
19
19
|
@feature_names = @config.features.map { |feature_name, feature_options| feature_name }
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
{
|
24
|
-
:select => "#{quoted_table_name}.*, (#{rank}) AS pg_search_rank",
|
25
|
-
:conditions => conditions,
|
26
|
-
:order => "pg_search_rank DESC, #{primary_key} ASC",
|
27
|
-
:joins => joins
|
28
|
-
}
|
22
|
+
def to_relation
|
23
|
+
@model.select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank").where(conditions).order("pg_search_rank DESC, #{order_within_rank}").joins(joins)
|
29
24
|
end
|
30
25
|
|
31
26
|
private
|
@@ -34,6 +29,10 @@ module PgSearch
|
|
34
29
|
@feature_names.map { |feature_name| "(#{sanitize_sql_array(feature_for(feature_name).conditions)})" }.join(" OR ")
|
35
30
|
end
|
36
31
|
|
32
|
+
def order_within_rank
|
33
|
+
@config.order_within_rank || "#{primary_key} ASC"
|
34
|
+
end
|
35
|
+
|
37
36
|
def primary_key
|
38
37
|
"#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
|
39
38
|
end
|
@@ -44,14 +43,15 @@ module PgSearch
|
|
44
43
|
end.join(' ')
|
45
44
|
end
|
46
45
|
|
46
|
+
FEATURE_CLASSES = {
|
47
|
+
:dmetaphone => Features::DMetaphone,
|
48
|
+
:tsearch => Features::TSearch,
|
49
|
+
:trigram => Features::Trigram
|
50
|
+
}
|
51
|
+
|
47
52
|
def feature_for(feature_name)
|
48
53
|
feature_name = feature_name.to_sym
|
49
|
-
|
50
|
-
feature_class = {
|
51
|
-
:dmetaphone => Features::DMetaphone,
|
52
|
-
:tsearch => Features::TSearch,
|
53
|
-
:trigram => Features::Trigram
|
54
|
-
}[feature_name]
|
54
|
+
feature_class = FEATURE_CLASSES[feature_name]
|
55
55
|
|
56
56
|
raise ArgumentError.new("Unknown feature: #{feature_name}") unless feature_class
|
57
57
|
|
@@ -60,10 +60,6 @@ module PgSearch
|
|
60
60
|
feature_class.new(@config.query, @feature_options[feature_name], @config.columns, @model, normalizer)
|
61
61
|
end
|
62
62
|
|
63
|
-
def tsearch_rank
|
64
|
-
sanitize_sql_array(@feature_names[Features::TSearch].rank)
|
65
|
-
end
|
66
|
-
|
67
63
|
def rank
|
68
64
|
(@config.ranking_sql || ":tsearch").gsub(/:(\w*)/) do
|
69
65
|
sanitize_sql_array(feature_for($1).rank)
|
data/lib/pg_search/tasks.rb
CHANGED
@@ -2,7 +2,44 @@ require 'rake'
|
|
2
2
|
require 'pg_search'
|
3
3
|
|
4
4
|
namespace :pg_search do
|
5
|
+
namespace :multisearch do
|
6
|
+
desc "Rebuild PgSearch multisearch records for MODEL"
|
7
|
+
task rebuild: :environment do
|
8
|
+
raise "must set MODEL=<model name>" unless ENV["MODEL"]
|
9
|
+
model_class = ENV["MODEL"].classify.constantize
|
10
|
+
PgSearch::Multisearch.rebuild(model_class)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
5
14
|
namespace :migration do
|
15
|
+
desc "Generate migration to add table for multisearch"
|
16
|
+
task :multisearch do
|
17
|
+
now = Time.now.utc
|
18
|
+
filename = "#{now.strftime('%Y%m%d%H%M%S')}_create_pg_search_documents.rb"
|
19
|
+
|
20
|
+
File.open(Rails.root + 'db' + 'migrate' + filename, 'wb') do |migration_file|
|
21
|
+
migration_file.puts <<-RUBY
|
22
|
+
class CreatePgSearchDocuments < ActiveRecord::Migration
|
23
|
+
def self.up
|
24
|
+
say_with_time("Creating table for pg_search multisearch") do
|
25
|
+
create_table :pg_search_documents do |t|
|
26
|
+
t.text :content
|
27
|
+
t.belongs_to :searchable, :polymorphic => true
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.down
|
34
|
+
say_with_time("Dropping table for pg_search multisearch") do
|
35
|
+
drop_table :pg_search_documents
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
6
43
|
desc "Generate migration to add support functions for :dmetaphone"
|
7
44
|
task :dmetaphone do
|
8
45
|
now = Time.now.utc
|
data/lib/pg_search/version.rb
CHANGED
data/pg_search.gemspec
CHANGED
@@ -16,4 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency 'activerecord', '>=3'
|
21
|
+
s.add_dependency 'activesupport', '>=3'
|
19
22
|
end
|
data/spec/associations_spec.rb
CHANGED
@@ -2,314 +2,293 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
2
|
|
3
3
|
describe PgSearch do
|
4
4
|
context "joining to another table" do
|
5
|
-
|
6
|
-
context "
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
t.string "title"
|
11
|
-
end
|
5
|
+
context "with Arel support" do
|
6
|
+
context "without an :against" do
|
7
|
+
with_model :AssociatedModel do
|
8
|
+
table do |t|
|
9
|
+
t.string "title"
|
12
10
|
end
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
with_model :ModelWithoutAgainst do
|
14
|
+
table do |t|
|
15
|
+
t.string "title"
|
16
|
+
t.belongs_to :another_model
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
model do
|
20
|
+
include PgSearch
|
21
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
23
22
|
|
24
|
-
|
25
|
-
end
|
23
|
+
pg_search_scope :with_another, :associated_against => {:another_model => :title}
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
it "returns rows that match the query in the columns of the associated model only" do
|
28
|
+
associated = AssociatedModel.create!(:title => 'abcdef')
|
29
|
+
included = [
|
30
|
+
ModelWithoutAgainst.create!(:title => 'abcdef', :another_model => associated),
|
31
|
+
ModelWithoutAgainst.create!(:title => 'ghijkl', :another_model => associated)
|
32
|
+
]
|
33
|
+
excluded = [
|
34
|
+
ModelWithoutAgainst.create!(:title => 'abcdef')
|
35
|
+
]
|
36
|
+
|
37
|
+
results = ModelWithoutAgainst.with_another('abcdef')
|
38
|
+
results.map(&:title).should =~ included.map(&:title)
|
39
|
+
results.should_not include(excluded)
|
40
|
+
end
|
41
|
+
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
context "through a belongs_to association" do
|
44
|
+
with_model :AssociatedModel do
|
45
|
+
table do |t|
|
46
|
+
t.string 'title'
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
50
|
+
with_model :ModelWithBelongsTo do
|
51
|
+
table do |t|
|
52
|
+
t.string 'title'
|
53
|
+
t.belongs_to 'another_model'
|
49
54
|
end
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
t.belongs_to 'another_model'
|
55
|
-
end
|
56
|
+
model do
|
57
|
+
include PgSearch
|
58
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
56
59
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
+
pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
|
61
|
+
end
|
62
|
+
end
|
60
63
|
|
61
|
-
|
62
|
-
|
64
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
65
|
+
associated = AssociatedModel.create!(:title => 'abcdef')
|
66
|
+
included = [
|
67
|
+
ModelWithBelongsTo.create!(:title => 'ghijkl', :another_model => associated),
|
68
|
+
ModelWithBelongsTo.create!(:title => 'abcdef')
|
69
|
+
]
|
70
|
+
excluded = ModelWithBelongsTo.create!(:title => 'mnopqr',
|
71
|
+
:another_model => AssociatedModel.create!(:title => 'stuvwx'))
|
72
|
+
|
73
|
+
results = ModelWithBelongsTo.with_associated('abcdef')
|
74
|
+
results.map(&:title).should =~ included.map(&:title)
|
75
|
+
results.should_not include(excluded)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "through a has_many association" do
|
80
|
+
with_model :AssociatedModelWithHasMany do
|
81
|
+
table do |t|
|
82
|
+
t.string 'title'
|
83
|
+
t.belongs_to 'ModelWithHasMany'
|
63
84
|
end
|
85
|
+
end
|
64
86
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
ModelWithBelongsTo.create!(:title => 'abcdef')
|
70
|
-
]
|
71
|
-
excluded = ModelWithBelongsTo.create!(:title => 'mnopqr',
|
72
|
-
:another_model => AssociatedModel.create!(:title => 'stuvwx'))
|
87
|
+
with_model :ModelWithHasMany do
|
88
|
+
table do |t|
|
89
|
+
t.string 'title'
|
90
|
+
end
|
73
91
|
|
74
|
-
|
75
|
-
|
76
|
-
|
92
|
+
model do
|
93
|
+
include PgSearch
|
94
|
+
has_many :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'ModelWithHasMany_id'
|
95
|
+
|
96
|
+
pg_search_scope :with_associated, :against => [:title], :associated_against => {:other_models => :title}
|
77
97
|
end
|
78
98
|
end
|
79
99
|
|
80
|
-
|
81
|
-
|
100
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
101
|
+
included = [
|
102
|
+
ModelWithHasMany.create!(:title => 'abcdef', :other_models => [
|
103
|
+
AssociatedModelWithHasMany.create!(:title => 'foo'),
|
104
|
+
AssociatedModelWithHasMany.create!(:title => 'bar')
|
105
|
+
]),
|
106
|
+
ModelWithHasMany.create!(:title => 'ghijkl', :other_models => [
|
107
|
+
AssociatedModelWithHasMany.create!(:title => 'foo bar'),
|
108
|
+
AssociatedModelWithHasMany.create!(:title => 'mnopqr')
|
109
|
+
]),
|
110
|
+
ModelWithHasMany.create!(:title => 'foo bar')
|
111
|
+
]
|
112
|
+
excluded = ModelWithHasMany.create!(:title => 'stuvwx', :other_models => [
|
113
|
+
AssociatedModelWithHasMany.create!(:title => 'abcdef')
|
114
|
+
])
|
115
|
+
|
116
|
+
results = ModelWithHasMany.with_associated('foo bar')
|
117
|
+
results.map(&:title).should =~ included.map(&:title)
|
118
|
+
results.should_not include(excluded)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "across multiple associations" do
|
123
|
+
context "on different tables" do
|
124
|
+
with_model :FirstAssociatedModel do
|
82
125
|
table do |t|
|
83
126
|
t.string 'title'
|
84
|
-
t.belongs_to '
|
127
|
+
t.belongs_to 'ModelWithManyAssociations'
|
85
128
|
end
|
129
|
+
model {}
|
86
130
|
end
|
87
131
|
|
88
|
-
with_model :
|
132
|
+
with_model :SecondAssociatedModel do
|
89
133
|
table do |t|
|
90
134
|
t.string 'title'
|
91
135
|
end
|
136
|
+
model {}
|
137
|
+
end
|
138
|
+
|
139
|
+
with_model :ModelWithManyAssociations do
|
140
|
+
table do |t|
|
141
|
+
t.string 'title'
|
142
|
+
t.belongs_to 'model_of_second_type'
|
143
|
+
end
|
92
144
|
|
93
145
|
model do
|
94
146
|
include PgSearch
|
95
|
-
has_many :
|
147
|
+
has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'ModelWithManyAssociations_id'
|
148
|
+
belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
|
96
149
|
|
97
|
-
pg_search_scope :with_associated, :against =>
|
150
|
+
pg_search_scope :with_associated, :against => :title,
|
151
|
+
:associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
|
98
152
|
end
|
99
153
|
end
|
100
154
|
|
101
155
|
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
156
|
+
matching_second = SecondAssociatedModel.create!(:title => "foo bar")
|
157
|
+
unmatching_second = SecondAssociatedModel.create!(:title => "uiop")
|
158
|
+
|
102
159
|
included = [
|
103
|
-
|
104
|
-
|
105
|
-
|
160
|
+
ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
|
161
|
+
FirstAssociatedModel.create!(:title => 'foo'),
|
162
|
+
FirstAssociatedModel.create!(:title => 'bar')
|
106
163
|
]),
|
107
|
-
|
108
|
-
|
109
|
-
|
164
|
+
ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
|
165
|
+
FirstAssociatedModel.create!(:title => 'foo bar'),
|
166
|
+
FirstAssociatedModel.create!(:title => 'mnopqr')
|
110
167
|
]),
|
111
|
-
|
168
|
+
ModelWithManyAssociations.create!(:title => 'foo bar'),
|
169
|
+
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
|
170
|
+
]
|
171
|
+
excluded = [
|
172
|
+
ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
|
173
|
+
FirstAssociatedModel.create!(:title => 'abcdef')
|
174
|
+
]),
|
175
|
+
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
|
112
176
|
]
|
113
|
-
excluded = ModelWithHasMany.create!(:title => 'stuvwx', :other_models => [
|
114
|
-
AssociatedModelWithHasMany.create!(:title => 'abcdef')
|
115
|
-
])
|
116
177
|
|
117
|
-
results =
|
178
|
+
results = ModelWithManyAssociations.with_associated('foo bar')
|
118
179
|
results.map(&:title).should =~ included.map(&:title)
|
119
|
-
results.should_not include(
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
context "across multiple associations" do
|
124
|
-
context "on different tables" do
|
125
|
-
with_model :FirstAssociatedModel do
|
126
|
-
table do |t|
|
127
|
-
t.string 'title'
|
128
|
-
t.belongs_to 'model_with_many_associations'
|
129
|
-
end
|
130
|
-
model {}
|
131
|
-
end
|
132
|
-
|
133
|
-
with_model :SecondAssociatedModel do
|
134
|
-
table do |t|
|
135
|
-
t.string 'title'
|
136
|
-
end
|
137
|
-
model {}
|
138
|
-
end
|
139
|
-
|
140
|
-
with_model :ModelWithManyAssociations do
|
141
|
-
table do |t|
|
142
|
-
t.string 'title'
|
143
|
-
t.belongs_to 'model_of_second_type'
|
144
|
-
end
|
145
|
-
|
146
|
-
model do
|
147
|
-
include PgSearch
|
148
|
-
has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'model_with_many_associations_id'
|
149
|
-
belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
|
150
|
-
|
151
|
-
pg_search_scope :with_associated, :against => :title,
|
152
|
-
:associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
157
|
-
matching_second = SecondAssociatedModel.create!(:title => "foo bar")
|
158
|
-
unmatching_second = SecondAssociatedModel.create!(:title => "uiop")
|
159
|
-
|
160
|
-
included = [
|
161
|
-
ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
|
162
|
-
FirstAssociatedModel.create!(:title => 'foo'),
|
163
|
-
FirstAssociatedModel.create!(:title => 'bar')
|
164
|
-
]),
|
165
|
-
ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
|
166
|
-
FirstAssociatedModel.create!(:title => 'foo bar'),
|
167
|
-
FirstAssociatedModel.create!(:title => 'mnopqr')
|
168
|
-
]),
|
169
|
-
ModelWithManyAssociations.create!(:title => 'foo bar'),
|
170
|
-
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
|
171
|
-
]
|
172
|
-
excluded = [
|
173
|
-
ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
|
174
|
-
FirstAssociatedModel.create!(:title => 'abcdef')
|
175
|
-
]),
|
176
|
-
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
|
177
|
-
]
|
178
|
-
|
179
|
-
results = ModelWithManyAssociations.with_associated('foo bar')
|
180
|
-
results.map(&:title).should =~ included.map(&:title)
|
181
|
-
excluded.each { |object| results.should_not include(object) }
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
context "on the same table" do
|
186
|
-
with_model :DoublyAssociatedModel do
|
187
|
-
table do |t|
|
188
|
-
t.string 'title'
|
189
|
-
t.belongs_to 'model_with_double_association'
|
190
|
-
t.belongs_to 'model_with_double_association_again'
|
191
|
-
end
|
192
|
-
model {}
|
193
|
-
end
|
194
|
-
|
195
|
-
with_model :model_with_double_association do
|
196
|
-
table do |t|
|
197
|
-
t.string 'title'
|
198
|
-
end
|
199
|
-
|
200
|
-
model do
|
201
|
-
include PgSearch
|
202
|
-
has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_id'
|
203
|
-
has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_again_id'
|
204
|
-
|
205
|
-
pg_search_scope :with_associated, :against => :title,
|
206
|
-
:associated_against => {:things => :title, :thingamabobs => :title}
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
211
|
-
included = [
|
212
|
-
ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
|
213
|
-
DoublyAssociatedModel.create!(:title => 'foo'),
|
214
|
-
DoublyAssociatedModel.create!(:title => 'bar')
|
215
|
-
]),
|
216
|
-
ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
|
217
|
-
DoublyAssociatedModel.create!(:title => 'foo bar'),
|
218
|
-
DoublyAssociatedModel.create!(:title => 'mnopqr')
|
219
|
-
]),
|
220
|
-
ModelWithDoubleAssociation.create!(:title => 'foo bar'),
|
221
|
-
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
222
|
-
DoublyAssociatedModel.create!(:title => "foo bar")
|
223
|
-
])
|
224
|
-
]
|
225
|
-
excluded = [
|
226
|
-
ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
|
227
|
-
DoublyAssociatedModel.create!(:title => 'abcdef')
|
228
|
-
]),
|
229
|
-
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
230
|
-
DoublyAssociatedModel.create!(:title => "uiop")
|
231
|
-
])
|
232
|
-
]
|
233
|
-
|
234
|
-
results = ModelWithDoubleAssociation.with_associated('foo bar')
|
235
|
-
results.map(&:title).should =~ included.map(&:title)
|
236
|
-
excluded.each { |object| results.should_not include(object) }
|
237
|
-
end
|
180
|
+
excluded.each { |object| results.should_not include(object) }
|
238
181
|
end
|
239
182
|
end
|
240
183
|
|
241
|
-
context "
|
242
|
-
with_model :
|
184
|
+
context "on the same table" do
|
185
|
+
with_model :DoublyAssociatedModel do
|
243
186
|
table do |t|
|
244
187
|
t.string 'title'
|
245
|
-
t.
|
188
|
+
t.belongs_to 'ModelWithDoubleAssociation'
|
189
|
+
t.belongs_to 'ModelWithDoubleAssociation_again'
|
246
190
|
end
|
191
|
+
model {}
|
247
192
|
end
|
248
193
|
|
249
|
-
with_model :
|
194
|
+
with_model :ModelWithDoubleAssociation do
|
250
195
|
table do |t|
|
251
|
-
t.
|
196
|
+
t.string 'title'
|
252
197
|
end
|
253
198
|
|
254
199
|
model do
|
255
200
|
include PgSearch
|
256
|
-
|
201
|
+
has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'ModelWithDoubleAssociation_id'
|
202
|
+
has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'ModelWithDoubleAssociation_again_id'
|
257
203
|
|
258
|
-
pg_search_scope :with_associated, :
|
204
|
+
pg_search_scope :with_associated, :against => :title,
|
205
|
+
:associated_against => {:things => :title, :thingamabobs => :title}
|
259
206
|
end
|
260
207
|
end
|
261
208
|
|
262
|
-
it "
|
209
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
263
210
|
included = [
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
211
|
+
ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
|
212
|
+
DoublyAssociatedModel.create!(:title => 'foo'),
|
213
|
+
DoublyAssociatedModel.create!(:title => 'bar')
|
214
|
+
]),
|
215
|
+
ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
|
216
|
+
DoublyAssociatedModel.create!(:title => 'foo bar'),
|
217
|
+
DoublyAssociatedModel.create!(:title => 'mnopqr')
|
218
|
+
]),
|
219
|
+
ModelWithDoubleAssociation.create!(:title => 'foo bar'),
|
220
|
+
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
221
|
+
DoublyAssociatedModel.create!(:title => "foo bar")
|
222
|
+
])
|
276
223
|
]
|
277
224
|
excluded = [
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
225
|
+
ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
|
226
|
+
DoublyAssociatedModel.create!(:title => 'abcdef')
|
227
|
+
]),
|
228
|
+
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
229
|
+
DoublyAssociatedModel.create!(:title => "uiop")
|
230
|
+
])
|
284
231
|
]
|
285
232
|
|
286
|
-
results =
|
287
|
-
|
288
|
-
results.to_sql.scan("INNER JOIN").length.should == 1
|
289
|
-
included.each { |object| results.should include(object) }
|
233
|
+
results = ModelWithDoubleAssociation.with_associated('foo bar')
|
234
|
+
results.map(&:title).should =~ included.map(&:title)
|
290
235
|
excluded.each { |object| results.should_not include(object) }
|
291
236
|
end
|
292
|
-
|
293
237
|
end
|
294
238
|
end
|
295
|
-
|
296
|
-
context "
|
297
|
-
with_model :
|
239
|
+
|
240
|
+
context "against multiple attributes on one association" do
|
241
|
+
with_model :AssociatedModel do
|
298
242
|
table do |t|
|
299
243
|
t.string 'title'
|
244
|
+
t.text 'author'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
with_model :ModelWithAssociation do
|
249
|
+
table do |t|
|
250
|
+
t.belongs_to 'another_model'
|
300
251
|
end
|
301
252
|
|
302
253
|
model do
|
303
254
|
include PgSearch
|
304
|
-
|
255
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
256
|
+
|
257
|
+
pg_search_scope :with_associated, :associated_against => {:another_model => [:title, :author]}
|
305
258
|
end
|
306
259
|
end
|
307
260
|
|
308
|
-
it "should
|
309
|
-
|
310
|
-
|
311
|
-
|
261
|
+
it "should only do one join" do
|
262
|
+
included = [
|
263
|
+
ModelWithAssociation.create!(
|
264
|
+
:another_model => AssociatedModel.create!(
|
265
|
+
:title => "foo",
|
266
|
+
:author => "bar"
|
267
|
+
)
|
268
|
+
),
|
269
|
+
ModelWithAssociation.create!(
|
270
|
+
:another_model => AssociatedModel.create!(
|
271
|
+
:title => "foo bar",
|
272
|
+
:author => "baz"
|
273
|
+
)
|
274
|
+
)
|
275
|
+
]
|
276
|
+
excluded = [
|
277
|
+
ModelWithAssociation.create!(
|
278
|
+
:another_model => AssociatedModel.create!(
|
279
|
+
:title => "foo",
|
280
|
+
:author => "baz"
|
281
|
+
)
|
282
|
+
)
|
283
|
+
]
|
284
|
+
|
285
|
+
results = ModelWithAssociation.with_associated('foo bar')
|
286
|
+
|
287
|
+
results.to_sql.scan("INNER JOIN").length.should == 1
|
288
|
+
included.each { |object| results.should include(object) }
|
289
|
+
excluded.each { |object| results.should_not include(object) }
|
312
290
|
end
|
291
|
+
|
313
292
|
end
|
314
293
|
end
|
315
294
|
end
|