runestone 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,177 @@
1
+ require 'test_helper'
2
+
3
+ class MultiIndexTest < ActiveSupport::TestCase
4
+
5
+ test 'simple index' do
6
+ address = assert_difference 'Runestone::Model.count', 2 do
7
+ assert_sql(/setweight\(to_tsvector\('english', 'empire state building'\), 'A'\)/, /setweight\(to_tsvector\('russian', 'эмпайр-стейт-билдинг'\), 'A'\)/) do
8
+ Building.create(name_en: 'Empire State Building', name_ru: 'Эмпайр-Стейт-Билдинг')
9
+ end
10
+ end
11
+
12
+ assert_equal([
13
+ [
14
+ 'Building', address.id,
15
+ nil, 'english',
16
+ {"name" => "Empire State Building"},
17
+ "'build':3A 'empir':1A 'state':2A"
18
+ ],
19
+ [
20
+ 'Building', address.id,
21
+ nil, 'russian',
22
+ {"name" => "Эмпайр-Стейт-Билдинг"},
23
+ "'билдинг':4A 'стейт':3A 'эмпайр':2A 'эмпайр-стейт-билдинг':1A"
24
+ ],
25
+ ], address.runestones.map { |rs|
26
+ [
27
+ rs.record_type, rs.record_id,
28
+ rs.name, rs.dictionary,
29
+ rs.data,
30
+ rs.vector
31
+ ]
32
+ })
33
+
34
+ query = Runestone::Model.search('empire')
35
+ assert_sql(<<~SQL, query.to_sql)
36
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'empire:*')) AS rank0
37
+ FROM "runestones"
38
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'empire:*')
39
+ ORDER BY rank0 DESC
40
+ SQL
41
+
42
+ query = Runestone::Model.search('empire', dictionary: 'english')
43
+ assert_sql(<<~SQL, query.to_sql)
44
+ SELECT
45
+ "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('english', 'empire:*')) AS rank0
46
+ FROM "runestones"
47
+ WHERE "runestones"."vector" @@ to_tsquery('english', 'empire:*')
48
+ AND "runestones"."dictionary" = 'english'
49
+ ORDER BY rank0 DESC
50
+ SQL
51
+
52
+ query = Runestone::Model.search('Эмпайр', dictionary: 'russian')
53
+ assert_sql(<<~SQL, query.to_sql)
54
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('russian', 'эмпайр:*')) AS rank0
55
+ FROM "runestones"
56
+ WHERE "runestones"."vector" @@ to_tsquery('russian', 'эмпайр:*')
57
+ AND "runestones"."dictionary" = 'russian'
58
+ ORDER BY rank0 DESC
59
+ SQL
60
+ end
61
+
62
+ test 'empty index' do
63
+ address = assert_difference 'Runestone::Model.count', 1 do
64
+ Address.create(name: nil)
65
+ end
66
+
67
+ assert_equal([[
68
+ 'Address', address.id,
69
+ {},
70
+ ""
71
+ ]], address.runestones.map { |rs|
72
+ [
73
+ rs.record_type,
74
+ rs.record_id,
75
+ rs.data,
76
+ rs.vector
77
+ ]
78
+ })
79
+ end
80
+
81
+ test 'complex index' do
82
+ property = assert_difference 'Runestone::Model.count', 3 do
83
+ Property.create(name: 'Property name', addresses: [
84
+ Address.create(name: 'Address 1'),
85
+ Address.create(name: 'Address 2')
86
+ ])
87
+ end
88
+
89
+ assert_equal([[
90
+ 'Property', property.id,
91
+ {
92
+ "name"=>"Property name",
93
+ "addresses"=>[
94
+ { "id" => property.addresses.first.id, "name" => "Address 1" },
95
+ { "id" => property.addresses.last.id, "name" => "Address 2" }
96
+ ]
97
+ },
98
+ "'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
99
+ ]], property.runestones.map { |rs|
100
+ [
101
+ rs.record_type,
102
+ rs.record_id,
103
+ rs.data,
104
+ rs.vector
105
+ ]
106
+ })
107
+ end
108
+
109
+ test 'index gets created on Model.create' do
110
+ address = assert_difference 'Runestone::Model.count', 1 do
111
+ Address.create(name: 'Address name')
112
+ end
113
+ end
114
+
115
+ test 'index gets updated on Model.create' do
116
+ address = Address.create(name: 'Address name')
117
+ assert_no_difference 'Runestone::Model.count' do
118
+ address.update!(name: 'Address name two')
119
+ end
120
+
121
+ assert_equal([[
122
+ 'Address', address.id,
123
+ {"name" => "Address name two"},
124
+ "'address':1A 'name':2A 'two':3A"
125
+ ]], address.runestones.map { |rs|
126
+ [
127
+ rs.record_type,
128
+ rs.record_id,
129
+ rs.data,
130
+ rs.vector
131
+ ]
132
+ })
133
+ end
134
+
135
+ test 'index gets deleted on Model.destroy' do
136
+ address = Address.create(name: 'Address name')
137
+ assert_difference 'Runestone::Model.count', -1 do
138
+ address.destroy!
139
+ end
140
+ end
141
+
142
+ test 'reindex! deleted removed records' do
143
+ a1 = Address.create(name: 'one')
144
+ a2 = Address.create(name: 'two')
145
+
146
+ assert_no_difference 'Runestone::Model.count' do
147
+ a2.delete
148
+ end
149
+
150
+ assert_difference 'Runestone::Model.count', -1 do
151
+ Address.reindex!
152
+ end
153
+ end
154
+
155
+ test 'reindex! updates runestones on outdated indexes' do
156
+ address = Address.create(name: 'one')
157
+ address.update_columns(name: 'two')
158
+
159
+ assert_equal(["'one':1A"], address.runestones.map(&:vector))
160
+ assert_no_difference 'Runestone::Model.count' do
161
+ Address.reindex!
162
+ end
163
+ assert_equal(["'two':1A"], address.runestones.map { |rs| rs.reload.vector })
164
+ end
165
+
166
+ test 'reindex! creates index if not there' do
167
+ address = Address.create(name: 'one')
168
+ address.runestones.each(&:delete)
169
+
170
+ assert_equal 0, address.reload.runestones.size
171
+ assert_difference 'Runestone::Model.count', 1 do
172
+ Address.reindex!
173
+ end
174
+ assert_equal(["'one':1A"], address.reload.runestones.map(&:vector))
175
+ end
176
+
177
+ end
@@ -0,0 +1,129 @@
1
+ require 'test_helper'
2
+
3
+ class QueryTest < ActiveSupport::TestCase
4
+
5
+ test '::search(query)' do
6
+ query = Runestone::Model.search('seaerch for this')
7
+
8
+ assert_sql(<<~SQL, query.to_sql)
9
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
10
+ FROM "runestones"
11
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
12
+ ORDER BY rank0 DESC
13
+ SQL
14
+ end
15
+
16
+ test '::search(query) normalizes Unicode strings' do
17
+ query = Runestone::Model.search("the search for \u0065\u0301")
18
+
19
+ assert_sql(<<~SQL, query.to_sql)
20
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'the & search & for & \u00e9:*')) AS rank0
21
+ FROM "runestones"
22
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'the & search & for & \u00e9:*')
23
+ ORDER BY rank0 DESC
24
+ SQL
25
+ end
26
+
27
+ test "::search(query with ')" do
28
+ query = Runestone::Model.search("seaerch for ' this")
29
+ assert_sql(<<~SQL, query.to_sql)
30
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
31
+ FROM "runestones"
32
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
33
+ ORDER BY rank0 DESC
34
+ SQL
35
+
36
+ query = Runestone::Model.search("seaerch for james' map")
37
+ assert_sql(<<~SQL, query.to_sql)
38
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & james'' & map:*')) AS rank0
39
+ FROM "runestones"
40
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & james'' & map:*')
41
+ ORDER BY rank0 DESC
42
+ SQL
43
+ end
44
+
45
+ test '::search(query, prefix: :all)' do
46
+ query = Runestone::Model.search('seaerch for this', prefix: :all)
47
+
48
+ assert_sql(<<~SQL, query.to_sql)
49
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch:* & for:* & this:*')) AS rank0
50
+ FROM "runestones"
51
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch:* & for:* & this:*')
52
+ ORDER BY rank0 DESC
53
+ SQL
54
+ end
55
+
56
+ test '::search(query).limit(N)' do
57
+ query = Runestone::Model.search('seaerch for this').limit(10)
58
+
59
+ assert_sql(<<~SQL, query.to_sql)
60
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
61
+ FROM "runestones"
62
+ WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
63
+ ORDER BY rank0 DESC
64
+ LIMIT 10
65
+ SQL
66
+ end
67
+
68
+ test 'Model::search(query)' do
69
+ query = Property.search('seaerch for this')
70
+
71
+ assert_sql(<<~SQL, query.to_sql)
72
+ SELECT
73
+ "properties".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
74
+ FROM "properties"
75
+ INNER JOIN "runestones"
76
+ ON "runestones"."record_id" = "properties"."id"
77
+ AND "runestones"."record_type" = 'Property'
78
+ WHERE
79
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
80
+ ORDER BY rank0 DESC
81
+ SQL
82
+ end
83
+
84
+ test 'Model::search(query) with misspelling in query' do
85
+ Runestone::Corpus.add('search')
86
+ query = Property.search('seaerch for this')
87
+
88
+ assert_sql(<<~SQL, query.to_sql)
89
+ SELECT
90
+ "properties".*,
91
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0,
92
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(seaerch | search) & for & this:*')) AS rank1
93
+ FROM "properties"
94
+ INNER JOIN "runestones" ON
95
+ "runestones"."record_id" = "properties"."id"
96
+ AND "runestones"."record_type" = 'Property'
97
+ WHERE
98
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(seaerch | search) & for & this:*')
99
+ ORDER BY
100
+ rank0 DESC,
101
+ rank1 DESC
102
+ SQL
103
+ end
104
+
105
+ test "::typos with special chars" do
106
+ Runestone::Corpus.add(*%w{avenue aveneue avenue)})
107
+
108
+ words = "AVENUE AV AVE AVN AVEN AVENU AVNUE".split(/\s+/)
109
+ words.each do |word|
110
+ Runestone.add_synonym(word, *words.select { |w| w != word })
111
+ end
112
+
113
+ assert_sql(<<~SQL, Runestone::Model.search('avenue').to_sql)
114
+ SELECT
115
+ "runestones".*,
116
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'avenue:*')) AS rank0,
117
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(avenue:* | aveneue)')) AS rank1,
118
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)')) AS rank2
119
+ FROM "runestones"
120
+ WHERE
121
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)')
122
+ ORDER BY
123
+ rank0 DESC,
124
+ rank1 DESC,
125
+ rank2 DESC
126
+ SQL
127
+ end
128
+
129
+ end
@@ -0,0 +1,128 @@
1
+ require 'test_helper'
2
+
3
+ class SynonymTest < ActiveSupport::TestCase
4
+
5
+ test '::synonyms' do
6
+ Runestone.add_synonyms({
7
+ '17' => %w(17th seventeen seventeenth),
8
+ 'spruce' => %w(pine)
9
+ })
10
+
11
+ query = Runestone::Model.search('17 spruce')
12
+
13
+ assert_sql(<<~SQL, query.to_sql)
14
+ SELECT
15
+ "runestones".*,
16
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
17
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
18
+ FROM "runestones"
19
+ WHERE
20
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
21
+ ORDER BY rank0 DESC, rank1 DESC
22
+ SQL
23
+ end
24
+
25
+ test '::synonyms expanded to two words' do
26
+ Runestone.add_synonyms({
27
+ 'supernovae' => ['super novae']
28
+ })
29
+
30
+ assert_equal "(supernovae:* | (super <1> novae))", Runestone::WebSearch.parse('supernovae').synonymize.to_s
31
+ end
32
+
33
+ test '::synonyms are evaluated in lowercase' do
34
+ Runestone.add_synonyms({
35
+ '17' => %w(17th seventeen seventeenth),
36
+ 'spruce' => %w(pine)
37
+ })
38
+ query = Runestone::Model.search('17 Spruce')
39
+
40
+ assert_sql(<<~SQL, query.to_sql)
41
+ SELECT
42
+ "runestones".*,
43
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
44
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
45
+ FROM "runestones"
46
+ WHERE
47
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
48
+ ORDER BY rank0 DESC, rank1 DESC
49
+ SQL
50
+ end
51
+
52
+ test '::not with synonyms' do
53
+ Runestone.add_synonyms({
54
+ '17' => %w(17th seventeen seventeenth),
55
+ 'spruce' => %w(pine)
56
+ })
57
+ query = Runestone::Model.search('17 -spruce')
58
+
59
+ assert_sql(<<~SQL, query.to_sql)
60
+ SELECT
61
+ "runestones".*,
62
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & !spruce')) AS rank0,
63
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & !spruce')) AS rank1
64
+ FROM "runestones"
65
+ WHERE
66
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & !spruce')
67
+ ORDER BY rank0 DESC, rank1 DESC
68
+ SQL
69
+ end
70
+
71
+ test '::synonym in quotes' do
72
+ Runestone.add_synonyms({
73
+ '17' => %w(17th seventeen seventeenth),
74
+ 'spruce' => %w(pine)
75
+ })
76
+ query = Runestone::Model.search('17 "spruce"')
77
+
78
+ assert_sql(<<~SQL, query.to_sql)
79
+ SELECT
80
+ "runestones".*,
81
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce')) AS rank0,
82
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & spruce')) AS rank1
83
+ FROM "runestones"
84
+ WHERE
85
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & spruce')
86
+ ORDER BY rank0 DESC, rank1 DESC
87
+ SQL
88
+ end
89
+
90
+ test '::synonym expanded for misspellings' do
91
+ Runestone::Corpus.add(*%w{17 seventeen spruce pine plne})
92
+ Runestone.add_synonyms({
93
+ '17' => %w(17th seventeen seventeenth),
94
+ 'spruce' => %w(pine)
95
+ })
96
+ query = Runestone::Model.search('17 spruce')
97
+
98
+ assert_sql(<<~SQL, query.to_sql)
99
+ SELECT
100
+ "runestones".*,
101
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
102
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
103
+ FROM "runestones"
104
+ WHERE
105
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
106
+ ORDER BY rank0 DESC, rank1 DESC
107
+ SQL
108
+ end
109
+
110
+ test '::synonym phrase substitution' do
111
+ Runestone.add_synonyms({
112
+ 'one hundred' => ['100', 'one hundy']
113
+ })
114
+ query = Runestone::Model.search('one hundred spruce')
115
+
116
+ assert_sql(<<~SQL, query.to_sql)
117
+ SELECT
118
+ "runestones".*,
119
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'one & hundred & spruce:*')) AS rank0,
120
+ ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)')) AS rank1
121
+ FROM "runestones"
122
+ WHERE
123
+ "runestones"."vector" @@ to_tsquery('simple_unaccent', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)')
124
+ ORDER BY rank0 DESC, rank1 DESC
125
+ SQL
126
+ end
127
+
128
+ end
@@ -0,0 +1,185 @@
1
+ # To make testing/debugging easier, test within this source tree versus an
2
+ # installed gem
3
+ $LOAD_PATH << File.expand_path('../lib', __FILE__)
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start do
7
+ add_filter %r{^/test/}
8
+ # add_group 'lib', 'sunstone/lib'
9
+ # add_group 'ext', 'sunstone/ext'
10
+ end
11
+
12
+ require "minitest/autorun"
13
+ require 'minitest/unit'
14
+ require 'minitest/reporters'
15
+ require 'faker'
16
+ require 'byebug'
17
+
18
+ require 'active_record'
19
+ require 'active_job'
20
+ require 'active_job/test_helper'
21
+ ActiveJob::Base.queue_adapter = :test
22
+ require 'runestone'
23
+
24
+ # Setup the test db
25
+ ActiveSupport.test_order = :random
26
+ require File.expand_path('../database', __FILE__)
27
+
28
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
29
+
30
+ $debugging = false
31
+
32
+ # File 'lib/active_support/testing/declarative.rb', somewhere in rails....
33
+ class ActiveSupport::TestCase
34
+
35
+ include ActiveJob::TestHelper
36
+
37
+ # File 'lib/active_support/testing/declarative.rb'
38
+ def self.test(name, &block)
39
+ test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
40
+ defined = method_defined? test_name
41
+ raise "#{test_name} is already defined in #{self}" if defined
42
+ if block_given?
43
+ define_method(test_name, &block)
44
+ else
45
+ define_method(test_name) do
46
+ skip "No implementation provided for #{name}"
47
+ end
48
+ end
49
+ end
50
+
51
+ def teardown
52
+ super
53
+ Runestone.synonyms.clear
54
+ Runestone::Model.connection.execute('DELETE FROM runestone_corpus')
55
+ ActiveRecord::Base.subclasses.reject{|k| k.name.start_with?('ActiveRecord') }.each(&:delete_all)
56
+ end
57
+
58
+ def debug
59
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
60
+ $debugging = true
61
+ yield
62
+ ensure
63
+ ActiveRecord::Base.logger = nil
64
+ $debugging = false
65
+ end
66
+
67
+ def capture_sql
68
+ # ActiveRecord::Base.connection.materialize_transactions
69
+ SQLLogger.clear_log
70
+ yield
71
+ SQLLogger.log_all.dup
72
+ end
73
+
74
+ def assert_sql(*patterns_to_match)
75
+ if patterns_to_match.all? { |s| s.is_a?(String) }
76
+ assert_equal(*patterns_to_match.take(2).map { |sql| sql.gsub(/( +|\n\s*|\s+)/, ' ').strip })
77
+ else
78
+ begin
79
+ ret_value = nil
80
+ capture_sql { ret_value = yield }
81
+ ret_value
82
+ ensure
83
+ failed_patterns = []
84
+ patterns_to_match.each do |pattern|
85
+ failed_patterns << pattern unless SQLLogger.log_all.any?{ |sql| pattern === sql }
86
+ end
87
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
88
+ end
89
+ end
90
+ end
91
+
92
+ def assert_no_sql(*patterns_to_match)
93
+ if patterns_to_match.all? { |s| s.is_a?(String) }
94
+ assert_not_equal(*patterns_to_match.take(2).map { |sql| sql.gsub(/( +|\n\s*|\s+)/, ' ').strip })
95
+ else
96
+ begin
97
+ ret_value = nil
98
+ capture_sql { ret_value = yield }
99
+ ret_value
100
+ ensure
101
+ failed_patterns = []
102
+ patterns_to_match.each do |pattern|
103
+ failed_patterns << pattern unless SQLLogger.log_all.any?{ |sql| pattern === sql }
104
+ end
105
+ assert !failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
106
+ end
107
+ end
108
+ end
109
+
110
+ def corpus
111
+ Runestone::Model.connection.execute('SELECT word FROM runestone_corpus ORDER BY word').values.flatten
112
+ end
113
+
114
+ def assert_corpus(*words)
115
+ assert_equal words.flatten.sort, corpus
116
+ end
117
+
118
+ def assert_corpus_has(*words)
119
+ assert_equal 0, (words - corpus).size
120
+ end
121
+
122
+ class SQLLogger
123
+ class << self
124
+ attr_accessor :ignored_sql, :log, :log_all
125
+ def clear_log; self.log = []; self.log_all = []; end
126
+ end
127
+
128
+ self.clear_log
129
+
130
+ self.ignored_sql = [/^PRAGMA/i, /^SELECT currval/i, /^SELECT CAST/i, /^SELECT @@IDENTITY/i, /^SELECT @@ROWCOUNT/i, /^SAVEPOINT/i, /^ROLLBACK TO SAVEPOINT/i, /^RELEASE SAVEPOINT/i, /^SHOW max_identifier_length/i, /^BEGIN/i, /^COMMIT/i]
131
+
132
+ # FIXME: this needs to be refactored so specific database can add their own
133
+ # ignored SQL, or better yet, use a different notification for the queries
134
+ # instead examining the SQL content.
135
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
136
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
137
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
138
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
139
+
140
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
141
+ ignored_sql.concat db_ignored_sql
142
+ end
143
+
144
+ attr_reader :ignore
145
+
146
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
147
+ @ignore = ignore
148
+ end
149
+
150
+ def call(name, start, finish, message_id, values)
151
+ sql = values[:sql]
152
+
153
+ # FIXME: this seems bad. we should probably have a better way to indicate
154
+ # the query was cached
155
+ return if 'CACHE' == values[:name]
156
+
157
+ self.class.log_all << sql
158
+ unless ignore =~ sql
159
+ if $debugging
160
+ puts caller.select { |l| l.starts_with?(File.expand_path('../../lib', __FILE__)) }
161
+ puts "\n\n"
162
+ end
163
+ end
164
+ self.class.log << sql unless ignore =~ sql
165
+ end
166
+ end
167
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLLogger.new)
168
+
169
+ # test/unit backwards compatibility methods
170
+ alias :assert_raise :assert_raises
171
+ alias :assert_not_empty :refute_empty
172
+ alias :assert_not_equal :refute_equal
173
+ alias :assert_not_in_delta :refute_in_delta
174
+ alias :assert_not_in_epsilon :refute_in_epsilon
175
+ alias :assert_not_includes :refute_includes
176
+ alias :assert_not_instance_of :refute_instance_of
177
+ alias :assert_not_kind_of :refute_kind_of
178
+ alias :assert_no_match :refute_match
179
+ alias :assert_not_nil :refute_nil
180
+ alias :assert_not_operator :refute_operator
181
+ alias :assert_not_predicate :refute_predicate
182
+ alias :assert_not_respond_to :refute_respond_to
183
+ alias :assert_not_same :refute_same
184
+
185
+ end