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