is_msfte_searchable 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +2 -0
- data/LICENSE +18 -0
- data/README.md +7 -0
- data/Rakefile +13 -0
- data/lib/is_msfte_searchable.rb +8 -0
- data/lib/is_msfte_searchable/active_record_extension.rb +21 -0
- data/lib/is_msfte_searchable/active_record_mixin.rb +45 -0
- data/lib/is_msfte_searchable/arel_mixin.rb +66 -0
- data/lib/is_msfte_searchable/version.rb +3 -0
- data/test/active_record_extension_test.rb +191 -0
- data/test/active_record_mixin_test.rb +151 -0
- data/test/arel_mixin_test.rb +183 -0
- data/test/helper.rb +6 -0
- metadata +179 -0
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2012 Decisiv, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# IsMsfteSearchable
|
2
|
+
|
3
|
+
IsMsfteSearchable provides a class method on ActiveRecord::Base that
|
4
|
+
subclasses may use to flag themselves as having full-text indexes in
|
5
|
+
Microsoft SQL Server. It provides utility methods for searching using
|
6
|
+
the index, as well as utility methods for building and destroying the
|
7
|
+
index.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
desc 'Test is_msfte_searchable'
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => [:test]
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_record'
|
3
|
+
require 'is_msfte_searchable/active_record_extension'
|
4
|
+
require 'is_msfte_searchable/active_record_mixin'
|
5
|
+
require 'is_msfte_searchable/arel_mixin'
|
6
|
+
require 'is_msfte_searchable/version'
|
7
|
+
|
8
|
+
ActiveRecord::Base.send :include, IsMsfteSearchable::ActiveRecordExtension
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module IsMsfteSearchable
|
2
|
+
module ActiveRecordExtension
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def is_msfte_searchable(options={})
|
7
|
+
options.reverse_merge! :change_tracking => true, :update_index => true
|
8
|
+
cattr_accessor :msfte_table_name, :msfte_columns, :msfte_catalog, :msfte_unique_key_column, :msfte_unique_key_index, :msfte_change_tracking, :msfte_update_index
|
9
|
+
self.msfte_table_name = options[:table_name] ? options[:table_name].to_s : table_name
|
10
|
+
self.msfte_columns = options[:columns] ? options[:columns].map(&:to_s) : column_names
|
11
|
+
self.msfte_catalog = options[:catalog] ? options[:catalog].to_s : "#{msfte_table_name}_fti"
|
12
|
+
self.msfte_unique_key_column = options[:unique_key_column] ? options[:unique_key_column].to_s : primary_key
|
13
|
+
self.msfte_unique_key_index = options[:unique_key_index] ? options[:unique_key_index].to_s : "#{msfte_unique_key_column}_idx"
|
14
|
+
self.msfte_change_tracking = options[:change_tracking]
|
15
|
+
self.msfte_update_index = options[:update_index]
|
16
|
+
include IsMsfteSearchable::ActiveRecordMixin
|
17
|
+
include IsMsfteSearchable::ArelMixin
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module IsMsfteSearchable
|
2
|
+
module ActiveRecordMixin
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def msfte_setup
|
7
|
+
connection.execute %|sp_fulltext_catalog '#{msfte_catalog}', 'create'|
|
8
|
+
connection.execute %|sp_fulltext_table 'dbo.#{msfte_table_name}', 'create', '#{msfte_catalog}', '#{msfte_unique_key_index}'|
|
9
|
+
msfte_columns.each { |col| connection.execute(%|sp_fulltext_column '#{msfte_table_name}', '#{col}', 'add'|) }
|
10
|
+
connection.execute %|sp_fulltext_table 'dbo.#{msfte_table_name}', 'start_change_tracking'| if msfte_change_tracking
|
11
|
+
connection.execute %|sp_fulltext_table 'dbo.#{msfte_table_name}', 'start_background_updateindex'| if msfte_update_index
|
12
|
+
end
|
13
|
+
|
14
|
+
def msfte_teardown
|
15
|
+
connection.execute %|sp_fulltext_table 'dbo.#{msfte_table_name}', 'drop'| rescue nil
|
16
|
+
connection.execute %|sp_fulltext_catalog '#{msfte_catalog}', 'drop'| rescue nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def msfte_reset!
|
20
|
+
msfte_teardown
|
21
|
+
yield if block_given?
|
22
|
+
msfte_setup
|
23
|
+
end
|
24
|
+
|
25
|
+
def msfte_catalog_rebuild
|
26
|
+
connection.execute %|sp_fulltext_catalog '#{msfte_catalog}', 'rebuild'|
|
27
|
+
end
|
28
|
+
|
29
|
+
def msfte_quote(string)
|
30
|
+
connection.quote_string(string)
|
31
|
+
end
|
32
|
+
|
33
|
+
def msfte_search_string(query, boolean=nil)
|
34
|
+
if boolean
|
35
|
+
# sql2k won't treat punctuation as valid search terms, so strip them out until we upgrade to 2k5+
|
36
|
+
termed_query = query.gsub(/[^\w\d\s]+/, '').split(/\s+/).map{ |term| %|"#{term} *"| }.join(" #{boolean} ").strip
|
37
|
+
else
|
38
|
+
termed_query = %|"#{query} *"|
|
39
|
+
end
|
40
|
+
%|'#{msfte_quote(termed_query)}'|
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module IsMsfteSearchable
|
2
|
+
module ArelMixin
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_eval do
|
7
|
+
scope :msfte_with_phrase, lambda { |query|
|
8
|
+
return {} if query.blank?
|
9
|
+
msfte_contains(msfte_search_string(query))
|
10
|
+
}
|
11
|
+
|
12
|
+
scope :msfte_with_any, lambda { |query|
|
13
|
+
return {} if query.blank?
|
14
|
+
msfte_contains(msfte_search_string(query, 'OR'))
|
15
|
+
}
|
16
|
+
|
17
|
+
scope :msfte_with_all, lambda { |query|
|
18
|
+
return {} if query.blank?
|
19
|
+
msfte_contains(msfte_search_string(query, 'AND'))
|
20
|
+
}
|
21
|
+
|
22
|
+
scope :msfte_with_booleans, lambda { |query|
|
23
|
+
return {} if query.blank?
|
24
|
+
msfte_contains(query, :quote => true)
|
25
|
+
}
|
26
|
+
|
27
|
+
msfte_columns.each do |c|
|
28
|
+
scope "msfte_#{c}_with_any".to_sym, lambda { |query|
|
29
|
+
return {} if query.blank?
|
30
|
+
return msfte_like_bailout(c, query) if Rails.env.test?
|
31
|
+
msfte_contains(msfte_search_string(query, 'OR'), :column => c)
|
32
|
+
}
|
33
|
+
|
34
|
+
scope "msfte_#{c}_with_all".to_sym, lambda { |query|
|
35
|
+
return {} if query.blank?
|
36
|
+
return msfte_like_bailout(c, query) if Rails.env.test?
|
37
|
+
msfte_contains(msfte_search_string(query, 'AND'), :column => c)
|
38
|
+
}
|
39
|
+
|
40
|
+
scope "msfte_#{c}_with_booleans".to_sym, lambda { |query|
|
41
|
+
return {} if query.blank?
|
42
|
+
return msfte_like_bailout(c, query) if Rails.env.test?
|
43
|
+
msfte_contains(query, :column => c, :quote => true)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
private
|
51
|
+
|
52
|
+
def msfte_like_bailout(column, query)
|
53
|
+
{ :conditions => "#{table_name}.#{column} LIKE '%#{msfte_quote(query)}%'" }
|
54
|
+
end
|
55
|
+
|
56
|
+
def msfte_contains(query, options = {})
|
57
|
+
column = options.fetch(:column, '*')
|
58
|
+
quote = options.fetch(:quote, false)
|
59
|
+
query_literal = quote ? '?' : query
|
60
|
+
condition = "#{table_name}.#{primary_key} IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(#{msfte_table_name},#{column},#{query_literal}) AS KEY_TBL)"
|
61
|
+
conditions = quote ? [condition, query] : condition
|
62
|
+
{ :conditions => conditions }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe IsMsfteSearchable::ActiveRecordExtension do
|
4
|
+
it "extends ActiveRecord::Base with an is_msfte_searchable class method" do
|
5
|
+
ActiveRecord::Base.must_respond_to(:is_msfte_searchable)
|
6
|
+
end
|
7
|
+
|
8
|
+
class FakeActiveRecord
|
9
|
+
def self.table_name
|
10
|
+
'people'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.column_names
|
14
|
+
%w(id name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.primary_key
|
18
|
+
'id'
|
19
|
+
end
|
20
|
+
|
21
|
+
include IsMsfteSearchable::ActiveRecordExtension
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".is_msfte_searchable" do
|
25
|
+
let(:model) do
|
26
|
+
Class.new(FakeActiveRecord)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "adds msfte_table_name class method" do
|
30
|
+
it "that exists" do
|
31
|
+
model.is_msfte_searchable
|
32
|
+
model.must_respond_to(:msfte_table_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "defaults to the model's table_name" do
|
36
|
+
model.is_msfte_searchable
|
37
|
+
model.msfte_table_name.must_equal 'people'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "is overridden by the :table_name option" do
|
41
|
+
model.is_msfte_searchable(:table_name => 'persons')
|
42
|
+
model.msfte_table_name.must_equal 'persons'
|
43
|
+
end
|
44
|
+
|
45
|
+
it "and an accessor method" do
|
46
|
+
model.is_msfte_searchable
|
47
|
+
model.msfte_table_name = 'persons'
|
48
|
+
model.msfte_table_name.must_equal 'persons'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "adds msfte_columns class method" do
|
53
|
+
it "that exists" do
|
54
|
+
model.is_msfte_searchable
|
55
|
+
model.must_respond_to(:msfte_columns)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "defaults to the model's column_names" do
|
59
|
+
model.is_msfte_searchable
|
60
|
+
model.msfte_columns.must_equal %w(id name)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "is overriden by the :columns option" do
|
64
|
+
model.is_msfte_searchable(:columns => [:first_name, :last_name])
|
65
|
+
model.msfte_columns.must_equal %w(first_name last_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "and an accessor method" do
|
69
|
+
model.is_msfte_searchable
|
70
|
+
model.msfte_columns = %w(first_name last_name)
|
71
|
+
model.msfte_columns.must_equal %w(first_name last_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "adds msfte_catalog class method" do
|
76
|
+
it "that exists" do
|
77
|
+
model.is_msfte_searchable
|
78
|
+
model.must_respond_to(:msfte_catalog)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "defaults to a value derived from the msfte_table_name" do
|
82
|
+
model.is_msfte_searchable
|
83
|
+
model.msfte_catalog.must_equal "people_fti"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "is overridden by the :catalog option" do
|
87
|
+
model.is_msfte_searchable(:catalog => 'persons_fti')
|
88
|
+
model.msfte_catalog.must_equal "persons_fti"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "and an accessor method" do
|
92
|
+
model.is_msfte_searchable
|
93
|
+
model.msfte_catalog = 'persons_fti'
|
94
|
+
model.msfte_catalog.must_equal "persons_fti"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "adds msfte_unique_key_column class method" do
|
99
|
+
it "that exists" do
|
100
|
+
model.is_msfte_searchable
|
101
|
+
model.must_respond_to(:msfte_unique_key_column)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "defaults to the model's primary_key" do
|
105
|
+
model.is_msfte_searchable
|
106
|
+
model.msfte_unique_key_column.must_equal 'id'
|
107
|
+
end
|
108
|
+
|
109
|
+
it "is overridden by the :unique_key_column option" do
|
110
|
+
model.is_msfte_searchable(:unique_key_column => 'pkey')
|
111
|
+
model.msfte_unique_key_column.must_equal 'pkey'
|
112
|
+
end
|
113
|
+
|
114
|
+
it "and an accessor method" do
|
115
|
+
model.is_msfte_searchable
|
116
|
+
model.msfte_unique_key_column = 'pkey'
|
117
|
+
model.msfte_unique_key_column.must_equal 'pkey'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "adds msfte_unique_key_index class method" do
|
122
|
+
it "that exists" do
|
123
|
+
model.is_msfte_searchable
|
124
|
+
model.must_respond_to(:msfte_unique_key_index)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "defaults to a value derived from the msfte_unique_key_column" do
|
128
|
+
model.is_msfte_searchable
|
129
|
+
model.msfte_unique_key_index.must_equal('id_idx')
|
130
|
+
end
|
131
|
+
|
132
|
+
it "is overridden by the :unique_key_index option" do
|
133
|
+
model.is_msfte_searchable(:unique_key_index => 'my_index')
|
134
|
+
model.msfte_unique_key_index.must_equal('my_index')
|
135
|
+
end
|
136
|
+
|
137
|
+
it "and an accessor method" do
|
138
|
+
model.is_msfte_searchable
|
139
|
+
model.msfte_unique_key_index = 'my_index'
|
140
|
+
model.msfte_unique_key_index.must_equal('my_index')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "adds msfte_change_tracking class method" do
|
145
|
+
it "that exists" do
|
146
|
+
model.is_msfte_searchable
|
147
|
+
model.must_respond_to(:msfte_change_tracking)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "defaults to true" do
|
151
|
+
model.is_msfte_searchable
|
152
|
+
model.msfte_change_tracking.must_equal true
|
153
|
+
end
|
154
|
+
|
155
|
+
it "is overridden by the :change_tracking option" do
|
156
|
+
model.is_msfte_searchable(:change_tracking => false)
|
157
|
+
model.msfte_change_tracking.must_equal false
|
158
|
+
end
|
159
|
+
|
160
|
+
it "and an accessor method" do
|
161
|
+
model.is_msfte_searchable
|
162
|
+
model.msfte_change_tracking = false
|
163
|
+
model.msfte_change_tracking.must_equal false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "adds msfte_update_index class method" do
|
168
|
+
it "that exists" do
|
169
|
+
model.is_msfte_searchable
|
170
|
+
model.must_respond_to(:msfte_update_index)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "defaults to true" do
|
174
|
+
model.is_msfte_searchable
|
175
|
+
model.msfte_update_index.must_equal true
|
176
|
+
end
|
177
|
+
|
178
|
+
it "is overridden by the :update_index option" do
|
179
|
+
model.is_msfte_searchable(:update_index => false)
|
180
|
+
model.msfte_update_index.must_equal false
|
181
|
+
end
|
182
|
+
|
183
|
+
it "and an accessor method" do
|
184
|
+
model.is_msfte_searchable
|
185
|
+
model.msfte_update_index = false
|
186
|
+
model.msfte_update_index.must_equal false
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe IsMsfteSearchable::ActiveRecordMixin do
|
4
|
+
class FakeConnection
|
5
|
+
def execute(command)
|
6
|
+
end
|
7
|
+
|
8
|
+
def quote_string(value)
|
9
|
+
value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class FakeActiveRecord
|
14
|
+
def self.table_name
|
15
|
+
'people'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.column_names
|
19
|
+
%w(id name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.primary_key
|
23
|
+
'id'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.connection
|
27
|
+
@connection ||= FakeConnection.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.scope(name, callable)
|
31
|
+
end
|
32
|
+
|
33
|
+
include IsMsfteSearchable::ActiveRecordExtension
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:model) do
|
37
|
+
model = Class.new(FakeActiveRecord)
|
38
|
+
model.is_msfte_searchable
|
39
|
+
model
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "adds msfte_setup class method" do
|
43
|
+
it "that exists" do
|
44
|
+
model.must_respond_to(:msfte_setup)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "that executes setup commands on the model's database connection" do
|
48
|
+
setup = sequence('setup')
|
49
|
+
connection = mock('connection') do
|
50
|
+
[
|
51
|
+
"sp_fulltext_catalog 'people_fti', 'create'",
|
52
|
+
"sp_fulltext_table 'dbo.people', 'create', 'people_fti', 'id_idx'",
|
53
|
+
"sp_fulltext_column 'people', 'id', 'add'",
|
54
|
+
"sp_fulltext_column 'people', 'name', 'add'",
|
55
|
+
"sp_fulltext_table 'dbo.people', 'start_change_tracking'",
|
56
|
+
"sp_fulltext_table 'dbo.people', 'start_background_updateindex'"
|
57
|
+
].each do |command|
|
58
|
+
expects(:execute).with(command).in_sequence(setup)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
model.stubs(:connection).returns(connection)
|
62
|
+
model.msfte_setup
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "adds msfte_teardown class method" do
|
67
|
+
it "that exists" do
|
68
|
+
model.must_respond_to(:msfte_teardown)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "executes teardown commands on the model's database connection" do
|
72
|
+
teardown = sequence(:teardown)
|
73
|
+
connection = mock('connection') do
|
74
|
+
[
|
75
|
+
"sp_fulltext_table 'dbo.people', 'drop'",
|
76
|
+
"sp_fulltext_catalog 'people_fti', 'drop'"
|
77
|
+
].each do |command|
|
78
|
+
expects(:execute).with(command).in_sequence(teardown)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
model.stubs(:connection).returns(connection)
|
82
|
+
model.msfte_teardown
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "adds msfte_reset! class method" do
|
87
|
+
it "that exists" do
|
88
|
+
model.must_respond_to(:msfte_reset!)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "calls msfte_teardown, then msfte_setup" do
|
92
|
+
catalog = states('catalog').starts_as('up')
|
93
|
+
model.expects(:msfte_teardown).then(catalog.is('down'))
|
94
|
+
model.expects(:msfte_setup).when(catalog.is('down')).then(catalog.is('up'))
|
95
|
+
model.msfte_reset!
|
96
|
+
end
|
97
|
+
|
98
|
+
it "yields the given block after msfte_teardown before msfte_setup" do
|
99
|
+
catalog = states('catalog').starts_as('up')
|
100
|
+
model.expects(:msfte_teardown).then(catalog.is('down'))
|
101
|
+
model.expects(:call_the_block!).when(catalog.is('down'))
|
102
|
+
model.expects(:msfte_setup).when(catalog.is('down')).then(catalog.is('up'))
|
103
|
+
model.msfte_reset! do
|
104
|
+
model.call_the_block!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "adds msfte_catalog_rebuild class method" do
|
110
|
+
it "that exists" do
|
111
|
+
model.must_respond_to(:msfte_catalog_rebuild)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "executes rebuild commands on the model's database connection" do
|
115
|
+
connection = mock('connection') do
|
116
|
+
command = "sp_fulltext_catalog 'people_fti', 'rebuild'"
|
117
|
+
expects(:execute).with(command)
|
118
|
+
end
|
119
|
+
model.stubs(:connection).returns(connection)
|
120
|
+
model.msfte_catalog_rebuild
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "adds msfte_quote class method" do
|
125
|
+
it "that exists" do
|
126
|
+
model.must_respond_to(:msfte_quote)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "delegates to the model's database connection" do
|
130
|
+
connection = mock('connection') do
|
131
|
+
expects('quote_string').with('unquoted').returns('quoted')
|
132
|
+
end
|
133
|
+
model.stubs(:connection).returns(connection)
|
134
|
+
model.msfte_quote('unquoted').must_equal 'quoted'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "adds msfte_search_string class method" do
|
139
|
+
it "that exists" do
|
140
|
+
model.must_respond_to(:msfte_search_string)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "adds a wildcard to and quotes the query" do
|
144
|
+
model.msfte_search_string('query').must_equal "'\"query *\"'"
|
145
|
+
end
|
146
|
+
|
147
|
+
it "joins the query with the given boolean operator" do
|
148
|
+
model.msfte_search_string('term1 term2 ', 'OR').must_equal "'\"term1 *\" OR \"term2 *\"'"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Rails
|
4
|
+
class Env
|
5
|
+
def self.test?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.env
|
11
|
+
Env
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
16
|
+
|
17
|
+
ActiveRecord::Base.class_eval do
|
18
|
+
silence do
|
19
|
+
connection.create_table :people, :force => true do |t|
|
20
|
+
t.column :name, :string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Person < ActiveRecord::Base
|
26
|
+
is_msfte_searchable(:columns => %w(name))
|
27
|
+
end
|
28
|
+
|
29
|
+
# The SQL generated is courtesy of the sqlite adapter, so the whitespace and
|
30
|
+
# quoting behavior are specific to it and may be brittle.
|
31
|
+
describe IsMsfteSearchable::ArelMixin do
|
32
|
+
|
33
|
+
describe ".msfte_with_phrase" do
|
34
|
+
it "exists" do
|
35
|
+
Person.must_respond_to(:msfte_with_phrase)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "returns a scope searching for the query as a phrase" do
|
39
|
+
Person.msfte_with_phrase('foo bar').to_sql.must_equal(
|
40
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,*,'"foo bar *"') AS KEY_TBL))}
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns an empty scope when the query is blank" do
|
45
|
+
Person.msfte_with_phrase('').to_sql.must_equal(
|
46
|
+
%{SELECT "people".* FROM "people" }
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".msfte_with_any" do
|
52
|
+
it "exists" do
|
53
|
+
Person.must_respond_to(:msfte_with_any)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns a scope searching for any of the query terms" do
|
57
|
+
Person.msfte_with_any('foo bar').to_sql.must_equal(
|
58
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,*,'"foo *" OR "bar *"') AS KEY_TBL))}
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "returns an empty scope when the query is blank" do
|
63
|
+
Person.msfte_with_any('').to_sql.must_equal(
|
64
|
+
%{SELECT "people".* FROM "people" }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe ".msfte_with_all" do
|
70
|
+
it "exists" do
|
71
|
+
Person.must_respond_to(:msfte_with_all)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns a scope searching for all of the query terms" do
|
75
|
+
Person.msfte_with_all('foo bar').to_sql.must_equal(
|
76
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,*,'"foo *" AND "bar *"') AS KEY_TBL))}
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "returns an empty scope when the query is blank" do
|
81
|
+
Person.msfte_with_all('').to_sql.must_equal(
|
82
|
+
%{SELECT "people".* FROM "people" }
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe ".msfte_with_booleans" do
|
88
|
+
it "exists" do
|
89
|
+
Person.must_respond_to(:msfte_with_booleans)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "returns a scope searching for the query terms as given?" do
|
93
|
+
Person.msfte_with_booleans('foo bar').to_sql.must_equal(
|
94
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,*,'foo bar') AS KEY_TBL))}
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns an empty scope when the query is blank" do
|
99
|
+
Person.msfte_with_booleans('').to_sql.must_equal(
|
100
|
+
%{SELECT "people".* FROM "people" }
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "column methods" do
|
106
|
+
|
107
|
+
describe ".msfte_name_with_any" do
|
108
|
+
it "exists" do
|
109
|
+
Person.must_respond_to(:msfte_name_with_any)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "returns a scope searching for any of the query terms" do
|
113
|
+
Person.msfte_name_with_any('foo bar').to_sql.must_equal(
|
114
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,name,'"foo *" OR "bar *"') AS KEY_TBL))}
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "returns an empty scope when the query is blank" do
|
119
|
+
Person.msfte_name_with_any('').to_sql.must_equal(
|
120
|
+
%{SELECT "people".* FROM "people" }
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "returns a scope using a LIKE query when Rails.env.test?" do
|
125
|
+
Rails.env.stubs(:test?).returns(true)
|
126
|
+
Person.msfte_name_with_any('foo bar').to_sql.must_equal(
|
127
|
+
%{SELECT "people".* FROM "people" WHERE (people.name LIKE '%foo bar%')}
|
128
|
+
)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe ".msfte_name_with_all" do
|
133
|
+
it "exists" do
|
134
|
+
Person.must_respond_to(:msfte_name_with_all)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "returns a scope searching for all of the query terms" do
|
138
|
+
Person.msfte_name_with_all('foo bar').to_sql.must_equal(
|
139
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,name,'"foo *" AND "bar *"') AS KEY_TBL))}
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "returns an empty scope when the query is blank" do
|
144
|
+
Person.msfte_name_with_all('').to_sql.must_equal(
|
145
|
+
%{SELECT "people".* FROM "people" }
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "returns a scope using a LIKE query when Rails.env.test?" do
|
150
|
+
Rails.env.stubs(:test?).returns(true)
|
151
|
+
Person.msfte_name_with_all('foo bar').to_sql.must_equal(
|
152
|
+
%{SELECT "people".* FROM "people" WHERE (people.name LIKE '%foo bar%')}
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe ".msfte_name_with_booleans" do
|
158
|
+
it "exists" do
|
159
|
+
Person.must_respond_to(:msfte_name_with_booleans)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "returns a scope searching for the query terms as given?" do
|
163
|
+
Person.msfte_name_with_booleans('foo bar').to_sql.must_equal(
|
164
|
+
%{SELECT "people".* FROM "people" WHERE (people.id IN (SELECT [KEY_TBL].[KEY] FROM CONTAINSTABLE(people,name,'foo bar') AS KEY_TBL))}
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
it "returns an empty scope when the query is blank" do
|
169
|
+
Person.msfte_name_with_booleans('').to_sql.must_equal(
|
170
|
+
%{SELECT "people".* FROM "people" }
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "returns a scope using a LIKE query when Rails.env.test?" do
|
175
|
+
Rails.env.stubs(:test?).returns(true)
|
176
|
+
Person.msfte_name_with_booleans('foo bar').to_sql.must_equal(
|
177
|
+
%{SELECT "people".* FROM "people" WHERE (people.name LIKE '%foo bar%')}
|
178
|
+
)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: is_msfte_searchable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 3
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 3.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ken Collins
|
14
|
+
- Donald Ball
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2012-03-14 00:00:00 Z
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 15
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 2
|
33
|
+
- 0
|
34
|
+
version: 3.2.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: activesupport
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 15
|
46
|
+
segments:
|
47
|
+
- 3
|
48
|
+
- 2
|
49
|
+
- 0
|
50
|
+
version: 3.2.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rake
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 63
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 9
|
65
|
+
- 2
|
66
|
+
version: 0.9.2
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 45
|
78
|
+
segments:
|
79
|
+
- 2
|
80
|
+
- 8
|
81
|
+
- 1
|
82
|
+
version: 2.8.1
|
83
|
+
type: :development
|
84
|
+
version_requirements: *id004
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: mocha
|
87
|
+
prerelease: false
|
88
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
hash: 61
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
- 10
|
97
|
+
- 5
|
98
|
+
version: 0.10.5
|
99
|
+
type: :development
|
100
|
+
version_requirements: *id005
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: sqlite3
|
103
|
+
prerelease: false
|
104
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
hash: 9
|
110
|
+
segments:
|
111
|
+
- 1
|
112
|
+
- 3
|
113
|
+
version: "1.3"
|
114
|
+
type: :development
|
115
|
+
version_requirements: *id006
|
116
|
+
description: ActiveRecord extensions for Microsoft SQL Server full-text index
|
117
|
+
email:
|
118
|
+
- ken@metaskills.net
|
119
|
+
- donald.ball@gmail.com
|
120
|
+
executables: []
|
121
|
+
|
122
|
+
extensions: []
|
123
|
+
|
124
|
+
extra_rdoc_files: []
|
125
|
+
|
126
|
+
files:
|
127
|
+
- .gitignore
|
128
|
+
- CHANGELOG.md
|
129
|
+
- Gemfile
|
130
|
+
- LICENSE
|
131
|
+
- README.md
|
132
|
+
- Rakefile
|
133
|
+
- lib/is_msfte_searchable.rb
|
134
|
+
- lib/is_msfte_searchable/active_record_extension.rb
|
135
|
+
- lib/is_msfte_searchable/active_record_mixin.rb
|
136
|
+
- lib/is_msfte_searchable/arel_mixin.rb
|
137
|
+
- lib/is_msfte_searchable/version.rb
|
138
|
+
- test/active_record_extension_test.rb
|
139
|
+
- test/active_record_mixin_test.rb
|
140
|
+
- test/arel_mixin_test.rb
|
141
|
+
- test/helper.rb
|
142
|
+
homepage: http://github.com/Decisiv/is_msfte_searchable/
|
143
|
+
licenses: []
|
144
|
+
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options:
|
147
|
+
- --charset=UTF-8
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ">="
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
hash: 3
|
156
|
+
segments:
|
157
|
+
- 0
|
158
|
+
version: "0"
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
none: false
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
hash: 3
|
165
|
+
segments:
|
166
|
+
- 0
|
167
|
+
version: "0"
|
168
|
+
requirements: []
|
169
|
+
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 1.8.17
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: ActiveRecord extensions for Microsoft SQL Server full-text index
|
175
|
+
test_files:
|
176
|
+
- test/active_record_extension_test.rb
|
177
|
+
- test/active_record_mixin_test.rb
|
178
|
+
- test/arel_mixin_test.rb
|
179
|
+
- test/helper.rb
|