activerecord_views 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record_views/database_tasks.rb +91 -0
  3. data/lib/active_record_views/railtie.rb +0 -4
  4. data/lib/active_record_views/version.rb +1 -1
  5. data/lib/active_record_views.rb +2 -0
  6. metadata +11 -65
  7. data/.gitignore +0 -8
  8. data/.rspec +0 -2
  9. data/.tool-versions +0 -1
  10. data/Appraisals +0 -15
  11. data/Gemfile +0 -3
  12. data/LICENSE.txt +0 -22
  13. data/Rakefile +0 -14
  14. data/activerecord_views.gemspec +0 -27
  15. data/gemfiles/rails5_2.gemfile +0 -7
  16. data/gemfiles/rails6_0.gemfile +0 -7
  17. data/gemfiles/rails6_1.gemfile +0 -7
  18. data/gemfiles/rails7_0.gemfile +0 -7
  19. data/lib/tasks/active_record_views.rake +0 -106
  20. data/spec/active_record_views_checksum_cache_spec.rb +0 -112
  21. data/spec/active_record_views_extension_spec.rb +0 -459
  22. data/spec/active_record_views_spec.rb +0 -344
  23. data/spec/internal/Rakefile +0 -24
  24. data/spec/internal/app/models/dependency_a.rb +0 -3
  25. data/spec/internal/app/models/dependency_b.rb +0 -3
  26. data/spec/internal/app/models/dependency_c.rb +0 -3
  27. data/spec/internal/app/models/erb_test_model.rb +0 -7
  28. data/spec/internal/app/models/erb_test_model.sql.erb +0 -1
  29. data/spec/internal/app/models/external_file_test_model.rb +0 -3
  30. data/spec/internal/app/models/external_file_test_model.sql +0 -1
  31. data/spec/internal/app/models/heredoc_test_model.rb +0 -5
  32. data/spec/internal/app/models/modified_a.rb +0 -3
  33. data/spec/internal/app/models/modified_b.rb +0 -3
  34. data/spec/internal/app/models/namespace/test_model.rb +0 -3
  35. data/spec/internal/app/models/namespace/test_model.sql +0 -1
  36. data/spec/internal/config/database.yml +0 -9
  37. data/spec/internal/config/routes.rb +0 -3
  38. data/spec/spec_helper.rb +0 -130
  39. data/spec/support/silence_warnings.rb +0 -6
  40. data/spec/tasks_spec.rb +0 -113
@@ -1,344 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe ActiveRecordViews do
4
- describe '.create_view' do
5
- let(:connection) { ActiveRecord::Base.connection }
6
-
7
- def create_test_view(sql, options = {})
8
- ActiveRecordViews.create_view connection, 'test', 'Test', sql, options
9
- end
10
-
11
- def drop_test_view
12
- ActiveRecordViews.drop_view connection, 'test'
13
- end
14
-
15
- def test_view_sql
16
- connection.select_value(<<-SQL.squish).try(&:squish)
17
- SELECT view_definition
18
- FROM information_schema.views
19
- WHERE table_schema = 'public' AND table_name = 'test'
20
- SQL
21
- end
22
-
23
- def test_view_populated?
24
- value = connection.select_value(<<~SQL)
25
- SELECT ispopulated
26
- FROM pg_matviews
27
- WHERE schemaname = 'public' AND matviewname = 'test'
28
- SQL
29
-
30
- value
31
- end
32
-
33
- def test_view_refreshed_at
34
- connection.select_value(<<~SQL)
35
- SELECT refreshed_at
36
- FROM active_record_views
37
- WHERE name = 'test'
38
- SQL
39
- end
40
-
41
- def test_materialized_view_sql
42
- connection.select_value(<<-SQL.squish).try(&:squish)
43
- SELECT definition
44
- FROM pg_matviews
45
- WHERE schemaname = 'public' AND matviewname = 'test'
46
- SQL
47
- end
48
-
49
- it 'creates database view' do
50
- expect(test_view_sql).to be_nil
51
- create_test_view 'select 1 as id'
52
- expect(test_view_sql).to eq 'SELECT 1 AS id;'
53
- end
54
-
55
- it 'records checksum, class name, and options' do
56
- create_test_view 'select 1 as id', materialized: true
57
- expect(connection.select_all('select * from active_record_views').to_a).to eq [
58
- {
59
- 'name' => 'test',
60
- 'class_name' => 'Test',
61
- 'checksum' => Digest::SHA1.hexdigest('select 1 as id'),
62
- 'options' => '{"materialized":true,"dependencies":[]}',
63
- 'refreshed_at' => nil,
64
- }
65
- ]
66
- end
67
-
68
- it 'persists views if transaction rolls back' do
69
- expect(test_view_sql).to be_nil
70
- connection.transaction :requires_new => true do
71
- create_test_view 'select 1 as id'
72
- raise ActiveRecord::Rollback
73
- end
74
- expect(test_view_sql).to eq 'SELECT 1 AS id;'
75
- end
76
-
77
- it 'raises descriptive error if view SQL is invalid' do
78
- expect {
79
- create_test_view 'select blah'
80
- }.to raise_error ActiveRecord::StatementInvalid, /column "blah" does not exist/
81
- end
82
-
83
- context 'with existing view' do
84
- before do
85
- create_test_view 'select 1 as id'
86
- expect(test_view_sql).to eq 'SELECT 1 AS id;'
87
- end
88
-
89
- it 'updates view with compatible change' do
90
- create_test_view 'select 2 as id'
91
- expect(test_view_sql).to eq 'SELECT 2 AS id;'
92
- end
93
-
94
- it 'recreates view with incompatible change' do
95
- create_test_view "select 'foo'::text as name"
96
- expect(test_view_sql).to eq "SELECT 'foo'::text AS name;"
97
- end
98
-
99
- context 'having dependant views' do
100
- before do
101
- without_dependency_checks do
102
- ActiveRecordViews.create_view connection, 'dependant1', 'Dependant1', 'SELECT id FROM test;'
103
- ActiveRecordViews.create_view connection, 'dependant2a', 'Dependant2a', 'SELECT id, id * 2 AS id2 FROM dependant1;'
104
- ActiveRecordViews.create_view connection, 'dependant2b', 'Dependant2b', 'SELECT id, id * 4 AS id4 FROM dependant1;'
105
- ActiveRecordViews.create_view connection, 'dependant3', 'Dependant3', 'SELECT * FROM dependant2b;'
106
- ActiveRecordViews.create_view connection, 'dependant4', 'Dependant4', 'SELECT id FROM dependant1 UNION ALL SELECT id FROM dependant3;'
107
- end
108
- end
109
-
110
- it 'updates view with compatible change' do
111
- create_test_view 'select 2 as id'
112
- expect(test_view_sql).to eq 'SELECT 2 AS id;'
113
- expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 4
114
- end
115
-
116
- describe 'changes incompatible with CREATE OR REPLACE' do
117
- it 'updates view with new column added before existing' do
118
- create_test_view "select 'foo'::text as name, 3 as id"
119
- expect(test_view_sql).to eq "SELECT 'foo'::text AS name, 3 AS id;"
120
- expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 6
121
- end
122
-
123
- it 'fails to update view if column used by dependant view is removed' do
124
- expect {
125
- create_test_view "select 'foo'::text as name"
126
- }.to raise_error ActiveRecord::StatementInvalid, /column test.id does not exist/
127
- expect(test_view_sql).to eq 'SELECT 1 AS id;'
128
- expect(Integer(connection.select_value('SELECT id2 FROM dependant2a'))).to eq 2
129
- end
130
- end
131
-
132
- describe '.drop_all_views' do
133
- it 'can drop all managed views' do
134
- connection.execute 'CREATE VIEW unmanaged AS SELECT 2 AS id;'
135
-
136
- expect(view_names).to match_array %w[test dependant1 dependant2a dependant2b dependant3 dependant4 unmanaged]
137
- ActiveRecordViews.drop_all_views connection
138
- expect(view_names).to match_array %w[unmanaged]
139
- end
140
-
141
- it 'support being ran inside a transaction' do
142
- expect(ActiveRecordViews).to receive(:without_transaction).at_least(:once).and_wrap_original do |original, *args, &block|
143
- original.call(*args) do |new_connection|
144
- new_connection.execute 'SET statement_timeout = 1000'
145
- block.call(new_connection)
146
- end
147
- end
148
-
149
- connection.transaction requires_new: true do
150
- expect {
151
- ActiveRecordViews.drop_all_views connection
152
- }.to change { view_names }
153
- end
154
- end
155
-
156
- it 'errors if an unmanaged view depends on a managed view' do
157
- connection.execute 'CREATE VIEW unmanaged AS SELECT * from dependant2a'
158
-
159
- expect {
160
- ActiveRecordViews.drop_all_views connection
161
- }.to raise_error ActiveRecord::StatementInvalid, /view unmanaged depends on view dependant2a/
162
- end
163
-
164
- it 'can drop materialized views' do
165
- without_dependency_checks do
166
- ActiveRecordViews.create_view connection, 'materialized', 'Materialized', 'SELECT id FROM test;', materialized: true
167
- end
168
- ActiveRecordViews.drop_all_views connection
169
- expect(view_names).to match_array %w[]
170
- end
171
- end
172
- end
173
-
174
- describe 'with unmanaged dependant view' do
175
- before do
176
- connection.execute 'CREATE VIEW dependant AS SELECT id FROM test'
177
- end
178
-
179
- after do
180
- connection.execute 'DROP VIEW dependant;'
181
- end
182
-
183
- it 'updates view with compatible change' do
184
- create_test_view 'select 2 as id'
185
- expect(test_view_sql).to eq 'SELECT 2 AS id;'
186
- end
187
-
188
- it 'fails to update view with incompatible change' do
189
- expect {
190
- create_test_view "SELECT 'foo'::text as name, 4 as id"
191
- }.to raise_error ActiveRecord::StatementInvalid, /view dependant depends on view test/
192
- expect(test_view_sql).to eq 'SELECT 1 AS id;'
193
- end
194
- end
195
- end
196
-
197
- it 'creates and drops materialized views' do
198
- create_test_view 'select 123 as id', materialized: true
199
- expect(test_view_sql).to eq nil
200
- expect(test_materialized_view_sql).to eq 'SELECT 123 AS id;'
201
-
202
- drop_test_view
203
- expect(test_view_sql).to eq nil
204
- expect(test_materialized_view_sql).to eq nil
205
- end
206
-
207
- it 'replaces a normal view with a materialized view' do
208
- create_test_view 'select 11 as id'
209
- create_test_view 'select 22 as id', materialized: true
210
-
211
- expect(test_view_sql).to eq nil
212
- expect(test_materialized_view_sql).to eq 'SELECT 22 AS id;'
213
- end
214
-
215
- it 'replaces a materialized view with a normal view' do
216
- create_test_view 'select 22 as id', materialized: true
217
- create_test_view 'select 11 as id'
218
-
219
- expect(test_view_sql).to eq 'SELECT 11 AS id;'
220
- expect(test_materialized_view_sql).to eq nil
221
- end
222
-
223
- it 'can test if materialized views can be refreshed concurrently' do
224
- expect(ActiveRecordViews.supports_concurrent_refresh?(connection)).to be true
225
- end
226
-
227
- it 'preserves materialized view if dropping/recreating' do
228
- without_dependency_checks do
229
- ActiveRecordViews.create_view connection, 'test1', 'Test1', 'SELECT 1 AS foo'
230
- ActiveRecordViews.create_view connection, 'test2', 'Test2', 'SELECT * FROM test1', materialized: true
231
- ActiveRecordViews.create_view connection, 'test1', 'Test1', 'SELECT 2 AS bar, 1 AS foo'
232
- end
233
-
234
- expect(materialized_view_names).to eq %w[test2]
235
- expect(view_names).to eq %w[test1]
236
- end
237
-
238
- it 'supports creating unique indexes on materialized views' do
239
- create_test_view 'select 1 as foo, 2 as bar, 3 as baz', materialized: true, unique_columns: [:foo, 'bar']
240
- index_sql = connection.select_value("SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'test_pkey';")
241
- expect(index_sql).to eq 'CREATE UNIQUE INDEX test_pkey ON public.test USING btree (foo, bar)'
242
- end
243
-
244
- it 'errors if trying to create unique index on non-materialized view' do
245
- expect {
246
- create_test_view 'select 1 as foo, 2 as bar, 3 as baz', materialized: false, unique_columns: [:foo, 'bar']
247
- }.to raise_error ArgumentError, 'unique_columns option requires view to be materialized'
248
- end
249
-
250
- it 'supports resetting all materialised views' do
251
- class ResetMaterializeViewTestModel < ActiveRecord::Base
252
- self.table_name = 'test'
253
- is_view 'select 123 as id', materialized: true
254
- end
255
- ResetMaterializeViewTestModel.refresh_view!
256
-
257
- expect {
258
- ActiveRecordViews.reset_materialized_views
259
- }.to change { test_view_populated? }.to(false)
260
- .and change { test_view_refreshed_at }.to(nil)
261
- end
262
- end
263
-
264
- describe '.drop_all_views' do
265
- let(:connection) { ActiveRecord::Base.connection }
266
-
267
- it 'does nothing when no views have been defined' do
268
- ActiveRecordViews.drop_all_views connection
269
- expect(view_names).to match_array %w[]
270
- end
271
- end
272
-
273
- describe '.without_transaction' do
274
- let(:original_connection) { ActiveRecord::Base.connection }
275
-
276
- it 'yields original connection if no active transaction' do
277
- ActiveRecordViews.without_transaction original_connection do |new_connection|
278
- expect(new_connection).to eq original_connection
279
- end
280
- end
281
-
282
- it 'yields a new connection if inside a transaction' do
283
- original_connection.transaction do
284
- ActiveRecordViews.without_transaction original_connection do |new_connection|
285
- expect(new_connection).to_not eq original_connection
286
- end
287
- end
288
- end
289
-
290
- it 'yields original connection if called recursively' do
291
- ActiveRecordViews.without_transaction original_connection do |new_connection_1|
292
- expect(new_connection_1).to eq original_connection
293
- new_connection_1.transaction do
294
- ActiveRecordViews.without_transaction new_connection_1 do |new_connection_2|
295
- expect(new_connection_2).to eq new_connection_1
296
- end
297
- end
298
- end
299
- end
300
-
301
- it 'yields same isolated connection if called recursively on original connection inside transaction' do
302
- original_connection.transaction do
303
- ActiveRecordViews.without_transaction original_connection do |new_connection_1|
304
- expect(new_connection_1).to_not eq original_connection
305
- ActiveRecordViews.without_transaction original_connection do |new_connection_2|
306
- expect(new_connection_2).to eq new_connection_1
307
- end
308
- end
309
- end
310
- end
311
-
312
- it 'yields different isolated connection if called recursively on different connections inside transcation' do
313
- begin
314
- original_connection_2 = original_connection.pool.checkout
315
-
316
- original_connection.transaction do
317
- ActiveRecordViews.without_transaction original_connection do |new_connection_1|
318
- expect(new_connection_1).to_not eq original_connection
319
- original_connection_2.transaction do
320
- ActiveRecordViews.without_transaction original_connection_2 do |new_connection_2|
321
- expect(new_connection_2).to_not eq original_connection
322
- expect(new_connection_2).to_not eq original_connection_2
323
- expect(new_connection_2).to_not eq new_connection_1
324
- end
325
- end
326
- end
327
- end
328
- ensure
329
- original_connection.pool.checkin original_connection_2
330
- end
331
- end
332
-
333
- it 'does not attempt to checkin when checkout fails' do
334
- expect(original_connection.pool).to receive(:checkout).and_raise PG::ConnectionBad
335
- expect(original_connection.pool).to_not receive(:checkin)
336
-
337
- expect {
338
- original_connection.transaction do
339
- ActiveRecordViews.without_transaction(original_connection) { }
340
- end
341
- }.to raise_error PG::ConnectionBad
342
- end
343
- end
344
- end
@@ -1,24 +0,0 @@
1
- require 'bundler/setup'
2
- require './spec/support/silence_warnings.rb'
3
- require 'combustion'
4
-
5
- Combustion::Database.instance_eval do
6
- def setup(options)
7
- ActiveRecord::Base.configurations = YAML.load(ERB.new(File.read("#{Rails.root}/config/database.yml")).result)
8
- end
9
- end
10
-
11
- Combustion.initialize! :active_record, :action_controller do
12
- config.cache_classes = false
13
- config.log_level = :debug
14
- config.active_record.schema_format = ENV.fetch('SCHEMA_FORMAT', 'sql').to_sym
15
- if Gem::Version.new(Rails.version) >= Gem::Version.new("6.1")
16
- config.active_record.legacy_connection_handling = false
17
- end
18
- if ENV['SKIP_MODEL_EAGER_LOAD']
19
- config.eager_load_paths -= Rails.application.config.paths['app/models'].to_a
20
- end
21
- end
22
-
23
- load 'active_record/railties/databases.rake'
24
- Combustion::Application.load_tasks
@@ -1,3 +0,0 @@
1
- class DependencyA < ActiveRecord::Base
2
- is_view 'SELECT 2 AS id;'
3
- end
@@ -1,3 +0,0 @@
1
- class DependencyB < ActiveRecord::Base
2
- is_view "SELECT id FROM #{DependencyA.table_name};", dependencies: [DependencyA]
3
- end
@@ -1,3 +0,0 @@
1
- class DependencyC < ActiveRecord::Base
2
- is_view "SELECT id FROM #{DependencyB.table_name};", dependencies: [DependencyB]
3
- end
@@ -1,7 +0,0 @@
1
- class ErbTestModel < ActiveRecord::Base
2
- def self.test_erb_method
3
- 'ERB method'
4
- end
5
-
6
- is_view
7
- end
@@ -1 +0,0 @@
1
- SELECT 1 AS id, '<%= test_erb_method %> file'::text AS name;
@@ -1,3 +0,0 @@
1
- class ExternalFileTestModel < ActiveRecord::Base
2
- is_view
3
- end
@@ -1 +0,0 @@
1
- SELECT 1 AS id, 'External SQL file'::text AS name;
@@ -1,5 +0,0 @@
1
- class HeredocTestModel < ActiveRecord::Base
2
- is_view <<-SQL
3
- SELECT 1 AS id, 'Here document'::text AS name;
4
- SQL
5
- end
@@ -1,3 +0,0 @@
1
- class ModifiedA < ActiveRecord::Base
2
- is_view 'SELECT 22 AS new_name;'
3
- end
@@ -1,3 +0,0 @@
1
- class ModifiedB < ActiveRecord::Base
2
- is_view "SELECT new_name FROM #{ModifiedA.table_name};", dependencies: [ModifiedA]
3
- end
@@ -1,3 +0,0 @@
1
- class Namespace::TestModel < ActiveRecord::Base
2
- is_view
3
- end
@@ -1 +0,0 @@
1
- SELECT 1 AS id, 'Namespaced SQL file'::text AS name;
@@ -1,9 +0,0 @@
1
- test: &test
2
- adapter: postgresql
3
- database: activerecord_views_test
4
- min_messages: warning
5
- advisory_locks: <%= !Rails.version.start_with?('6.0.') %>
6
- development:
7
- <<: *test
8
- production:
9
- <<: *test
@@ -1,3 +0,0 @@
1
- Rails.application.routes.draw do
2
- root to: -> (env) { [204, {}, []] }
3
- end
data/spec/spec_helper.rb DELETED
@@ -1,130 +0,0 @@
1
- require 'bundler'
2
- Bundler.setup
3
-
4
- require 'rails/version'
5
- $VERBOSE = true
6
-
7
- require './spec/support/silence_warnings.rb'
8
-
9
- require 'combustion'
10
- require 'active_record_views'
11
-
12
- FileUtils.mkdir_p 'spec/internal/db'
13
- File.write 'spec/internal/db/schema.rb', ''
14
-
15
- TEST_TEMP_MODEL_DIR = Rails.root + 'spec/internal/app/models_temp'
16
- FileUtils.mkdir_p TEST_TEMP_MODEL_DIR
17
- Rails.application.config.paths['app/models'] << 'app/models_temp'
18
-
19
- Combustion.initialize! :active_record, :action_controller do
20
- config.cache_classes = false
21
- config.secret_key_base = 'dummy'
22
- if Gem::Version.new(Rails.version) >= Gem::Version.new("6.1")
23
- config.active_record.legacy_connection_handling = false
24
- end
25
- end
26
- require 'rspec/rails'
27
-
28
- RSpec.shared_context 'sql_statements' do
29
- let(:sql_statements) { [] }
30
-
31
- let!(:sql_statements_subscription) do
32
- ActiveSupport::Notifications.subscribe('sql.active_record') do |_, _, _, _, details|
33
- sql_statements << details.fetch(:sql)
34
- end
35
- end
36
-
37
- after do
38
- ActiveSupport::Notifications.unsubscribe sql_statements_subscription
39
- end
40
- end
41
-
42
- RSpec.configure do |config|
43
- config.use_transactional_fixtures = false
44
- config.filter_run_when_matching focus: true
45
- config.filter_run_excluding skip: true
46
- config.example_status_persistence_file_path = 'tmp/examples.txt'
47
-
48
- config.mock_with :rspec do |mocks|
49
- mocks.verify_doubled_constant_names = true
50
- mocks.verify_partial_doubles = true
51
- end
52
-
53
- config.before do
54
- FileUtils.rm_rf Dir["spec/internal/app/models_temp/*"]
55
-
56
- Rails.application.reloader.reload!
57
-
58
- connection = ActiveRecord::Base.connection
59
-
60
- connection.execute 'DROP TABLE IF EXISTS active_record_views'
61
-
62
- view_names = connection.select_values(<<-SQL.squish)
63
- SELECT table_name
64
- FROM information_schema.views
65
- WHERE table_schema = 'public';
66
- SQL
67
- view_names.each do |view_name|
68
- connection.execute "DROP VIEW IF EXISTS #{connection.quote_table_name view_name} CASCADE"
69
- end
70
-
71
- materialized_view_names = connection.select_values(<<-SQL.squish)
72
- SELECT matviewname
73
- FROM pg_matviews
74
- WHERE schemaname = 'public'
75
- SQL
76
- materialized_view_names.each do |view_name|
77
- connection.execute "DROP MATERIALIZED VIEW IF EXISTS #{connection.quote_table_name view_name} CASCADE"
78
- end
79
- end
80
-
81
- config.include_context 'sql_statements'
82
- end
83
-
84
- def test_request
85
- status, headers, body = Rails.application.call(
86
- 'REQUEST_METHOD' => 'GET',
87
- 'PATH_INFO' => '/',
88
- 'rack.input' => StringIO.new,
89
- )
90
- expect(status).to eq 204
91
- body.close
92
- end
93
-
94
- def update_file(file, new_content)
95
- time = File.exist?(file) ? File.mtime(file) : Time.parse('2012-01-01')
96
- time = time + 1
97
- File.write file, new_content
98
- File.utime time, time, file
99
- end
100
-
101
- def view_names
102
- ActiveRecord::Base.connection.select_values(<<-SQL.squish)
103
- SELECT table_name
104
- FROM information_schema.views
105
- WHERE table_schema = 'public'
106
- SQL
107
- end
108
-
109
- def materialized_view_names
110
- ActiveRecord::Base.connection.select_values(<<-SQL.squish)
111
- SELECT matviewname
112
- FROM pg_matviews
113
- WHERE schemaname = 'public'
114
- SQL
115
- end
116
-
117
- def without_dependency_checks
118
- allow(ActiveRecordViews).to receive(:check_dependencies)
119
- yield
120
- ensure
121
- allow(ActiveRecordViews).to receive(:check_dependencies).and_call_original
122
- end
123
-
124
- def without_create_enabled
125
- old_enabled = ActiveRecordViews::Extension.create_enabled
126
- ActiveRecordViews::Extension.create_enabled = false
127
- yield
128
- ensure
129
- ActiveRecordViews::Extension.create_enabled = old_enabled
130
- end
@@ -1,6 +0,0 @@
1
- require 'warning'
2
- require 'rails/version'
3
-
4
- Warning.process do |_warning|
5
- :raise
6
- end