activerecord_views 0.0.20 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.tool-versions +1 -0
- data/Appraisals +10 -0
- data/README.markdown +28 -14
- data/activerecord_views.gemspec +3 -1
- data/gemfiles/rails4_2.gemfile +1 -0
- data/gemfiles/rails5_0.gemfile +1 -0
- data/gemfiles/rails6_0.gemfile +7 -0
- data/gemfiles/rails6_1.gemfile +7 -0
- data/lib/active_record_views/checksum_cache.rb +9 -5
- data/lib/active_record_views/extension.rb +10 -13
- data/lib/active_record_views/railtie.rb +13 -4
- data/lib/active_record_views/registered_view.rb +7 -2
- data/lib/active_record_views/version.rb +1 -1
- data/lib/active_record_views.rb +35 -6
- data/lib/tasks/active_record_views.rake +70 -16
- data/spec/active_record_views_checksum_cache_spec.rb +10 -6
- data/spec/active_record_views_extension_spec.rb +149 -68
- data/spec/active_record_views_spec.rb +55 -0
- data/spec/internal/Rakefile +5 -0
- 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 +1 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/spec_helper.rb +47 -13
- data/spec/support/silence_warnings.rb +23 -0
- data/spec/tasks_spec.rb +53 -11
- metadata +44 -12
- data/spec/internal/app/models/missing_file_test_model.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67e33c3b91d943b9fec0cf41b263302025ae787d41fe9404724cfd93c0105c73
|
4
|
+
data.tar.gz: 29b11133c94e86c16563083c1176616da9988fe9493fd69e8f2201436edded43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 194ac35e51118b4d57956d083d5bff2fc859c6b1bcc09ccfde564fa3425dade51caa53835676f676b3c26999b138ee81710a5d5d0c605f90fbdaebf718b0e89f
|
7
|
+
data.tar.gz: a611fff391e863d46580cd6dea1c02e95d14660a3e900212e624a162f9a99a7879ddfe6c5be709a902f44930c9c3c02ebf58cde1b67cd46d23bf141816b392d2
|
data/.gitignore
CHANGED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.7
|
data/Appraisals
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
appraise 'rails4_2' do
|
2
2
|
gem 'rails', '~> 4.2.0'
|
3
|
+
gem "pg", "< 0.21"
|
3
4
|
end
|
4
5
|
|
5
6
|
appraise 'rails5_0' do
|
6
7
|
gem 'rails', '~> 5.0.0'
|
8
|
+
gem "pg", "< 1.0"
|
7
9
|
end
|
8
10
|
|
9
11
|
appraise 'rails5_1' do
|
@@ -13,3 +15,11 @@ end
|
|
13
15
|
appraise 'rails5_2' do
|
14
16
|
gem 'rails', '~> 5.2.0'
|
15
17
|
end
|
18
|
+
|
19
|
+
appraise 'rails6_0' do
|
20
|
+
gem 'rails', '~> 6.0.0'
|
21
|
+
end
|
22
|
+
|
23
|
+
appraise 'rails6_1' do
|
24
|
+
gem 'rails', '~> 6.1.0'
|
25
|
+
end
|
data/README.markdown
CHANGED
@@ -23,7 +23,7 @@ gem 'activerecord_views'
|
|
23
23
|
app/models/account.rb:
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
class Account <
|
26
|
+
class Account < ApplicationRecord
|
27
27
|
has_many :transactions
|
28
28
|
|
29
29
|
has_one :account_balance
|
@@ -34,7 +34,7 @@ end
|
|
34
34
|
app/models/transaction.rb:
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
class Transaction <
|
37
|
+
class Transaction < ApplicationRecord
|
38
38
|
belongs_to :account
|
39
39
|
end
|
40
40
|
```
|
@@ -42,7 +42,7 @@ end
|
|
42
42
|
app/models/account_balance.rb:
|
43
43
|
|
44
44
|
```ruby
|
45
|
-
class AccountBalance <
|
45
|
+
class AccountBalance < ApplicationRecord
|
46
46
|
is_view
|
47
47
|
|
48
48
|
belongs_to :account
|
@@ -70,7 +70,7 @@ end
|
|
70
70
|
|
71
71
|
## Dependencies
|
72
72
|
|
73
|
-
You can use
|
73
|
+
You can use a view model from another view model or within SQL blocks in your application code.
|
74
74
|
In order to ensure the model file is loaded (and thus the view is created), you should reference
|
75
75
|
the model class when you use the view rather than using the database table name directly:
|
76
76
|
|
@@ -88,11 +88,11 @@ a safety check which will require you to specify the dependency explicitly if yo
|
|
88
88
|
to another view model:
|
89
89
|
|
90
90
|
```ruby
|
91
|
-
class AccountBalance <
|
91
|
+
class AccountBalance < ApplicationRecord
|
92
92
|
is_view
|
93
93
|
end
|
94
94
|
|
95
|
-
class AccountSummary <
|
95
|
+
class AccountSummary < ApplicationRecord
|
96
96
|
is_view dependencies: [AccountBalance]
|
97
97
|
end
|
98
98
|
```
|
@@ -106,7 +106,7 @@ Materialized views let you cache the output of views. This is useful for views w
|
|
106
106
|
To configure an ActiveRecordViews model as being materialized, pass the `materialized: true` option to `is_view`:
|
107
107
|
|
108
108
|
```ruby
|
109
|
-
class AccountBalance <
|
109
|
+
class AccountBalance < ApplicationRecord
|
110
110
|
is_view materialized: true
|
111
111
|
end
|
112
112
|
```
|
@@ -121,24 +121,34 @@ AccountBalance.refresh_view!
|
|
121
121
|
AccountBalance.view_populated? # => true
|
122
122
|
```
|
123
123
|
|
124
|
-
ActiveRecordViews records when a view was last refreshed. This is often useful for giving users an idea of how
|
124
|
+
ActiveRecordViews records when a view was last refreshed. This is often useful for giving users an idea of how old the cached data is. To retrieve this timestamp, call `.refreshed_at` on the model:
|
125
125
|
|
126
126
|
ActiveRecordViews also has a convenience method called `ensure_populated!` which checks all materialized views in a chain of dependencies have been initially populated. This can be used as a safeguard to lazily populate views on demand. You will probably also setup a schedule to periodically refresh the view data when it gets stale.
|
127
127
|
|
128
128
|
PostgreSQL 9.4 supports refreshing materialized views concurrently. This allows other processes to continue reading old cached data while the view is being updated. To use this feature you must have define a unique index on the materialized view:
|
129
129
|
|
130
|
-
```
|
131
|
-
class AccountBalance <
|
130
|
+
```ruby
|
131
|
+
class AccountBalance < ApplicationRecord
|
132
132
|
is_view materialized: true, unique_columns: %w[account_id]
|
133
133
|
end
|
134
134
|
```
|
135
135
|
|
136
136
|
Note: If your view has a single column as the unique key, you can also tell ActiveRecord about it by adding `self.primary_key = :account_id` in your model file. This is required for features such as `.find` and `.find_each` to work.
|
137
137
|
|
138
|
-
Once you have defined the unique columns for the view,
|
138
|
+
Once you have defined the unique columns for the view, ActiveRecordViews will automatically start refreshing concurrently when you call `refresh_view!`. If you wish to force a non-concurrent refresh, which is typically faster but blocks reads while the view is refreshing, you can then use the `concurrent: false` option:
|
139
139
|
|
140
140
|
```ruby
|
141
|
-
AccountBalance.refresh_view! concurrent:
|
141
|
+
AccountBalance.refresh_view! concurrent: false
|
142
|
+
```
|
143
|
+
|
144
|
+
### Resetting all materialised views
|
145
|
+
|
146
|
+
If you are using [Database Cleaner](https://github.com/DatabaseCleaner/database_cleaner) in your test suite,
|
147
|
+
you probably also want to reset the contents of materialised views for each test run to ensure state
|
148
|
+
does not leak between tests. You can do this with the following in your test suite hooks:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
ActiveRecordViews.reset_materialized_views
|
142
152
|
```
|
143
153
|
|
144
154
|
## Pre-populating views in Rails development mode
|
@@ -153,9 +163,11 @@ Rails.application.eager_load!
|
|
153
163
|
|
154
164
|
## Handling renames/deletions
|
155
165
|
|
156
|
-
ActiveRecordViews tracks database views by their name. When an ActiveRecordViews model is renamed or deleted, there is no longer a link between the model and the associated database table. This means an orphan view
|
166
|
+
ActiveRecordViews tracks database views by their name. When an ActiveRecordViews model is renamed or deleted, there is no longer a link between the model and the associated database table. This means an orphan view would be left in the database.
|
157
167
|
|
158
|
-
|
168
|
+
ActiveRecordViews will automatically drop deleted views while checking for changes in both production mode when migrations are run and in development mode when code reloads (e.g. refreshing the browser).
|
169
|
+
|
170
|
+
You should avoid dropping views using `DROP VIEW` as this will cause the internal state of ActiveRecordViews to become out of sync. If you want to drop an existing view manually you can instead run the following:
|
159
171
|
|
160
172
|
```ruby
|
161
173
|
ActiveRecordViews.drop_view connection, 'account_balances'
|
@@ -167,6 +179,8 @@ Alternatively, all view models can be dropped with the following:
|
|
167
179
|
ActiveRecordViews.drop_all_views connection
|
168
180
|
```
|
169
181
|
|
182
|
+
Dropping all views is most useful when doing significant structual changes. Both methods can be used in migrations where there are incompatible dependency changes that would otherwise error. ActiveRecordViews will automatically recreate views again when it next checks for pending changes (e.g. production migrations or dev reloading).
|
183
|
+
|
170
184
|
## Usage outside of Rails
|
171
185
|
|
172
186
|
When included in a Ruby on Rails project, ActiveRecordViews will automatically detect `.sql` files alongside models in `app/models`.
|
data/activerecord_views.gemspec
CHANGED
@@ -17,10 +17,12 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ['lib']
|
19
19
|
|
20
|
-
gem.add_dependency 'activerecord', ['>= 4.2', '<
|
20
|
+
gem.add_dependency 'activerecord', ['>= 4.2', '< 6.2']
|
21
21
|
|
22
22
|
gem.add_development_dependency 'appraisal'
|
23
23
|
gem.add_development_dependency 'rspec-rails', '>= 2.14'
|
24
|
+
gem.add_development_dependency 'super_diff'
|
24
25
|
gem.add_development_dependency 'combustion', '>= 0.5.1'
|
25
26
|
gem.add_development_dependency 'pg'
|
27
|
+
gem.add_development_dependency 'warning'
|
26
28
|
end
|
data/gemfiles/rails4_2.gemfile
CHANGED
data/gemfiles/rails5_0.gemfile
CHANGED
@@ -13,12 +13,13 @@ module ActiveRecordViews
|
|
13
13
|
end
|
14
14
|
|
15
15
|
if table_exists && !@connection.column_exists?('active_record_views', 'class_name')
|
16
|
-
@connection.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@connection.execute
|
16
|
+
@connection.begin_transaction
|
17
|
+
in_transaction = true
|
18
|
+
|
19
|
+
@connection.select_values('SELECT name FROM active_record_views;').each do |view_name|
|
20
|
+
@connection.execute "DROP VIEW IF EXISTS #{view_name} CASCADE;"
|
21
21
|
end
|
22
|
+
@connection.execute 'DROP TABLE active_record_views;'
|
22
23
|
table_exists = false
|
23
24
|
end
|
24
25
|
|
@@ -33,6 +34,9 @@ module ActiveRecordViews
|
|
33
34
|
unless table_exists
|
34
35
|
@connection.execute "CREATE TABLE active_record_views(name text PRIMARY KEY, class_name text NOT NULL UNIQUE, checksum text NOT NULL, options json NOT NULL DEFAULT '{}', refreshed_at timestamp);"
|
35
36
|
end
|
37
|
+
|
38
|
+
ensure
|
39
|
+
@connection.commit_transaction if in_transaction
|
36
40
|
end
|
37
41
|
|
38
42
|
def get(name)
|
@@ -16,12 +16,6 @@ module ActiveRecordViews
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
def self.currently_migrating?
|
20
|
-
if defined?(Rake) && Rake.respond_to?(:application)
|
21
|
-
Rake.application.top_level_tasks.any? { |task_name| task_name =~ /^db:migrate($|:)/ }
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
19
|
module ClassMethods
|
26
20
|
def is_view(*args)
|
27
21
|
cattr_accessor :view_options
|
@@ -33,11 +27,11 @@ module ActiveRecordViews
|
|
33
27
|
sql ||= begin
|
34
28
|
sql_path = ActiveRecordViews.find_sql_file(self.name.underscore)
|
35
29
|
ActiveRecordViews.register_for_reload self, sql_path
|
36
|
-
ActiveRecordViews.read_sql_file(sql_path)
|
30
|
+
ActiveRecordViews.read_sql_file(self, sql_path)
|
37
31
|
end
|
38
32
|
|
39
33
|
create_args = [self.table_name, self.name, sql, self.view_options]
|
40
|
-
if ActiveRecordViews::Extension.create_enabled
|
34
|
+
if ActiveRecordViews::Extension.create_enabled
|
41
35
|
ActiveRecordViews.create_view self.connection, *create_args
|
42
36
|
else
|
43
37
|
ActiveRecordViews::Extension.create_queue << create_args
|
@@ -47,13 +41,13 @@ module ActiveRecordViews
|
|
47
41
|
def refresh_view!(options = {})
|
48
42
|
options.assert_valid_keys :concurrent
|
49
43
|
|
50
|
-
concurrent = case options
|
51
|
-
when
|
44
|
+
concurrent = case options.fetch(:concurrent, :auto)
|
45
|
+
when false
|
52
46
|
false
|
53
47
|
when true
|
54
48
|
true
|
55
49
|
when :auto
|
56
|
-
view_populated? && ActiveRecordViews.supports_concurrent_refresh?(connection)
|
50
|
+
view_options.fetch(:unique_columns, nil) && view_populated? && ActiveRecordViews.supports_concurrent_refresh?(connection)
|
57
51
|
else
|
58
52
|
raise ArgumentError, 'invalid concurrent option'
|
59
53
|
end
|
@@ -61,6 +55,7 @@ module ActiveRecordViews
|
|
61
55
|
connection.transaction do
|
62
56
|
connection.execute "REFRESH MATERIALIZED VIEW#{' CONCURRENTLY' if concurrent} #{connection.quote_table_name self.table_name};"
|
63
57
|
connection.execute "UPDATE active_record_views SET refreshed_at = current_timestamp AT TIME ZONE 'UTC' WHERE name = #{connection.quote self.table_name};"
|
58
|
+
connection.clear_query_cache
|
64
59
|
end
|
65
60
|
end
|
66
61
|
|
@@ -89,9 +84,11 @@ module ActiveRecordViews
|
|
89
84
|
WHERE name = #{connection.quote self.table_name};
|
90
85
|
SQL
|
91
86
|
|
92
|
-
if value
|
93
|
-
ActiveSupport::TimeZone['UTC'].parse(value)
|
87
|
+
if value.is_a? String
|
88
|
+
value = ActiveSupport::TimeZone['UTC'].parse(value)
|
94
89
|
end
|
90
|
+
|
91
|
+
value
|
95
92
|
end
|
96
93
|
|
97
94
|
def ensure_populated!
|
@@ -2,16 +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
7
|
ActiveRecordViews::Extension.create_enabled = !Rails.env.production?
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
unless app.config.cache_classes
|
11
|
+
if app.respond_to?(:reloader)
|
12
|
+
app.reloader.before_class_unload do
|
13
13
|
ActiveRecordViews.reload_stale_views!
|
14
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
|
15
24
|
end
|
16
25
|
end
|
17
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
|
data/lib/active_record_views.rb
CHANGED
@@ -28,9 +28,9 @@ module ActiveRecordViews
|
|
28
28
|
raise "could not find #{name}.sql"
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.read_sql_file(sql_path)
|
31
|
+
def self.read_sql_file(model_class, sql_path)
|
32
32
|
if sql_path.end_with?('.erb')
|
33
|
-
ERB.new(File.read(sql_path)).result
|
33
|
+
ERB.new(File.read(sql_path)).result(model_class.instance_eval { binding })
|
34
34
|
else
|
35
35
|
File.read(sql_path)
|
36
36
|
end
|
@@ -54,7 +54,7 @@ module ActiveRecordViews
|
|
54
54
|
states[temp_connection] = states[connection] = temp_connection
|
55
55
|
yield temp_connection
|
56
56
|
ensure
|
57
|
-
connection.pool.checkin temp_connection
|
57
|
+
connection.pool.checkin temp_connection if temp_connection
|
58
58
|
states[temp_connection] = states[connection] = nil
|
59
59
|
end
|
60
60
|
else
|
@@ -154,6 +154,7 @@ module ActiveRecordViews
|
|
154
154
|
|
155
155
|
def self.drop_all_views(base_connection)
|
156
156
|
without_transaction base_connection do |connection|
|
157
|
+
ActiveRecordViews::ChecksumCache.new(connection)
|
157
158
|
names = Set.new connection.select_values('SELECT name FROM active_record_views;')
|
158
159
|
|
159
160
|
func = lambda do |name|
|
@@ -173,17 +174,17 @@ module ActiveRecordViews
|
|
173
174
|
options.assert_valid_keys :dependencies, :materialized, :unique_columns
|
174
175
|
sql = sql.sub(/;\s*\z/, '')
|
175
176
|
|
176
|
-
if options
|
177
|
+
if options.fetch(:materialized, false)
|
177
178
|
connection.execute "CREATE MATERIALIZED VIEW #{connection.quote_table_name name} AS #{sql} WITH NO DATA;"
|
178
179
|
else
|
179
180
|
connection.execute "CREATE VIEW #{connection.quote_table_name name} AS #{sql};"
|
180
181
|
end
|
181
182
|
|
182
|
-
if options
|
183
|
+
if unique_columns = options.fetch(:unique_columns, nil)
|
183
184
|
connection.execute <<-SQL.squish
|
184
185
|
CREATE UNIQUE INDEX #{connection.quote_table_name "#{name}_pkey"}
|
185
186
|
ON #{connection.quote_table_name name}(
|
186
|
-
#{
|
187
|
+
#{unique_columns.map { |column_name| connection.quote_table_name(column_name) }.join(', ')}
|
187
188
|
);
|
188
189
|
SQL
|
189
190
|
end
|
@@ -320,14 +321,42 @@ module ActiveRecordViews
|
|
320
321
|
if registered_view.stale?
|
321
322
|
registered_view.reload!
|
322
323
|
end
|
324
|
+
if registered_view.dead?
|
325
|
+
self.registered_views.delete registered_view
|
326
|
+
end
|
323
327
|
end
|
324
328
|
end
|
325
329
|
|
326
330
|
def self.drop_unregistered_views!
|
327
331
|
connection = ActiveRecord::Base.connection
|
332
|
+
ActiveRecordViews::ChecksumCache.new(connection)
|
328
333
|
|
329
334
|
connection.select_rows('SELECT name, class_name FROM active_record_views')
|
330
335
|
.reject { |name, class_name| Object.const_defined? class_name }
|
331
336
|
.each { |name, class_name| ActiveRecordViews.drop_view connection, name }
|
332
337
|
end
|
338
|
+
|
339
|
+
def self.reset_materialized_views
|
340
|
+
connection = ActiveRecord::Base.connection
|
341
|
+
ActiveRecordViews::ChecksumCache.new(connection)
|
342
|
+
|
343
|
+
connection.transaction do
|
344
|
+
materialized_views = connection.select_values(<<~SQL)
|
345
|
+
SELECT name
|
346
|
+
FROM active_record_views
|
347
|
+
WHERE (options ->> 'materialized')::boolean
|
348
|
+
AND refreshed_at IS NOT NULL
|
349
|
+
SQL
|
350
|
+
|
351
|
+
materialized_views.each do |view|
|
352
|
+
connection.execute(<<~SQL)
|
353
|
+
REFRESH MATERIALIZED VIEW #{view} WITH NO DATA;
|
354
|
+
SQL
|
355
|
+
end
|
356
|
+
|
357
|
+
connection.execute(<<~SQL)
|
358
|
+
UPDATE active_record_views SET refreshed_at = NULL;
|
359
|
+
SQL
|
360
|
+
end
|
361
|
+
end
|
333
362
|
end
|
@@ -6,33 +6,87 @@ Rake::Task['db:migrate'].enhance do
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
-
|
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
|
10
12
|
table_exists = if Rails::VERSION::MAJOR >= 5
|
11
13
|
ActiveRecord::Base.connection.data_source_exists?('active_record_views')
|
12
14
|
else
|
13
15
|
ActiveRecord::Base.connection.table_exists?('active_record_views')
|
14
16
|
end
|
15
17
|
|
16
|
-
if
|
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
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
tasks.send(:class_for_adapter, config.fetch('adapter')).new(config)
|
23
|
-
pg_tasks = tasks.send(:class_for_adapter, config.fetch('adapter')).new(config)
|
24
|
-
pg_tasks.send(:set_psql_env)
|
25
|
+
filename = case
|
26
|
+
when tasks.respond_to?(:dump_filename)
|
27
|
+
tasks.dump_filename('primary')
|
25
28
|
else
|
26
|
-
|
27
|
-
set_psql_env(config)
|
29
|
+
tasks.schema_file
|
28
30
|
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
+
# Substitute out any timestamps that were dumped from the active_record_views table
|
66
|
+
#
|
67
|
+
# Before:
|
68
|
+
#
|
69
|
+
# COPY public.active_record_views (name, class_name, checksum, options, refreshed_at) FROM stdin;
|
70
|
+
# test_view TestView 42364a017b73ef516a0eca9827e6fa00623257ee {"dependencies":[]} 2021-10-26 02:49:12.247494
|
71
|
+
# \.
|
72
|
+
#
|
73
|
+
# After:
|
74
|
+
#
|
75
|
+
# COPY public.active_record_views (name, class_name, checksum, options, refreshed_at) FROM stdin;
|
76
|
+
# test_view TestView 42364a017b73ef516a0eca9827e6fa00623257ee {"dependencies":[]} \N
|
77
|
+
# \.
|
78
|
+
active_record_views_dump_content = active_record_views_dump.read
|
79
|
+
if active_record_views_dump_content !~ /^COPY public.active_record_views \(.+, refreshed_at\) FROM stdin;$/
|
80
|
+
raise 'refreshed_at is not final column'
|
81
|
+
end
|
82
|
+
active_record_views_dump_content.gsub!(/\t\d\d\d\d-\d\d-\d\d.*$/, "\t\\N")
|
33
83
|
|
34
|
-
|
35
|
-
|
84
|
+
File.open filename, 'a' do |io|
|
85
|
+
io.puts active_record_views_dump_content
|
86
|
+
end
|
87
|
+
ensure
|
88
|
+
active_record_views_dump.close
|
89
|
+
active_record_views_dump.unlink
|
36
90
|
end
|
37
91
|
end
|
38
92
|
end
|
@@ -49,18 +49,22 @@ describe ActiveRecordViews::ChecksumCache do
|
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'drops existing managed views recreates the table' do
|
52
|
-
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ABEGIN\z/).once.and_call_original
|
53
|
-
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP VIEW IF EXISTS test_view CASCADE;\z/).once.and_call_original
|
54
|
-
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ADROP TABLE active_record_views;\z/).once.and_call_original
|
55
|
-
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACREATE TABLE active_record_views/).once.and_call_original
|
56
|
-
expect(ActiveRecord::Base.connection).to receive(:execute).with(/\ACOMMIT\z/).once.and_call_original
|
57
|
-
|
58
52
|
expect(connection.column_exists?('active_record_views', 'class_name')).to eq false
|
59
53
|
expect(ActiveRecordViews.view_exists?(connection, 'test_view')).to eq true
|
60
54
|
expect(ActiveRecordViews.view_exists?(connection, 'other_view')).to eq true
|
61
55
|
|
56
|
+
sql_statements.clear
|
57
|
+
|
62
58
|
ActiveRecordViews::ChecksumCache.new(connection)
|
63
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
|
+
|
64
68
|
expect(connection.column_exists?('active_record_views', 'class_name')).to eq true
|
65
69
|
expect(ActiveRecordViews.view_exists?(connection, 'test_view')).to eq false
|
66
70
|
expect(ActiveRecordViews.view_exists?(connection, 'other_view')).to eq true
|