pg_search 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -12,3 +12,5 @@ rvm:
12
12
 
13
13
  before_script:
14
14
  - "psql -c 'create database pg_search_test;' -U postgres >/dev/null"
15
+
16
+ script: "bundle exec rspec spec"
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,9 @@
1
1
  = PgSearch
2
2
 
3
+ == 0.5.2
4
+
5
+ * Don't save twice if pg_search_document is missing on update
6
+
3
7
  == 0.5.1
4
8
 
5
9
  * Add ability to override multisearch rebuild SQL
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-11 Case Commons, LLC
1
+ Copyright (c) 2010-12 Case Commons, LLC
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/lib/pg_search.rb CHANGED
@@ -3,6 +3,16 @@ require "active_support/concern"
3
3
  require "active_support/core_ext/module/attribute_accessors"
4
4
 
5
5
  module PgSearch
6
+ autoload :Configuration, "pg_search/configuration"
7
+ autoload :Document, "pg_search/document"
8
+ autoload :Features, "pg_search/features"
9
+ autoload :Multisearch, "pg_search/multisearch"
10
+ autoload :Multisearchable, "pg_search/multisearchable"
11
+ autoload :Normalizer, "pg_search/normalizer"
12
+ autoload :Scope, "pg_search/scope"
13
+ autoload :ScopeOptions, "pg_search/scope_options"
14
+ autoload :Version, "pg_search/version"
15
+
6
16
  extend ActiveSupport::Concern
7
17
 
8
18
  mattr_accessor :multisearch_options
@@ -10,10 +20,15 @@ module PgSearch
10
20
 
11
21
  module ClassMethods
12
22
  def pg_search_scope(name, options)
13
- self.scope(
14
- name,
15
- PgSearch::Scope.new(name, self, options).to_proc
16
- )
23
+ scope = PgSearch::Scope.new(name, self, options)
24
+
25
+ method_proc = scope.method(:build_relation)
26
+
27
+ if respond_to?(:define_singleton_method)
28
+ define_singleton_method name, &method_proc
29
+ else
30
+ (class << self; self; end).send :define_method, name, &method_proc
31
+ end
17
32
  end
18
33
 
19
34
  def multisearchable(options = {})
@@ -40,21 +55,15 @@ module PgSearch
40
55
  end
41
56
 
42
57
  def multisearch_enabled?
43
- Thread.current.key?("PgSearch.enable_multisearch") ? Thread.current["PgSearch.enable_multisearch"] : true
58
+ if Thread.current.key?("PgSearch.enable_multisearch")
59
+ Thread.current["PgSearch.enable_multisearch"]
60
+ else
61
+ true
62
+ end
44
63
  end
45
64
  end
46
65
 
47
66
  class NotSupportedForPostgresqlVersion < StandardError; end
48
67
  end
49
68
 
50
- require "pg_search/configuration"
51
- require "pg_search/document"
52
- require "pg_search/features"
53
- require "pg_search/multisearch"
54
- require "pg_search/multisearchable"
55
- require "pg_search/normalizer"
56
- require "pg_search/scope"
57
- require "pg_search/scope_options"
58
- require "pg_search/version"
59
-
60
69
  require "pg_search/railtie" if defined?(Rails)
@@ -1,13 +1,16 @@
1
- require "pg_search/configuration/association"
2
- require "pg_search/configuration/column"
3
-
4
1
  module PgSearch
5
2
  class Configuration
3
+ autoload :Association, "pg_search/configuration/association"
4
+ autoload :Column, "pg_search/configuration/column"
5
+ autoload :ForeignColumn, "pg_search/configuration/foreign_column"
6
+
7
+ attr_reader :model
8
+
6
9
  def initialize(options, model)
7
- options = options.reverse_merge(default_options)
8
- assert_valid_options(options)
9
- @options = options
10
+ @options = default_options.merge(options)
10
11
  @model = model
12
+
13
+ assert_valid_options(@options)
11
14
  end
12
15
 
13
16
  class << self
@@ -32,8 +35,7 @@ module PgSearch
32
35
  def associations
33
36
  return [] unless @options[:associated_against]
34
37
  @options[:associated_against].map do |association, column_names|
35
- association = Association.new(@model, association, column_names)
36
- association
38
+ Association.new(@model, association, column_names)
37
39
  end.flatten
38
40
  end
39
41
 
@@ -75,20 +77,26 @@ module PgSearch
75
77
  {:using => :tsearch}
76
78
  end
77
79
 
78
- def assert_valid_options(options)
79
- valid_keys = [:against, :ranked_by, :ignoring, :using, :query, :associated_against, :order_within_rank]
80
- valid_values = {
81
- :ignoring => [:accents]
82
- }
80
+ VALID_KEYS = %w[
81
+ against ranked_by ignoring using query associated_against order_within_rank
82
+ ].map(&:to_sym)
83
83
 
84
+ VALID_VALUES = {
85
+ :ignoring => [:accents]
86
+ }
87
+
88
+ def assert_valid_options(options)
84
89
  unless options[:against] || options[:associated_against]
85
90
  raise ArgumentError, "the search scope #{@name} must have :against#{" or :associated_against" if defined?(ActiveRecord::Relation)} in its options"
86
91
  end
87
- raise ArgumentError, ":associated_against requires ActiveRecord 3 or later" if options[:associated_against] && !defined?(ActiveRecord::Relation)
88
92
 
89
- options.assert_valid_keys(valid_keys)
93
+ if options[:associated_against] && !defined?(ActiveRecord::Relation)
94
+ raise ArgumentError, ":associated_against requires Active Record 3 or later"
95
+ end
96
+
97
+ options.assert_valid_keys(VALID_KEYS)
90
98
 
91
- valid_values.each do |key, values_for_key|
99
+ VALID_VALUES.each do |key, values_for_key|
92
100
  Array.wrap(options[key]).each do |value|
93
101
  unless values_for_key.include?(value)
94
102
  raise ArgumentError, ":#{key} cannot accept #{value}"
@@ -9,7 +9,7 @@ module PgSearch
9
9
  @model = model
10
10
  @name = name
11
11
  @columns = Array(column_names).map do |column_name, weight|
12
- Column.new(column_name, weight, @model, self)
12
+ ForeignColumn.new(column_name, weight, @model, self)
13
13
  end
14
14
  end
15
15
 
@@ -18,20 +18,30 @@ module PgSearch
18
18
  end
19
19
 
20
20
  def join(primary_key)
21
- selects = columns.map do |column|
22
- case @model.connection.send(:postgresql_version)
21
+ "LEFT OUTER JOIN (#{relation(primary_key).to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
22
+ end
23
+
24
+ def subselect_alias
25
+ Configuration.alias(table_name, @name, "subselect")
26
+ end
27
+
28
+ private
29
+
30
+ def selects
31
+ postgresql_version = @model.connection.send(:postgresql_version)
32
+
33
+ columns.map do |column|
34
+ case postgresql_version
23
35
  when 0..90000
24
- "array_to_string(array_agg(#{column.full_name}), ' ') AS #{column.alias}"
36
+ "array_to_string(array_agg(#{column.full_name}::text), ' ') AS #{column.alias}"
25
37
  else
26
38
  "string_agg(#{column.full_name}::text, ' ') AS #{column.alias}"
27
39
  end
28
40
  end.join(", ")
29
- relation = @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
30
- "LEFT OUTER JOIN (#{relation.to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
31
41
  end
32
42
 
33
- def subselect_alias
34
- Configuration.alias(table_name, @name, "subselect")
43
+ def relation(primary_key)
44
+ @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
35
45
  end
36
46
  end
37
47
  end
@@ -3,34 +3,35 @@ require 'digest'
3
3
  module PgSearch
4
4
  class Configuration
5
5
  class Column
6
- attr_reader :weight, :association
6
+ attr_reader :weight
7
7
 
8
- def initialize(column_name, weight, model, association = nil)
8
+ def initialize(column_name, weight, model)
9
9
  @column_name = column_name.to_s
10
10
  @weight = weight
11
11
  @model = model
12
- @association = association
13
- end
14
-
15
- def table
16
- foreign? ? @association.table_name : @model.table_name
12
+ @connection = model.connection
17
13
  end
18
14
 
19
15
  def full_name
20
- "#{@model.connection.quote_table_name(table)}.#{@model.connection.quote_column_name(@column_name)}"
16
+ "#{table_name}.#{column_name}"
21
17
  end
22
18
 
23
19
  def to_sql
24
- name = if foreign?
25
- "#{@association.subselect_alias}.#{self.alias}"
26
- else
27
- full_name
28
- end
29
- "coalesce(#{name}::text, '')"
20
+ "coalesce(#{expression}::text, '')"
21
+ end
22
+
23
+ private
24
+
25
+ def table_name
26
+ @connection.quote_table_name(@model.table_name)
27
+ end
28
+
29
+ def column_name
30
+ @connection.quote_column_name(@column_name)
30
31
  end
31
32
 
32
- def foreign?
33
- @association.present?
33
+ def expression
34
+ full_name
34
35
  end
35
36
 
36
37
  def alias
@@ -0,0 +1,28 @@
1
+ require 'digest'
2
+
3
+ module PgSearch
4
+ class Configuration
5
+ class ForeignColumn < Column
6
+ attr_reader :weight
7
+
8
+ def initialize(column_name, weight, model, association)
9
+ super(column_name, weight, model)
10
+ @association = association
11
+ end
12
+
13
+ def alias
14
+ Configuration.alias(@association.subselect_alias, @column_name)
15
+ end
16
+
17
+ private
18
+
19
+ def expression
20
+ "#{@association.subselect_alias}.#{self.alias}"
21
+ end
22
+
23
+ def table_name
24
+ @connection.quote_table_name(@association.table_name)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,4 @@
1
1
  require "logger"
2
- require "pg_search/scope"
3
2
 
4
3
  module PgSearch
5
4
  class Document < ActiveRecord::Base
@@ -19,9 +18,10 @@ module PgSearch
19
18
  options = if PgSearch.multisearch_options.respond_to?(:call)
20
19
  PgSearch.multisearch_options.call(*args)
21
20
  else
22
- PgSearch.multisearch_options.reverse_merge(:query => args.first)
21
+ {:query => args.first}.merge(PgSearch.multisearch_options)
23
22
  end
24
- options.reverse_merge(:against => :content)
23
+
24
+ {:against => :content}.merge(options)
25
25
  }
26
26
 
27
27
  private
@@ -1,7 +1,9 @@
1
1
  module PgSearch
2
2
  module Features
3
+ autoload :Feature, "pg_search/features/feature"
4
+
5
+ autoload :DMetaphone, "pg_search/features/dmetaphone"
6
+ autoload :Trigram, "pg_search/features/trigram"
7
+ autoload :TSearch, "pg_search/features/tsearch"
3
8
  end
4
9
  end
5
- require 'pg_search/features/dmetaphone'
6
- require 'pg_search/features/trigram'
7
- require 'pg_search/features/tsearch'
@@ -1,15 +1,18 @@
1
- require "active_support/core_ext/module/delegation"
2
-
3
1
  module PgSearch
4
2
  module Features
5
3
  class DMetaphone
6
- delegate :conditions, :rank, :to => :'@tsearch'
7
-
8
- # config is temporary as we refactor
9
- def initialize(query, options, config, model, normalizer)
4
+ def initialize(query, options, columns, model, normalizer)
10
5
  dmetaphone_normalizer = Normalizer.new(normalizer)
11
6
  options = (options || {}).merge(:dictionary => 'simple')
12
- @tsearch = TSearch.new(query, options, config, model, dmetaphone_normalizer)
7
+ @tsearch = TSearch.new(query, options, columns, model, dmetaphone_normalizer)
8
+ end
9
+
10
+ def conditions
11
+ @tsearch.conditions
12
+ end
13
+
14
+ def rank
15
+ @tsearch.rank
13
16
  end
14
17
 
15
18
  # Decorates a normalizer with dmetaphone processing.
@@ -0,0 +1,28 @@
1
+ module PgSearch
2
+ module Features
3
+ class Feature
4
+ def initialize(query, options, columns, model, normalizer)
5
+ @query = query
6
+ @options = options || {}
7
+ @columns = columns
8
+ @model = model
9
+ @normalizer = normalizer
10
+ end
11
+
12
+ private
13
+
14
+ def document
15
+ if @columns.length == 1
16
+ @columns.first.to_sql
17
+ else
18
+ expressions = @columns.map { |column| column.to_sql }.join(", ")
19
+ "array_to_string(ARRAY[#{expressions}], ' ')"
20
+ end
21
+ end
22
+
23
+ def normalize(expression)
24
+ @normalizer.add_normalization(expression)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,28 +1,18 @@
1
- require "active_support/core_ext/module/delegation"
2
-
3
1
  module PgSearch
4
2
  module Features
5
- class Trigram
6
- def initialize(query, options, columns, model, normalizer)
7
- @query = query
8
- @options = options
9
- @columns = columns
10
- @model = model
11
- @normalizer = normalizer
12
- end
13
-
3
+ class Trigram < Feature
14
4
  def conditions
15
- ["(#{@normalizer.add_normalization(document)}) % #{@normalizer.add_normalization(":query")}", {:query => @query}]
5
+ [
6
+ "(#{normalize(document)}) % #{normalize(":query")}",
7
+ {:query => @query}
8
+ ]
16
9
  end
17
10
 
18
11
  def rank
19
- ["similarity((#{@normalizer.add_normalization(document)}), #{@normalizer.add_normalization(":query")})", {:query => @query}]
20
- end
21
-
22
- private
23
-
24
- def document
25
- @columns.map { |column| column.to_sql }.join(" || ' ' || ")
12
+ [
13
+ "similarity((#{normalize(document)}), #{normalize(":query")})",
14
+ {:query => @query}
15
+ ]
26
16
  end
27
17
  end
28
18
  end
@@ -2,15 +2,11 @@ require "active_support/core_ext/module/delegation"
2
2
 
3
3
  module PgSearch
4
4
  module Features
5
- class TSearch
5
+ class TSearch < Feature
6
6
  delegate :connection, :quoted_table_name, :to => :'@model'
7
7
 
8
8
  def initialize(query, options, columns, model, normalizer)
9
- @query = query
10
- @options = options || {}
11
- @model = model
12
- @columns = columns
13
- @normalizer = normalizer
9
+ super
14
10
 
15
11
  if @options[:prefix] && @model.connection.send(:postgresql_version) < 80400
16
12
  raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.gsub /^\s*/, '')
@@ -33,24 +29,20 @@ module PgSearch
33
29
  {:query => @query.to_s, :dictionary => dictionary.to_s}
34
30
  end
35
31
 
36
- def document
37
- @columns.map { |column| column.to_sql }.join(" || ' ' || ")
38
- end
39
-
40
32
  DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/
41
33
 
42
34
  def tsquery_for_term(term)
43
35
  sanitized_term = term.gsub(DISALLOWED_TSQUERY_CHARACTERS, " ")
44
36
 
45
- term_sql = @normalizer.add_normalization(connection.quote(sanitized_term))
37
+ term_sql = normalize(connection.quote(sanitized_term))
46
38
 
47
39
  # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
48
40
  # If :prefix is true, then the term will also have :* appended to the end.
49
41
  tsquery_sql = [
50
- connection.quote("' "),
51
- term_sql,
52
- connection.quote(" '"),
53
- (connection.quote(':*') if @options[:prefix])
42
+ connection.quote("' "),
43
+ term_sql,
44
+ connection.quote(" '"),
45
+ (connection.quote(':*') if @options[:prefix])
54
46
  ].compact.join(" || ")
55
47
 
56
48
  "to_tsquery(:dictionary, #{tsquery_sql})"
@@ -58,7 +50,9 @@ module PgSearch
58
50
 
59
51
  def tsquery
60
52
  return "''" if @query.blank?
61
- @query.split(" ").compact.map { |term| tsquery_for_term(term) }.join(@options[:any_word] ? ' || ' : ' && ')
53
+ query_terms = @query.split(" ").compact
54
+ tsquery_terms = query_terms.map { |term| tsquery_for_term(term) }
55
+ tsquery_terms.join(@options[:any_word] ? ' || ' : ' && ')
62
56
  end
63
57
 
64
58
  def tsdocument
@@ -67,8 +61,12 @@ module PgSearch
67
61
  "#{quoted_table_name}.#{column_name}"
68
62
  else
69
63
  @columns.map do |search_column|
70
- tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
71
- search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
64
+ tsvector = "to_tsvector(:dictionary, #{normalize(search_column.to_sql)})"
65
+ if search_column.weight.nil?
66
+ tsvector
67
+ else
68
+ "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
69
+ end
72
70
  end.join(" || ")
73
71
  end
74
72
  end
@@ -1,56 +1,14 @@
1
1
  module PgSearch
2
2
  module Multisearch
3
- REBUILD_SQL_TEMPLATE = <<-SQL
4
- INSERT INTO :documents_table (searchable_type, searchable_id, content, created_at, updated_at)
5
- SELECT :model_name AS searchable_type,
6
- :model_table.id AS searchable_id,
7
- (
8
- :content_expressions
9
- ) AS content,
10
- :current_time AS created_at,
11
- :current_time AS updated_at
12
- FROM :model_table
13
- SQL
3
+ autoload :Rebuilder, "pg_search/multisearch/rebuilder"
14
4
 
15
5
  class << self
16
6
  def rebuild(model, clean_up=true)
17
7
  model.transaction do
18
8
  PgSearch::Document.where(:searchable_type => model.name).delete_all if clean_up
19
- if model.respond_to?(:rebuild_pg_search_documents)
20
- model.rebuild_pg_search_documents
21
- else
22
- model.connection.execute(rebuild_sql(model))
23
- end
9
+ Rebuilder.new(model).rebuild
24
10
  end
25
11
  end
26
-
27
- def rebuild_sql(model)
28
- connection = model.connection
29
-
30
- unless model.respond_to?(:pg_search_multisearchable_options)
31
- raise ModelNotMultisearchable.new(model)
32
- end
33
-
34
- columns = Array.wrap(
35
- model.pg_search_multisearchable_options[:against]
36
- )
37
-
38
- content_expressions = columns.map { |column|
39
- %Q{coalesce(:model_table.#{column}::text, '')}
40
- }.join(" || ' ' || ")
41
-
42
- REBUILD_SQL_TEMPLATE.gsub(
43
- ":content_expressions", content_expressions
44
- ).gsub(
45
- ":model_name", connection.quote(model.name)
46
- ).gsub(
47
- ":model_table", model.quoted_table_name
48
- ).gsub(
49
- ":documents_table", PgSearch::Document.quoted_table_name
50
- ).gsub(
51
- ":current_time", connection.quote(connection.quoted_date(Time.now))
52
- )
53
- end
54
12
  end
55
13
 
56
14
  class ModelNotMultisearchable < StandardError
@@ -0,0 +1,75 @@
1
+ module PgSearch
2
+ module Multisearch
3
+ class Rebuilder
4
+ REBUILD_SQL_TEMPLATE = <<-SQL
5
+ INSERT INTO :documents_table (searchable_type, searchable_id, content, created_at, updated_at)
6
+ SELECT :model_name AS searchable_type,
7
+ :model_table.id AS searchable_id,
8
+ (
9
+ :content_expressions
10
+ ) AS content,
11
+ :current_time AS created_at,
12
+ :current_time AS updated_at
13
+ FROM :model_table
14
+ SQL
15
+
16
+ def initialize(model)
17
+ unless model.respond_to?(:pg_search_multisearchable_options)
18
+ raise ModelNotMultisearchable.new(model)
19
+ end
20
+
21
+ @model = model
22
+ end
23
+
24
+ def rebuild
25
+ if @model.respond_to?(:rebuild_pg_search_documents)
26
+ @model.rebuild_pg_search_documents
27
+ else
28
+ @model.connection.execute(rebuild_sql)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def connection
35
+ @model.connection
36
+ end
37
+
38
+ def rebuild_sql
39
+ replacements.inject(REBUILD_SQL_TEMPLATE) do |sql, key|
40
+ sql.gsub ":#{key}", send(key)
41
+ end
42
+ end
43
+
44
+ def replacements
45
+ %w[content_expressions model_name model_table documents_table current_time]
46
+ end
47
+
48
+ def content_expressions
49
+ columns.map { |column|
50
+ %Q{coalesce(:model_table.#{column}::text, '')}
51
+ }.join(" || ' ' || ")
52
+ end
53
+
54
+ def columns
55
+ Array.wrap(@model.pg_search_multisearchable_options[:against])
56
+ end
57
+
58
+ def model_name
59
+ connection.quote(@model.name)
60
+ end
61
+
62
+ def model_table
63
+ @model.quoted_table_name
64
+ end
65
+
66
+ def documents_table
67
+ PgSearch::Document.quoted_table_name
68
+ end
69
+
70
+ def current_time
71
+ connection.quote(connection.quoted_date(Time.now))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -19,8 +19,11 @@ module PgSearch
19
19
  end
20
20
 
21
21
  def update_pg_search_document
22
- create_pg_search_document unless self.pg_search_document
23
- self.pg_search_document.save
22
+ if self.pg_search_document
23
+ self.pg_search_document.save
24
+ else
25
+ create_pg_search_document
26
+ end
24
27
  end
25
28
  end
26
29
  end
@@ -4,19 +4,19 @@ module PgSearch
4
4
  @config = config
5
5
  end
6
6
 
7
- def add_normalization(original_sql)
8
- normalized_sql = original_sql
7
+ def add_normalization(sql_expression)
9
8
  if @config.ignore.include?(:accents)
10
9
  if @config.postgresql_version < 90000
11
10
  raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.gsub /^\s*/, '')
12
- Sorry, {:ignoring => :accents} only works in PostgreSQL 9.0 and above.
13
- #{@config.inspect}
11
+ Sorry, {:ignoring => :accents} only works in PostgreSQL 9.0 and above.
12
+ #{@config.inspect}
14
13
  MESSAGE
15
14
  else
16
- normalized_sql = "unaccent(#{normalized_sql})"
15
+ "unaccent(#{sql_expression})"
17
16
  end
17
+ else
18
+ sql_expression
18
19
  end
19
- normalized_sql
20
20
  end
21
21
  end
22
22
  end
@@ -6,26 +6,22 @@ module PgSearch
6
6
  @options_proc = build_options_proc(scope_options_or_proc)
7
7
  end
8
8
 
9
- def to_proc
10
- lambda { |*args|
11
- config = Configuration.new(@options_proc.call(*args), @model)
12
- ScopeOptions.new(@name, @model, config).to_relation
13
- }
9
+ def build_relation(*args)
10
+ config = Configuration.new(@options_proc.call(*args), @model)
11
+ scope_options = ScopeOptions.new(@name, config)
12
+ scope_options.apply(@model)
14
13
  end
15
14
 
16
15
  private
17
16
 
18
- def build_options_proc(scope_options_or_proc)
19
- case scope_options_or_proc
20
- when Proc
21
- scope_options_or_proc
22
- when Hash
23
- lambda { |query|
24
- scope_options_or_proc.reverse_merge(:query => query)
25
- }
26
- else
27
- raise ArgumentError, "A PgSearch scope expects a Proc or Hash for its options"
17
+ def build_options_proc(scope_options)
18
+ return scope_options if scope_options.respond_to?(:call)
19
+
20
+ unless scope_options.respond_to?(:merge)
21
+ raise ArgumentError, "pg_search_scope expects a Hash or Proc"
28
22
  end
23
+
24
+ lambda { |query| {:query => query}.merge(scope_options) }
29
25
  end
30
26
  end
31
27
  end
@@ -2,31 +2,25 @@ require "active_support/core_ext/module/delegation"
2
2
 
3
3
  module PgSearch
4
4
  class ScopeOptions
5
- attr_reader :model
5
+ delegate :connection, :quoted_table_name, :sanitize_sql_array, :to => :@model
6
6
 
7
- delegate :connection, :quoted_table_name, :sanitize_sql_array, :to => :model
8
-
9
- def initialize(name, model, config)
7
+ def initialize(name, config)
10
8
  @name = name
11
- @model = model
12
9
  @config = config
13
-
14
- @feature_options = @config.features.inject({}) do |features_hash, (feature_name, feature_options)|
15
- features_hash.merge(
16
- feature_name => feature_options
17
- )
18
- end
19
- @feature_names = @config.features.map { |feature_name, feature_options| feature_name }
10
+ @model = config.model
11
+ @feature_options = Hash[config.features]
20
12
  end
21
13
 
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)
14
+ def apply(scope)
15
+ scope.select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank").where(conditions).order("pg_search_rank DESC, #{order_within_rank}").joins(joins)
24
16
  end
25
17
 
26
18
  private
27
19
 
28
20
  def conditions
29
- @feature_names.map { |feature_name| "(#{sanitize_sql_array(feature_for(feature_name).conditions)})" }.join(" OR ")
21
+ @config.features.map do |feature_name, feature_options|
22
+ "(#{sanitize_sql_array(feature_for(feature_name).conditions)})"
23
+ end.join(" OR ")
30
24
  end
31
25
 
32
26
  def order_within_rank
@@ -34,7 +28,7 @@ module PgSearch
34
28
  end
35
29
 
36
30
  def primary_key
37
- "#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
31
+ "#{quoted_table_name}.#{connection.quote_column_name(@model.primary_key)}"
38
32
  end
39
33
 
40
34
  def joins
@@ -57,7 +51,13 @@ module PgSearch
57
51
 
58
52
  normalizer = Normalizer.new(@config)
59
53
 
60
- feature_class.new(@config.query, @feature_options[feature_name], @config.columns, @model, normalizer)
54
+ feature_class.new(
55
+ @config.query,
56
+ @feature_options[feature_name],
57
+ @config.columns,
58
+ @config.model,
59
+ normalizer
60
+ )
61
61
  end
62
62
 
63
63
  def rank
@@ -1,3 +1,3 @@
1
1
  module PgSearch
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.2"
3
3
  end
@@ -105,22 +105,21 @@ describe PgSearch::Multisearch do
105
105
  PgSearch::Document.last(2).map(&:searchable).map(&:title).should =~ new_models.map(&:title)
106
106
  end
107
107
  end
108
- end
109
-
110
- describe ".rebuild_sql" do
111
- let(:now) { Time.now }
112
108
 
113
- before do
114
- Time.stub(:now => now)
115
- end
109
+ describe "the generated SQL" do
110
+ let(:now) { Time.now }
116
111
 
117
- context "with one attribute" do
118
112
  before do
119
- model.multisearchable :against => [:title]
113
+ Time.stub(:now => now)
120
114
  end
121
115
 
122
- it "should generate the proper SQL code" do
123
- expected_sql = <<-SQL
116
+ context "with one attribute" do
117
+ before do
118
+ model.multisearchable :against => [:title]
119
+ end
120
+
121
+ it "should generate the proper SQL code" do
122
+ expected_sql = <<-SQL
124
123
  INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
125
124
  SELECT #{connection.quote(model.name)} AS searchable_type,
126
125
  #{model.quoted_table_name}.id AS searchable_id,
@@ -132,17 +131,22 @@ INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable
132
131
  FROM #{model.quoted_table_name}
133
132
  SQL
134
133
 
135
- PgSearch::Multisearch.rebuild_sql(model).should == expected_sql
136
- end
137
- end
134
+ statements = []
135
+ connection.stub(:execute) { |sql| statements << sql }
138
136
 
139
- context "with multiple attributes" do
140
- before do
141
- model.multisearchable :against => [:title, :content]
137
+ PgSearch::Multisearch.rebuild(model)
138
+
139
+ statements.should include(expected_sql)
140
+ end
142
141
  end
143
142
 
144
- it "should generate the proper SQL code" do
145
- expected_sql = <<-SQL
143
+ context "with multiple attributes" do
144
+ before do
145
+ model.multisearchable :against => [:title, :content]
146
+ end
147
+
148
+ it "should generate the proper SQL code" do
149
+ expected_sql = <<-SQL
146
150
  INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
147
151
  SELECT #{connection.quote(model.name)} AS searchable_type,
148
152
  #{model.quoted_table_name}.id AS searchable_id,
@@ -152,9 +156,15 @@ INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable
152
156
  #{connection.quote(connection.quoted_date(now))} AS created_at,
153
157
  #{connection.quote(connection.quoted_date(now))} AS updated_at
154
158
  FROM #{model.quoted_table_name}
155
- SQL
159
+ SQL
160
+
161
+ statements = []
162
+ connection.stub(:execute) { |sql| statements << sql }
156
163
 
157
- PgSearch::Multisearch.rebuild_sql(model).should == expected_sql
164
+ PgSearch::Multisearch.rebuild(model)
165
+
166
+ statements.should include(expected_sql)
167
+ end
158
168
  end
159
169
  end
160
170
  end
data/spec/spec_helper.rb CHANGED
@@ -28,11 +28,13 @@ begin
28
28
  postgresql_version = connection.send(:postgresql_version)
29
29
  connection.execute("SELECT 1")
30
30
  rescue error_class => e
31
- puts "-" * 80
32
- puts "Unable to connect to database. Please run:"
33
- puts
34
- puts " createdb pg_search_test"
35
- puts "-" * 80
31
+ at_exit do
32
+ puts "-" * 80
33
+ puts "Unable to connect to database. Please run:"
34
+ puts
35
+ puts " createdb pg_search_test"
36
+ puts "-" * 80
37
+ end
36
38
  raise e
37
39
  end
38
40
 
@@ -56,9 +58,11 @@ rescue => e
56
58
  puts $!.message
57
59
  end
58
60
  rescue => e2
59
- puts "-" * 80
60
- puts "Please install the #{name} contrib module"
61
- puts "-" * 80
61
+ at_exit do
62
+ puts "-" * 80
63
+ puts "Please install the #{name} contrib module"
64
+ puts "-" * 80
65
+ end
62
66
  raise e2
63
67
  end
64
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-01 00:00:00.000000000 Z
12
+ date: 2012-09-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -65,9 +65,11 @@ files:
65
65
  - lib/pg_search/configuration.rb
66
66
  - lib/pg_search/configuration/association.rb
67
67
  - lib/pg_search/configuration/column.rb
68
+ - lib/pg_search/configuration/foreign_column.rb
68
69
  - lib/pg_search/document.rb
69
70
  - lib/pg_search/features.rb
70
71
  - lib/pg_search/features/dmetaphone.rb
72
+ - lib/pg_search/features/feature.rb
71
73
  - lib/pg_search/features/trigram.rb
72
74
  - lib/pg_search/features/tsearch.rb
73
75
  - lib/pg_search/migration/associated_against_generator.rb
@@ -77,6 +79,7 @@ files:
77
79
  - lib/pg_search/migration/templates/add_pg_search_dmetaphone_support_functions.rb.erb
78
80
  - lib/pg_search/migration/templates/create_pg_search_documents.rb
79
81
  - lib/pg_search/multisearch.rb
82
+ - lib/pg_search/multisearch/rebuilder.rb
80
83
  - lib/pg_search/multisearchable.rb
81
84
  - lib/pg_search/normalizer.rb
82
85
  - lib/pg_search/railtie.rb
@@ -111,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
114
  version: '0'
112
115
  segments:
113
116
  - 0
114
- hash: -1351986722427483921
117
+ hash: -1173780423204059667
115
118
  required_rubygems_version: !ruby/object:Gem::Requirement
116
119
  none: false
117
120
  requirements:
@@ -120,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
123
  version: '0'
121
124
  segments:
122
125
  - 0
123
- hash: -1351986722427483921
126
+ hash: -1173780423204059667
124
127
  requirements: []
125
128
  rubyforge_project:
126
129
  rubygems_version: 1.8.24