activerecord_views 0.0.17 → 0.1.1

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.
@@ -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.17'
2
+ VERSION = '0.1.1'
3
3
  end
@@ -1,24 +1,77 @@
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
 
16
- require 'shellwords'
17
- system("pg_dump --data-only --table=active_record_views #{Shellwords.escape config['database']} >> #{Shellwords.escape filename}")
18
- raise 'active_record_views metadata dump failed' unless $?.success?
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
+
55
+ begin
56
+ active_record_views_dump = Tempfile.open("active_record_views_dump.sql")
57
+ require 'shellwords'
58
+ system("pg_dump --data-only --no-owner --table=active_record_views #{Shellwords.escape database} >> #{Shellwords.escape active_record_views_dump.path}")
59
+ raise 'active_record_views metadata dump failed' unless $?.success?
60
+
61
+ if Gem::Version.new(Rails.version) >= Gem::Version.new("5.1")
62
+ pg_tasks.send(:remove_sql_header_comments, active_record_views_dump.path)
63
+ end
64
+
65
+ File.open filename, 'a' do |io|
66
+ # Seek to the end to ensure we don't overwrite the existing content
67
+ io.seek(0, IO::SEEK_END)
68
+ IO.copy_stream active_record_views_dump, io
19
69
 
20
- File.open filename, 'a' do |io|
21
- io.puts 'UPDATE public.active_record_views SET refreshed_at = NULL WHERE refreshed_at IS NOT NULL;'
70
+ io.puts 'UPDATE public.active_record_views SET refreshed_at = NULL WHERE refreshed_at IS NOT NULL;'
71
+ end
72
+ ensure
73
+ active_record_views_dump.close
74
+ active_record_views_dump.unlink
22
75
  end
23
76
  end
24
77
  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