activerecord_views 0.0.17 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.tool-versions +1 -0
- data/Appraisals +18 -8
- data/README.markdown +28 -14
- data/activerecord_views.gemspec +3 -1
- data/gemfiles/rails4_2.gemfile +2 -1
- data/gemfiles/rails5_0.gemfile +8 -0
- data/gemfiles/{rails3_2.gemfile → rails5_1.gemfile} +2 -2
- data/gemfiles/{rails4_0.gemfile → rails5_2.gemfile} +2 -2
- data/gemfiles/{rails4_1.gemfile → rails6_0.gemfile} +2 -2
- data/gemfiles/rails6_1.gemfile +7 -0
- data/lib/active_record_views.rb +53 -14
- data/lib/active_record_views/checksum_cache.rb +14 -6
- data/lib/active_record_views/extension.rb +29 -13
- data/lib/active_record_views/railtie.rb +14 -4
- data/lib/active_record_views/registered_view.rb +7 -2
- data/lib/active_record_views/version.rb +1 -1
- data/lib/tasks/active_record_views.rake +70 -17
- data/spec/active_record_views_checksum_cache_spec.rb +21 -12
- data/spec/active_record_views_extension_spec.rb +176 -68
- data/spec/active_record_views_spec.rb +62 -3
- data/spec/internal/Rakefile +6 -1
- data/spec/internal/app/models/erb_test_model.rb +4 -0
- data/spec/internal/app/models/erb_test_model.sql.erb +1 -1
- data/spec/internal/config/database.yml +6 -1
- data/spec/internal/config/routes.rb +3 -0
- data/spec/spec_helper.rb +65 -17
- data/spec/support/silence_warnings.rb +23 -0
- data/spec/tasks_spec.rb +86 -10
- metadata +49 -19
- data/spec/internal/app/models/missing_file_test_model.rb +0 -3
- data/spec/internal/db/schema.rb +0 -2
@@ -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
|
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
|
-
|
10
|
-
|
11
|
-
|
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,24 +1,77 @@
|
|
1
|
-
Rake::Task['db:
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
set_psql_env(config)
|
29
|
+
tasks.schema_file
|
14
30
|
end
|
15
31
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
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(
|
19
|
+
expect(metadata_table_exists?).to eq false
|
12
20
|
ActiveRecordViews::ChecksumCache.new(connection)
|
13
|
-
expect(
|
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(
|
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
|
-
|
33
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
96
|
+
class ModifiedErbFileTestModel < ActiveRecord::Base
|
97
|
+
def self.test_erb_method
|
98
|
+
2 * 3 * 7
|
79
99
|
end
|
80
100
|
|
81
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
126
|
+
File.unlink sql_file
|
127
|
+
File.unlink rb_file
|
99
128
|
|
100
|
-
|
129
|
+
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP/).once.and_call_original
|
130
|
+
expect {
|
101
131
|
test_request
|
102
|
-
|
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
|
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
|
-
|
165
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
200
|
+
class MaterializedViewTestModel < ActiveRecord::Base
|
201
|
+
is_view materialized: true
|
202
|
+
end
|
171
203
|
|
172
|
-
|
173
|
-
|
174
|
-
|
204
|
+
expect {
|
205
|
+
MaterializedViewTestModel.first!
|
206
|
+
}.to raise_error ActiveRecord::StatementInvalid, /materialized view "materialized_view_test_models" has not been populated/
|
175
207
|
|
176
|
-
|
177
|
-
|
208
|
+
expect(MaterializedViewTestModel.view_populated?).to eq false
|
209
|
+
expect(MaterializedViewTestModel.refreshed_at).to eq nil
|
178
210
|
|
179
|
-
|
211
|
+
MaterializedViewTestModel.refresh_view!
|
180
212
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
218
|
+
expect(MaterializedViewTestModel.first!.id).to eq 123
|
187
219
|
|
188
|
-
|
189
|
-
test_request
|
220
|
+
File.unlink sql_file
|
190
221
|
|
191
|
-
|
192
|
-
|
193
|
-
|
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(
|
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
|
-
|
253
|
-
|
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
|
-
|
275
|
-
|
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
|