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