pg_search 0.2.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- subselect_name = ["pg_search", table_name, @name, "subselect"].compact.join('_')
30
- "pg_search_#{Digest::SHA2.hexdigest(subselect_name)}"
29
+ Configuration.alias(table_name, @name, "subselect")
31
30
  end
32
31
  end
33
32
  end
@@ -34,8 +34,7 @@ module PgSearch
34
34
  end
35
35
 
36
36
  def alias
37
- name = [association.subselect_alias, @column_name].compact.join('_')
38
- "pg_search_#{Digest::SHA2.hexdigest(name)}"
37
+ Configuration.alias(association.subselect_alias, @column_name)
39
38
  end
40
39
  end
41
40
  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
- if @options[:tsvector_column]
54
- @options[:tsvector_column].to_s
55
- else
56
- @columns.map do |search_column|
57
- tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
58
- search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
59
- end.join(" || ")
60
- end
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
@@ -1,10 +1,6 @@
1
- require 'pg_search'
2
- #require 'rails'
3
-
4
1
  module PgSearch
5
2
  class Railtie < Rails::Railtie
6
3
  rake_tasks do
7
- raise
8
4
  load "pg_search/tasks.rb"
9
5
  end
10
6
  end
@@ -9,7 +9,7 @@ module PgSearch
9
9
  def to_proc
10
10
  lambda { |*args|
11
11
  config = Configuration.new(@options_proc.call(*args), @model)
12
- ScopeOptions.new(@name, @model, config).to_hash
12
+ ScopeOptions.new(@name, @model, config).to_relation
13
13
  }
14
14
  end
15
15
 
@@ -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 to_hash
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)
@@ -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
@@ -1,3 +1,3 @@
1
1
  module PgSearch
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3"
3
3
  end
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
@@ -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
- if defined?(ActiveRecord::Relation)
6
- context "with Arel support" do
7
- context "without an :against" do
8
- with_model :AssociatedModel do
9
- table do |t|
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
- with_model :ModelWithoutAgainst do
15
- table do |t|
16
- t.string "title"
17
- t.belongs_to :another_model
18
- end
13
+ with_model :ModelWithoutAgainst do
14
+ table do |t|
15
+ t.string "title"
16
+ t.belongs_to :another_model
17
+ end
19
18
 
20
- model do
21
- include PgSearch
22
- belongs_to :another_model, :class_name => 'AssociatedModel'
19
+ model do
20
+ include PgSearch
21
+ belongs_to :another_model, :class_name => 'AssociatedModel'
23
22
 
24
- pg_search_scope :with_another, :associated_against => {:another_model => :title}
25
- end
23
+ pg_search_scope :with_another, :associated_against => {:another_model => :title}
26
24
  end
25
+ end
27
26
 
28
- it "returns rows that match the query in the columns of the associated model only" do
29
- associated = AssociatedModel.create!(:title => 'abcdef')
30
- included = [
31
- ModelWithoutAgainst.create!(:title => 'abcdef', :another_model => associated),
32
- ModelWithoutAgainst.create!(:title => 'ghijkl', :another_model => associated)
33
- ]
34
- excluded = [
35
- ModelWithoutAgainst.create!(:title => 'abcdef')
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
- results = ModelWithoutAgainst.with_another('abcdef')
39
- results.map(&:title).should =~ included.map(&:title)
40
- results.should_not include(excluded)
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
- context "through a belongs_to association" do
45
- with_model :AssociatedModel do
46
- table do |t|
47
- t.string 'title'
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
- with_model :ModelWithBelongsTo do
52
- table do |t|
53
- t.string 'title'
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
- model do
58
- include PgSearch
59
- belongs_to :another_model, :class_name => 'AssociatedModel'
60
+ pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
61
+ end
62
+ end
60
63
 
61
- pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
62
- end
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
- it "returns rows that match the query in either its own columns or the columns of the associated model" do
66
- associated = AssociatedModel.create!(:title => 'abcdef')
67
- included = [
68
- ModelWithBelongsTo.create!(:title => 'ghijkl', :another_model => associated),
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
- results = ModelWithBelongsTo.with_associated('abcdef')
75
- results.map(&:title).should =~ included.map(&:title)
76
- results.should_not include(excluded)
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
- context "through a has_many association" do
81
- with_model :AssociatedModelWithHasMany do
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 'ModelWithHasMany'
127
+ t.belongs_to 'ModelWithManyAssociations'
85
128
  end
129
+ model {}
86
130
  end
87
131
 
88
- with_model :ModelWithHasMany do
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 :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'ModelWithHasMany_id'
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 => [:title], :associated_against => {:other_models => :title}
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
- ModelWithHasMany.create!(:title => 'abcdef', :other_models => [
104
- AssociatedModelWithHasMany.create!(:title => 'foo'),
105
- AssociatedModelWithHasMany.create!(:title => 'bar')
160
+ ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
161
+ FirstAssociatedModel.create!(:title => 'foo'),
162
+ FirstAssociatedModel.create!(:title => 'bar')
106
163
  ]),
107
- ModelWithHasMany.create!(:title => 'ghijkl', :other_models => [
108
- AssociatedModelWithHasMany.create!(:title => 'foo bar'),
109
- AssociatedModelWithHasMany.create!(:title => 'mnopqr')
164
+ ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
165
+ FirstAssociatedModel.create!(:title => 'foo bar'),
166
+ FirstAssociatedModel.create!(:title => 'mnopqr')
110
167
  ]),
111
- ModelWithHasMany.create!(:title => 'foo bar')
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 = ModelWithHasMany.with_associated('foo bar')
178
+ results = ModelWithManyAssociations.with_associated('foo bar')
118
179
  results.map(&:title).should =~ included.map(&:title)
119
- results.should_not include(excluded)
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 "against multiple attributes on one association" do
242
- with_model :AssociatedModel do
184
+ context "on the same table" do
185
+ with_model :DoublyAssociatedModel do
243
186
  table do |t|
244
187
  t.string 'title'
245
- t.text 'author'
188
+ t.belongs_to 'ModelWithDoubleAssociation'
189
+ t.belongs_to 'ModelWithDoubleAssociation_again'
246
190
  end
191
+ model {}
247
192
  end
248
193
 
249
- with_model :ModelWithAssociation do
194
+ with_model :ModelWithDoubleAssociation do
250
195
  table do |t|
251
- t.belongs_to 'another_model'
196
+ t.string 'title'
252
197
  end
253
198
 
254
199
  model do
255
200
  include PgSearch
256
- belongs_to :another_model, :class_name => 'AssociatedModel'
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, :associated_against => {:another_model => [:title, :author]}
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 "should only do one join" do
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
- ModelWithAssociation.create!(
265
- :another_model => AssociatedModel.create!(
266
- :title => "foo",
267
- :author => "bar"
268
- )
269
- ),
270
- ModelWithAssociation.create!(
271
- :another_model => AssociatedModel.create!(
272
- :title => "foo bar",
273
- :author => "baz"
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
- ModelWithAssociation.create!(
279
- :another_model => AssociatedModel.create!(
280
- :title => "foo",
281
- :author => "baz"
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 = ModelWithAssociation.with_associated('foo bar')
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
- else
296
- context "without Arel support" do
297
- with_model :Model do
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
- pg_search_scope :with_joins, :against => :title, :joins => :another_model
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 raise an error" do
309
- lambda {
310
- Model.with_joins('foo')
311
- }.should raise_error(ArgumentError, /joins/)
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