pg_search 0.2.2 → 0.3

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.
@@ -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