activerecord_views 0.1.5 → 0.1.7

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.
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