activerecord_views 0.0.16 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,15 +2,25 @@ module ActiveRecordViews
2
2
  class Railtie < ::Rails::Railtie
3
3
  initializer 'active_record_views' do |app|
4
4
  ActiveSupport.on_load :active_record do
5
- ActiveRecordViews.sql_load_path << Rails.root + 'app/models'
5
+ ActiveRecordViews.sql_load_path += Rails.application.config.paths['app/models'].to_a
6
6
  ActiveRecordViews.init!
7
+ ActiveRecordViews::Extension.create_enabled = !Rails.env.production?
7
8
  end
8
9
 
9
- ActiveSupport.on_load :action_controller do
10
- unless app.config.cache_classes
11
- ActionDispatch::Callbacks.before do
10
+ unless app.config.cache_classes
11
+ if app.respond_to?(:reloader)
12
+ app.reloader.before_class_unload do
12
13
  ActiveRecordViews.reload_stale_views!
13
14
  end
15
+ app.executor.to_run do
16
+ ActiveRecordViews.reload_stale_views!
17
+ end
18
+ else
19
+ ActiveSupport.on_load :action_controller do
20
+ ActionDispatch::Callbacks.before do
21
+ ActiveRecordViews.reload_stale_views!
22
+ end
23
+ end
14
24
  end
15
25
  end
16
26
  end
@@ -1,6 +1,6 @@
1
1
  module ActiveRecordViews
2
2
  class RegisteredView
3
- attr_reader :sql_path
3
+ attr_reader :model_class_name, :sql_path
4
4
 
5
5
  def initialize(model_class, sql_path)
6
6
  @model_class_name = model_class.name
@@ -16,9 +16,14 @@ module ActiveRecordViews
16
16
  sql_timestamp != @cached_sql_timestamp
17
17
  end
18
18
 
19
+ def dead?
20
+ !File.exist?(sql_path)
21
+ end
22
+
19
23
  def reload!
20
24
  if File.exist? sql_path
21
- ActiveRecordViews.create_view model_class.connection, model_class.table_name, model_class.name, ActiveRecordViews.read_sql_file(sql_path), model_class.view_options
25
+ ActiveRecordViews.create_view model_class.connection, model_class.table_name, model_class.name, ActiveRecordViews.read_sql_file(model_class, sql_path), model_class.view_options
26
+ model_class.reset_column_information
22
27
  else
23
28
  ActiveRecordViews.drop_view model_class.connection, model_class.table_name
24
29
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordViews
2
- VERSION = '0.0.16'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -1,24 +1,63 @@
1
- Rake::Task['db:structure:dump'].enhance do
2
- if ActiveRecord::Base.connection.table_exists?('active_record_views')
3
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
4
-
5
- if defined? ActiveRecord::Tasks::DatabaseTasks
6
- tasks = ActiveRecord::Tasks::DatabaseTasks
7
- config = tasks.current_config
8
- tasks.send(:class_for_adapter, config.fetch('adapter')).new(config)
9
- pg_tasks = tasks.send(:class_for_adapter, config.fetch('adapter')).new(config)
10
- pg_tasks.send(:set_psql_env)
1
+ Rake::Task['db:migrate'].enhance do
2
+ unless ActiveRecordViews::Extension.create_enabled
3
+ Rails.application.eager_load!
4
+ ActiveRecordViews::Extension.process_create_queue!
5
+ ActiveRecordViews.drop_unregistered_views!
6
+ end
7
+ end
8
+
9
+ schema_rake_task = Gem::Version.new(Rails.version) >= Gem::Version.new("6.1") ? 'db:schema:dump' : 'db:structure:dump'
10
+
11
+ Rake::Task[schema_rake_task].enhance do
12
+ table_exists = if Rails::VERSION::MAJOR >= 5
13
+ ActiveRecord::Base.connection.data_source_exists?('active_record_views')
14
+ else
15
+ ActiveRecord::Base.connection.table_exists?('active_record_views')
16
+ end
17
+
18
+ if schema_rake_task == 'db:structure:dump'
19
+ ActiveRecord::Base.schema_format = :sql
20
+ end
21
+
22
+ if table_exists && ActiveRecord::Base.schema_format == :sql
23
+ tasks = ActiveRecord::Tasks::DatabaseTasks
24
+
25
+ filename = case
26
+ when tasks.respond_to?(:dump_filename)
27
+ tasks.dump_filename('primary')
11
28
  else
12
- config = current_config
13
- set_psql_env(config)
29
+ tasks.schema_file
14
30
  end
15
31
 
32
+ config = if ActiveRecord::Base.configurations.respond_to?(:configs_for)
33
+ if Rails.version.start_with?('6.0.')
34
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: 'primary').config
35
+ else
36
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'primary')
37
+ end
38
+ else
39
+ tasks.current_config
40
+ end
41
+ adapter = if config.respond_to?(:adapter)
42
+ config.adapter
43
+ else
44
+ config.fetch('adapter')
45
+ end
46
+ database = if config.respond_to?(:database)
47
+ config.database
48
+ else
49
+ config.fetch('database')
50
+ end
51
+
52
+ pg_tasks = tasks.send(:class_for_adapter, adapter).new(config)
53
+ pg_tasks.send(:set_psql_env)
54
+
16
55
  require 'shellwords'
17
- system("pg_dump --data-only --table=active_record_views #{Shellwords.escape config['database']} >> #{Shellwords.escape filename}")
56
+ system("pg_dump --data-only --table=active_record_views #{Shellwords.escape database} >> #{Shellwords.escape filename}")
18
57
  raise 'active_record_views metadata dump failed' unless $?.success?
19
58
 
20
59
  File.open filename, 'a' do |io|
21
- io.puts 'UPDATE active_record_views SET refreshed_at = NULL WHERE refreshed_at IS NOT NULL;'
60
+ io.puts 'UPDATE public.active_record_views SET refreshed_at = NULL WHERE refreshed_at IS NOT NULL;'
22
61
  end
23
62
  end
24
63
  end
@@ -4,13 +4,21 @@ describe ActiveRecordViews::ChecksumCache do
4
4
  let(:connection) { ActiveRecord::Base.connection }
5
5
 
6
6
  describe 'initialisation' do
7
+ def metadata_table_exists?
8
+ if Rails::VERSION::MAJOR >= 5
9
+ connection.data_source_exists?('active_record_views')
10
+ else
11
+ connection.table_exists?('active_record_views')
12
+ end
13
+ end
14
+
7
15
  context 'no existing table' do
8
16
  it 'creates the table' do
9
17
  expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE TABLE active_record_views/).once.and_call_original
10
18
 
11
- expect(connection.table_exists?('active_record_views')).to eq false
19
+ expect(metadata_table_exists?).to eq false
12
20
  ActiveRecordViews::ChecksumCache.new(connection)
13
- expect(connection.table_exists?('active_record_views')).to eq true
21
+ expect(metadata_table_exists?).to eq true
14
22
 
15
23
  expect(connection.column_exists?('active_record_views', 'class_name')).to eq true
16
24
  expect(connection.column_exists?('active_record_views', 'options')).to eq true
@@ -20,7 +28,7 @@ describe ActiveRecordViews::ChecksumCache do
20
28
  context 'existing table with current structure' do
21
29
  before do
22
30
  ActiveRecordViews::ChecksumCache.new(connection)
23
- expect(connection.table_exists?('active_record_views')).to eq true
31
+ expect(metadata_table_exists?).to eq true
24
32
  end
25
33
 
26
34
  it 'does not recreate the table' do
@@ -41,21 +49,22 @@ describe ActiveRecordViews::ChecksumCache do
41
49
  end
42
50
 
43
51
  it 'drops existing managed views recreates the table' do
44
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ABEGIN\z/).once.and_call_original
45
- if Rails::VERSION::MAJOR < 4
46
- expect(ActiveRecord::Base.connection).to receive(:execute).with('SELECT name FROM active_record_views;', nil).once.and_call_original
47
- end
48
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP VIEW IF EXISTS test_view CASCADE;\z/).once.and_call_original
49
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP TABLE active_record_views;\z/).once.and_call_original
50
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE TABLE active_record_views/).once.and_call_original
51
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACOMMIT\z/).once.and_call_original
52
-
53
52
  expect(connection.column_exists?('active_record_views', 'class_name')).to eq false
54
53
  expect(ActiveRecordViews.view_exists?(connection, 'test_view')).to eq true
55
54
  expect(ActiveRecordViews.view_exists?(connection, 'other_view')).to eq true
56
55
 
56
+ sql_statements.clear
57
+
57
58
  ActiveRecordViews::ChecksumCache.new(connection)
58
59
 
60
+ expect(sql_statements.grep_v(/^\s*SELECT/)).to match [
61
+ 'BEGIN',
62
+ 'DROP VIEW IF EXISTS test_view CASCADE;',
63
+ 'DROP TABLE active_record_views;',
64
+ /^CREATE TABLE active_record_views/,
65
+ 'COMMIT',
66
+ ]
67
+
59
68
  expect(connection.column_exists?('active_record_views', 'class_name')).to eq true
60
69
  expect(ActiveRecordViews.view_exists?(connection, 'test_view')).to eq false
61
70
  expect(ActiveRecordViews.view_exists?(connection, 'other_view')).to eq true
@@ -2,6 +2,19 @@ require 'spec_helper'
2
2
 
3
3
  describe ActiveRecordViews::Extension do
4
4
  describe '.is_view' do
5
+ def registered_model_class_names
6
+ ActiveRecordViews.registered_views.map(&:model_class_name)
7
+ end
8
+
9
+ def view_exists?(name)
10
+ connection = ActiveRecord::Base.connection
11
+ if connection.respond_to?(:view_exists?)
12
+ connection.view_exists?(name)
13
+ else
14
+ connection.table_exists?(name)
15
+ end
16
+ end
17
+
5
18
  it 'creates database views from heredocs' do
6
19
  expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
7
20
  expect(HeredocTestModel.first.name).to eq 'Here document'
@@ -19,43 +32,50 @@ describe ActiveRecordViews::Extension do
19
32
 
20
33
  it 'creates database views from external ERB files' do
21
34
  expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
22
- expect(ErbTestModel.first.name).to eq 'ERB file'
35
+ expect(ErbTestModel.first.name).to eq 'ERB method file'
23
36
  end
24
37
 
25
38
  it 'errors if external SQL file is missing' do
26
39
  expect {
27
- MissingFileTestModel
40
+ class MissingFileTestModel < ActiveRecord::Base
41
+ is_view
42
+ end
28
43
  }.to raise_error RuntimeError, /could not find missing_file_test_model.sql/
29
44
  end
30
45
 
31
46
  it 'reloads the database view when external SQL file is modified' do
32
- %w[foo bar baz].each do |sql|
33
- expect(ActiveRecordViews).to receive(:create_view).with(
34
- anything,
35
- 'modified_file_test_models',
36
- 'ModifiedFileTestModel',
37
- sql,
38
- {}
39
- ).once.ordered
40
- end
41
-
42
- with_temp_sql_dir do |temp_dir|
43
- sql_file = File.join(temp_dir, 'modified_file_test_model.sql')
44
- update_file sql_file, 'foo'
47
+ sql_file = File.join(TEST_TEMP_MODEL_DIR, 'modified_file_test_model.sql')
48
+ update_file sql_file, "SELECT 'foo'::text AS test"
45
49
 
50
+ expect {
51
+ expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
46
52
  class ModifiedFileTestModel < ActiveRecord::Base
47
53
  is_view
48
54
  end
55
+ }.to change { begin; ModifiedFileTestModel.take!.test; rescue NameError; end }.from(nil).to('foo')
56
+ .and change { registered_model_class_names.include?('ModifiedFileTestModel') }.from(false).to(true)
49
57
 
50
- update_file sql_file, 'bar'
58
+ expect {
59
+ update_file sql_file, "SELECT 'bar'::text AS test, 42::integer AS test2"
60
+ }.to_not change { ModifiedFileTestModel.take!.test }
51
61
 
62
+ expect {
63
+ expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
52
64
  test_request
53
65
  test_request # second request does not `create_view` again
66
+ }.to change { ModifiedFileTestModel.take!.test }.to('bar')
67
+ .and change { ModifiedFileTestModel.column_names }.from(%w[test]).to(%w[test test2])
54
68
 
55
- update_file sql_file, 'baz'
69
+ expect {
70
+ update_file sql_file, "SELECT 'baz'::text AS test"
71
+ }.to_not change { ModifiedFileTestModel.take!.test }
56
72
 
73
+ expect {
74
+ expect(ActiveRecordViews).to receive(:create_view).once.and_call_original
57
75
  test_request
58
- end
76
+ }.to change { ModifiedFileTestModel.take!.test }.to('baz')
77
+
78
+ File.unlink sql_file
59
79
  test_request # trigger cleanup
60
80
  end
61
81
 
@@ -70,40 +90,53 @@ describe ActiveRecordViews::Extension do
70
90
  ).once.ordered
71
91
  end
72
92
 
73
- with_temp_sql_dir do |temp_dir|
74
- sql_file = File.join(temp_dir, 'modified_erb_file_test_model.sql.erb')
75
- update_file sql_file, 'foo <%= 2*3*7 %>'
93
+ sql_file = File.join(TEST_TEMP_MODEL_DIR, 'modified_erb_file_test_model.sql.erb')
94
+ update_file sql_file, 'foo <%= test_erb_method %>'
76
95
 
77
- class ModifiedErbFileTestModel < ActiveRecord::Base
78
- is_view
96
+ class ModifiedErbFileTestModel < ActiveRecord::Base
97
+ def self.test_erb_method
98
+ 2 * 3 * 7
79
99
  end
80
100
 
81
- update_file sql_file, 'bar <%= 2*3*7 %>'
82
- test_request
101
+ is_view
83
102
  end
103
+
104
+ update_file sql_file, 'bar <%= test_erb_method %>'
105
+ test_request
106
+
107
+ File.unlink sql_file
84
108
  test_request # trigger cleanup
85
109
  end
86
110
 
87
111
  it 'drops the view if the external SQL file is deleted' do
88
- with_temp_sql_dir do |temp_dir|
89
- sql_file = File.join(temp_dir, 'deleted_file_test_model.sql')
90
- File.write sql_file, "SELECT 1 AS id, 'delete test'::text AS name"
112
+ sql_file = File.join(TEST_TEMP_MODEL_DIR, 'deleted_file_test_model.sql')
113
+ File.write sql_file, "SELECT 1 AS id, 'delete test'::text AS name"
91
114
 
115
+ rb_file = 'spec/internal/app/models_temp/deleted_file_test_model.rb'
116
+ File.write rb_file, <<~RB
92
117
  class DeletedFileTestModel < ActiveRecord::Base
93
118
  is_view
94
119
  end
120
+ RB
95
121
 
122
+ with_reloader do
96
123
  expect(DeletedFileTestModel.first.name).to eq 'delete test'
124
+ end
97
125
 
98
- File.unlink sql_file
126
+ File.unlink sql_file
127
+ File.unlink rb_file
99
128
 
100
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP/).once.and_call_original
129
+ expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP/).once.and_call_original
130
+ expect {
101
131
  test_request
102
- test_request # second request does not `drop_view` again
132
+ }.to change { registered_model_class_names.include?('DeletedFileTestModel') }.from(true).to(false)
133
+ .and change { view_exists?('deleted_file_test_models') }.from(true).to(false)
134
+ test_request # second request does not `drop_view` again
103
135
 
136
+ if Rails::VERSION::MAJOR >= 5
104
137
  expect {
105
138
  DeletedFileTestModel.first.name
106
- }.to raise_error ActiveRecord::StatementInvalid, /relation "deleted_file_test_models" does not exist/
139
+ }.to raise_error NameError, 'uninitialized constant DeletedFileTestModel'
107
140
  end
108
141
  end
109
142
 
@@ -161,37 +194,34 @@ describe ActiveRecordViews::Extension do
161
194
  end
162
195
 
163
196
  it 'creates/refreshes/drops materialized views' do
164
- with_temp_sql_dir do |temp_dir|
165
- sql_file = File.join(temp_dir, 'materialized_view_test_model.sql')
166
- File.write sql_file, 'SELECT 123 AS id;'
197
+ sql_file = File.join(TEST_TEMP_MODEL_DIR, 'materialized_view_test_model.sql')
198
+ File.write sql_file, 'SELECT 123 AS id;'
167
199
 
168
- class MaterializedViewTestModel < ActiveRecord::Base
169
- is_view materialized: true
170
- end
200
+ class MaterializedViewTestModel < ActiveRecord::Base
201
+ is_view materialized: true
202
+ end
171
203
 
172
- expect {
173
- MaterializedViewTestModel.first!
174
- }.to raise_error ActiveRecord::StatementInvalid, /materialized view "materialized_view_test_models" has not been populated/
204
+ expect {
205
+ MaterializedViewTestModel.first!
206
+ }.to raise_error ActiveRecord::StatementInvalid, /materialized view "materialized_view_test_models" has not been populated/
175
207
 
176
- expect(MaterializedViewTestModel.view_populated?).to eq false
177
- expect(MaterializedViewTestModel.refreshed_at).to eq nil
208
+ expect(MaterializedViewTestModel.view_populated?).to eq false
209
+ expect(MaterializedViewTestModel.refreshed_at).to eq nil
178
210
 
179
- MaterializedViewTestModel.refresh_view!
211
+ MaterializedViewTestModel.refresh_view!
180
212
 
181
- expect(MaterializedViewTestModel.view_populated?).to eq true
182
- expect(MaterializedViewTestModel.refreshed_at).to be_a ActiveSupport::TimeWithZone
183
- expect(MaterializedViewTestModel.refreshed_at.zone).to eq 'UTC'
184
- expect(MaterializedViewTestModel.refreshed_at).to be_within(1.second).of Time.now
213
+ expect(MaterializedViewTestModel.view_populated?).to eq true
214
+ expect(MaterializedViewTestModel.refreshed_at).to be_a Time
215
+ expect(MaterializedViewTestModel.refreshed_at.zone).to eq 'UTC'
216
+ expect(MaterializedViewTestModel.refreshed_at).to be_within(1.second).of Time.now
185
217
 
186
- expect(MaterializedViewTestModel.first!.id).to eq 123
218
+ expect(MaterializedViewTestModel.first!.id).to eq 123
187
219
 
188
- File.unlink sql_file
189
- test_request
220
+ File.unlink sql_file
190
221
 
191
- expect {
192
- MaterializedViewTestModel.first!
193
- }.to raise_error ActiveRecord::StatementInvalid, /relation "materialized_view_test_models" does not exist/
194
- end
222
+ expect {
223
+ test_request
224
+ }.to change { view_exists?('materialized_view_test_models') }.from(true).to(false)
195
225
  end
196
226
 
197
227
  it 'raises an error for `view_populated?` if view is not materialized' do
@@ -217,7 +247,7 @@ describe ActiveRecordViews::Extension do
217
247
  is_view "SELECT * FROM #{EnsurePopulatedBar.table_name}", dependencies: [EnsurePopulatedBar]
218
248
  end
219
249
 
220
- expect(ActiveRecord::Base.connection).to receive(:execute).with(/^REFRESH MATERIALIZED VIEW/).once.and_call_original
250
+ expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW "ensure_populated_foos";').once.and_call_original
221
251
  allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
222
252
 
223
253
  expect(EnsurePopulatedFoo.view_populated?).to eq false
@@ -227,6 +257,23 @@ describe ActiveRecordViews::Extension do
227
257
  expect(EnsurePopulatedFoo.view_populated?).to eq true
228
258
  end
229
259
 
260
+ it 'invalidates ActiveRecord query cache after populating' do
261
+ class EnsurePopulatedCache < ActiveRecord::Base
262
+ is_view 'SELECT 1 AS id;', materialized: true
263
+ end
264
+
265
+ expect(ActiveRecord::Base.connection).to receive(:execute).with('REFRESH MATERIALIZED VIEW "ensure_populated_caches";').once.and_call_original
266
+ allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
267
+
268
+ ActiveRecord::Base.connection.cache do
269
+ expect(EnsurePopulatedCache.view_populated?).to eq false
270
+ EnsurePopulatedCache.ensure_populated!
271
+ expect(EnsurePopulatedCache.view_populated?).to eq true
272
+ EnsurePopulatedCache.ensure_populated!
273
+ expect(EnsurePopulatedCache.view_populated?).to eq true
274
+ end
275
+ end
276
+
230
277
  it 'supports refreshing materialized views concurrently' do
231
278
  class MaterializedViewRefreshTestModel < ActiveRecord::Base
232
279
  is_view 'SELECT 1 AS id;', materialized: true
@@ -234,23 +281,50 @@ describe ActiveRecordViews::Extension do
234
281
  class MaterializedViewConcurrentRefreshTestModel < ActiveRecord::Base
235
282
  is_view 'SELECT 1 AS id;', materialized: true, unique_columns: [:id]
236
283
  end
284
+ MaterializedViewRefreshTestModel.refresh_view!
237
285
  MaterializedViewConcurrentRefreshTestModel.refresh_view!
238
286
 
239
- [
287
+ sql_statements.clear
288
+
289
+ MaterializedViewRefreshTestModel.refresh_view!
290
+ MaterializedViewRefreshTestModel.refresh_view! concurrent: false
291
+ expect {
292
+ MaterializedViewRefreshTestModel.refresh_view! concurrent: true
293
+ }.to raise_error ActiveRecord::StatementInvalid, /^PG::ObjectNotInPrerequisiteState: ERROR: +cannot refresh/
294
+ MaterializedViewConcurrentRefreshTestModel.refresh_view!
295
+ MaterializedViewConcurrentRefreshTestModel.refresh_view! concurrent: false
296
+ MaterializedViewConcurrentRefreshTestModel.refresh_view! concurrent: true
297
+
298
+ expect(sql_statements.grep_v(/^SELECT/)).to eq [
299
+ 'BEGIN',
300
+ 'REFRESH MATERIALIZED VIEW "materialized_view_refresh_test_models";',
301
+ "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_refresh_test_models';",
302
+ 'COMMIT',
303
+
240
304
  'BEGIN',
241
305
  'REFRESH MATERIALIZED VIEW "materialized_view_refresh_test_models";',
242
306
  "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_refresh_test_models';",
243
307
  'COMMIT',
308
+
309
+ 'BEGIN',
310
+ 'REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_refresh_test_models";',
311
+ 'ROLLBACK',
312
+
244
313
  'BEGIN',
245
314
  'REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_concurrent_refresh_test_models";',
246
315
  "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_concurrent_refresh_test_models';",
247
316
  'COMMIT',
248
- ].each do |sql|
249
- expect(ActiveRecord::Base.connection).to receive(:execute).with(sql).once.and_call_original
250
- end
251
317
 
252
- MaterializedViewRefreshTestModel.refresh_view!
253
- MaterializedViewConcurrentRefreshTestModel.refresh_view! concurrent: true
318
+ 'BEGIN',
319
+ 'REFRESH MATERIALIZED VIEW "materialized_view_concurrent_refresh_test_models";',
320
+ "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_concurrent_refresh_test_models';",
321
+ 'COMMIT',
322
+
323
+ 'BEGIN',
324
+ 'REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_concurrent_refresh_test_models";',
325
+ "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_concurrent_refresh_test_models';",
326
+ 'COMMIT',
327
+ ]
254
328
  end
255
329
 
256
330
  it 'supports opportunistically refreshing materialized views concurrently' do
@@ -258,21 +332,28 @@ describe ActiveRecordViews::Extension do
258
332
  is_view 'SELECT 1 AS id;', materialized: true, unique_columns: [:id]
259
333
  end
260
334
 
261
- [
335
+ sql_statements.clear
336
+
337
+ MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
338
+ MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
339
+ MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
340
+
341
+ expect(sql_statements.grep_v(/^SELECT/)).to eq [
262
342
  'BEGIN',
263
343
  'REFRESH MATERIALIZED VIEW "materialized_view_auto_refresh_test_models";',
264
344
  "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_auto_refresh_test_models';",
265
345
  'COMMIT',
346
+
266
347
  'BEGIN',
267
348
  'REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_auto_refresh_test_models";',
268
349
  "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_auto_refresh_test_models';",
269
350
  'COMMIT',
270
- ].each do |sql|
271
- expect(ActiveRecord::Base.connection).to receive(:execute).with(sql).once.and_call_original
272
- end
273
351
 
274
- MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
275
- MaterializedViewAutoRefreshTestModel.refresh_view! concurrent: :auto
352
+ 'BEGIN',
353
+ 'REFRESH MATERIALIZED VIEW CONCURRENTLY "materialized_view_auto_refresh_test_models";',
354
+ "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = 'materialized_view_auto_refresh_test_models';",
355
+ 'COMMIT',
356
+ ]
276
357
  end
277
358
 
278
359
  it 'raises an error when refreshing materialized views with invalid concurrent option' do
@@ -353,5 +434,32 @@ describe ActiveRecordViews::Extension do
353
434
  dependency_check_good_unmanageds
354
435
  ]
355
436
  end
437
+
438
+ context 'without create_enabled' do
439
+ around do |example|
440
+ without_create_enabled(&example)
441
+ end
442
+
443
+ it 'delays create_view until process_create_queue! is called' do
444
+ allow(ActiveRecordViews).to receive(:create_view).and_call_original
445
+
446
+ expect(ActiveRecordViews::Extension.create_queue.size).to eq 0
447
+ expect(ActiveRecordViews).to_not have_received(:create_view)
448
+
449
+ expect {
450
+ expect(HeredocTestModel.first.name).to eq 'Here document'
451
+ }.to raise_error ActiveRecord::StatementInvalid
452
+
453
+ expect(ActiveRecordViews::Extension.create_queue.size).to eq 1
454
+ expect(ActiveRecordViews).to_not have_received(:create_view)
455
+
456
+ ActiveRecordViews::Extension.process_create_queue!
457
+
458
+ expect(ActiveRecordViews::Extension.create_queue.size).to eq 0
459
+ expect(ActiveRecordViews).to have_received(:create_view)
460
+
461
+ expect(HeredocTestModel.first.name).to eq 'Here document'
462
+ end
463
+ end
356
464
  end
357
465
  end