scenic 1.3.0 → 1.4.0
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/.travis.yml +15 -2
- data/Appraisals +21 -14
- data/NEWS.md +34 -0
- data/README.md +12 -3
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +1 -0
- data/gemfiles/rails42_with_fg_rails.gemfile +10 -0
- data/gemfiles/rails50.gemfile +2 -7
- data/gemfiles/rails51.gemfile +8 -0
- data/gemfiles/rails_edge.gemfile +8 -0
- data/lib/generators/scenic/model/model_generator.rb +6 -1
- data/lib/generators/scenic/model/templates/model.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
- data/lib/generators/scenic/view/view_generator.rb +8 -0
- data/lib/scenic/adapters/postgres.rb +14 -2
- data/lib/scenic/adapters/postgres/index_reapplication.rb +1 -1
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +102 -0
- data/lib/scenic/adapters/postgres/views.rb +7 -2
- data/lib/scenic/schema_dumper.rb +1 -1
- data/lib/scenic/statements.rb +27 -10
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +1 -2
- data/spec/acceptance/user_manages_views_spec.rb +1 -0
- data/spec/acceptance_helper.rb +2 -1
- data/spec/dummy/Rakefile +7 -0
- data/spec/integration/revert_spec.rb +11 -3
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +42 -0
- data/spec/scenic/adapters/postgres_spec.rb +9 -0
- data/spec/scenic/schema_dumper_spec.rb +63 -2
- data/spec/scenic/statements_spec.rb +39 -7
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5bf3b09e1e09c0e1256111963adf6345e7c48a95
|
4
|
+
data.tar.gz: e3a945e6bb156e49837cc945c120096c9664cda9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64fee8693cc22dd425049c19efde6f47ca19cf42a3002dd1a3b89f511e6fd9801a1f5d040988819457551e8db54e7b4d03662af373489bad2b442e3078ddaff0
|
7
|
+
data.tar.gz: bf1ca04759e648a4422289e1c2b84b13598956c5470518b354a114b802c2b8aff5788d65a34d2fefb5d5a7339ed9f06b185615efda273ba12f42902b093911df
|
data/.travis.yml
CHANGED
@@ -17,15 +17,28 @@ notifications:
|
|
17
17
|
- false
|
18
18
|
sudo: false
|
19
19
|
rvm:
|
20
|
-
- 2.
|
21
|
-
- 2.
|
20
|
+
- 2.4.1
|
21
|
+
- 2.3.4
|
22
|
+
- 2.2.7
|
22
23
|
- 2.1.8
|
23
24
|
gemfile:
|
24
25
|
- gemfiles/rails40.gemfile
|
25
26
|
- gemfiles/rails41.gemfile
|
26
27
|
- gemfiles/rails42.gemfile
|
27
28
|
- gemfiles/rails50.gemfile
|
29
|
+
- gemfiles/rails51.gemfile
|
30
|
+
- gemfiles/rails_edge.gemfile
|
28
31
|
matrix:
|
29
32
|
exclude:
|
33
|
+
- rvm: 2.4.1
|
34
|
+
gemfile: gemfiles/rails40.gemfile
|
35
|
+
- rvm: 2.4.1
|
36
|
+
gemfile: gemfiles/rails41.gemfile
|
30
37
|
- rvm: 2.1.8
|
31
38
|
gemfile: gemfiles/rails50.gemfile
|
39
|
+
- rvm: 2.1.8
|
40
|
+
gemfile: gemfiles/rails51.gemfile
|
41
|
+
- rvm: 2.1.8
|
42
|
+
gemfile: gemfiles/rails_edge.gemfile
|
43
|
+
allow_failures:
|
44
|
+
- gemfile: gemfiles/rails_edge.gemfile
|
data/Appraisals
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
if RUBY_VERSION < "2.4.0"
|
2
|
+
appraise "rails40" do
|
3
|
+
gem "activerecord", "~> 4.0.0"
|
4
|
+
gem "railties", "~> 4.0.0"
|
5
|
+
end
|
5
6
|
|
6
|
-
appraise "rails41" do
|
7
|
-
|
8
|
-
|
7
|
+
appraise "rails41" do
|
8
|
+
gem "activerecord", "~> 4.1.0"
|
9
|
+
gem "railties", "~> 4.1.0"
|
10
|
+
end
|
9
11
|
end
|
10
12
|
|
11
13
|
appraise "rails42" do
|
@@ -15,12 +17,17 @@ end
|
|
15
17
|
|
16
18
|
if RUBY_VERSION > "2.2.0"
|
17
19
|
appraise "rails50" do
|
18
|
-
gem "
|
19
|
-
gem "
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
gem "
|
24
|
-
gem "
|
20
|
+
gem "activerecord", "~> 5.0.0"
|
21
|
+
gem "railties", "~> 5.0.0"
|
22
|
+
end
|
23
|
+
|
24
|
+
appraise "rails51" do
|
25
|
+
gem "activerecord", "~> 5.1.0"
|
26
|
+
gem "railties", "~> 5.1.0"
|
27
|
+
end
|
28
|
+
|
29
|
+
appraise "rails-edge" do
|
30
|
+
gem "rails", git: "https://github.com/rails/rails"
|
31
|
+
gem "arel", git: "https://github.com/rails/arel"
|
25
32
|
end
|
26
33
|
end
|
data/NEWS.md
CHANGED
@@ -5,6 +5,40 @@ changelog, see the [CHANGELOG] for each version via the version links.
|
|
5
5
|
|
6
6
|
[CHANGELOG]: https://github.com/thoughtbot/scenic/commits/master
|
7
7
|
|
8
|
+
## [1.4.0] - May 11, 2017
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- `refresh_materialized_view` now accepts a `cascade` option, which defaults to
|
13
|
+
`false`. Setting this option to `true` will refresh any materialized views the
|
14
|
+
current view depends on first, ensuring the view being refreshed has the most
|
15
|
+
up-to-date information.
|
16
|
+
- `sql_definition` argument is now supported when using `update_view`.
|
17
|
+
|
18
|
+
### Fixed
|
19
|
+
|
20
|
+
- View migrations created under Rails 5 and newer will no longer result in
|
21
|
+
warnings.
|
22
|
+
- `ar_internal_metadata` is no longer included in the schema dump for Rails 5
|
23
|
+
and newer apps.
|
24
|
+
- Using the `scenic:model` generator will no longer create a fixture or factory.
|
25
|
+
|
26
|
+
## [1.3.0] - May 27, 2016
|
27
|
+
|
28
|
+
### Added
|
29
|
+
- Add `replace_view` migration statement, which issues `CREATE OR REPLACE
|
30
|
+
VIEW` rather than `CREATE VIEW` or `DROP VIEW` and `CREATE VIEW`.
|
31
|
+
- Schema-qualify views outside the 'public' namespace, such as
|
32
|
+
`scenic.searches`
|
33
|
+
|
34
|
+
### Fixed
|
35
|
+
* Singularize generated model name when injecting into class.
|
36
|
+
Previously, pluralized names would issue a warning and Scenic would
|
37
|
+
attempt to insert model code into the pluralized model file.
|
38
|
+
* Convert shell-based smoke tests to RSpec syntax.
|
39
|
+
|
40
|
+
[1.3.0]: https://github.com/thoughtbot/scenic/compare/v1.2.0...v1.3.0
|
41
|
+
|
8
42
|
## [1.2.0] - February 5, 2016
|
9
43
|
|
10
44
|
### Added
|
data/README.md
CHANGED
@@ -173,14 +173,23 @@ refreshes:
|
|
173
173
|
|
174
174
|
```ruby
|
175
175
|
def self.refresh
|
176
|
-
Scenic.database.refresh_materialized_view(table_name, concurrently: false)
|
176
|
+
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
|
177
177
|
end
|
178
178
|
```
|
179
179
|
|
180
180
|
This will perform a non-concurrent refresh, locking the view for selects until
|
181
181
|
the refresh is complete. You can avoid locking the view by passing
|
182
182
|
`concurrently: true` but this requires both PostgreSQL 9.4 and your view to have
|
183
|
-
at least one unique index that covers all rows.
|
183
|
+
at least one unique index that covers all rows. You can add or update indexes for
|
184
|
+
materialized views using table migration methods (e.g. `add_index table_name`)
|
185
|
+
and these will be automatically re-applied when views are updated.
|
186
|
+
|
187
|
+
The `cascade` option is to refresh materialized views that depend on other
|
188
|
+
materialized views. For example, say you have materialized view A, which selects
|
189
|
+
data from materialized view B. To get the most up to date information in view A
|
190
|
+
you would need to refresh view B first, then right after refresh view A. If you
|
191
|
+
would like this cascading refresh of materialized views, set `cascade: true`
|
192
|
+
when you refresh your materialized view.
|
184
193
|
|
185
194
|
## I don't need this view anymore. Make it go away.
|
186
195
|
|
@@ -243,7 +252,7 @@ thoughtbot, inc.
|
|
243
252
|
[Derek Prior]: http://prioritized.net
|
244
253
|
[Caleb Thompson]: http://calebthompson.io
|
245
254
|
|
246
|
-

|
247
256
|
|
248
257
|
We love open source software! See [our other projects][community] or [hire
|
249
258
|
us][hire] to help build your product.
|
data/bin/rake
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rake' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
CHANGED
data/gemfiles/rails50.gemfile
CHANGED
@@ -2,12 +2,7 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "
|
6
|
-
gem "
|
7
|
-
gem "rspec-support", :github => "rspec/rspec-support"
|
8
|
-
gem "rspec-core", :github => "rspec/rspec-core"
|
9
|
-
gem "rspec-mocks", :github => "rspec/rspec-mocks"
|
10
|
-
gem "rspec-expectations", :github => "rspec/rspec-expectations"
|
11
|
-
gem "rspec", :github => "rspec/rspec"
|
5
|
+
gem "activerecord", "~> 5.0.0"
|
6
|
+
gem "railties", "~> 5.0.0"
|
12
7
|
|
13
8
|
gemspec :path => "../"
|
@@ -11,7 +11,12 @@ module Scenic
|
|
11
11
|
source_root File.expand_path("../templates", __FILE__)
|
12
12
|
|
13
13
|
def invoke_rails_model_generator
|
14
|
-
invoke "model",
|
14
|
+
invoke "model",
|
15
|
+
[file_path.singularize],
|
16
|
+
options.merge(
|
17
|
+
fixture_replacement: false,
|
18
|
+
migration: false,
|
19
|
+
)
|
15
20
|
end
|
16
21
|
|
17
22
|
def inject_model_methods
|
@@ -61,6 +61,14 @@ module Scenic
|
|
61
61
|
"Update#{class_name.pluralize}ToVersion#{version}"
|
62
62
|
end
|
63
63
|
end
|
64
|
+
|
65
|
+
def activerecord_migration_class
|
66
|
+
if ActiveRecord::Migration.respond_to?(:current_version)
|
67
|
+
"ActiveRecord::Migration[5.0]"
|
68
|
+
else
|
69
|
+
"ActiveRecord::Migration"
|
70
|
+
end
|
71
|
+
end
|
64
72
|
end
|
65
73
|
|
66
74
|
private
|
@@ -3,6 +3,7 @@ require_relative "postgres/errors"
|
|
3
3
|
require_relative "postgres/index_reapplication"
|
4
4
|
require_relative "postgres/indexes"
|
5
5
|
require_relative "postgres/views"
|
6
|
+
require_relative "postgres/refresh_dependencies"
|
6
7
|
|
7
8
|
module Scenic
|
8
9
|
# Scenic database adapters.
|
@@ -192,11 +193,14 @@ module Scenic
|
|
192
193
|
# @example Non-concurrent refresh
|
193
194
|
# Scenic.database.refresh_materialized_view(:search_results)
|
194
195
|
# @example Concurrent refresh
|
195
|
-
# Scenic.database.refresh_materialized_view(:posts,
|
196
|
+
# Scenic.database.refresh_materialized_view(:posts, concurrently: true)
|
196
197
|
#
|
197
198
|
# @return [void]
|
198
|
-
def refresh_materialized_view(name, concurrently: false)
|
199
|
+
def refresh_materialized_view(name, concurrently: false, cascade: false)
|
199
200
|
raise_unless_materialized_views_supported
|
201
|
+
if cascade
|
202
|
+
refresh_dependencies_for(name)
|
203
|
+
end
|
200
204
|
|
201
205
|
if concurrently
|
202
206
|
raise_unless_concurrent_refresh_supported
|
@@ -226,6 +230,14 @@ module Scenic
|
|
226
230
|
raise ConcurrentRefreshesNotSupportedError
|
227
231
|
end
|
228
232
|
end
|
233
|
+
|
234
|
+
def refresh_dependencies_for(name)
|
235
|
+
Scenic::Adapters::Postgres::RefreshDependencies.call(
|
236
|
+
name,
|
237
|
+
self,
|
238
|
+
connection,
|
239
|
+
)
|
240
|
+
end
|
229
241
|
end
|
230
242
|
end
|
231
243
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Scenic
|
2
2
|
module Adapters
|
3
3
|
class Postgres
|
4
|
-
#
|
4
|
+
# Updating a materialized view causes the view to be dropped and
|
5
5
|
# recreated. This causes any associated indexes to be dropped as well.
|
6
6
|
# This object can be used to capture the existing indexes before the drop
|
7
7
|
# and then reapply appropriate indexes following the create.
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
class RefreshDependencies
|
5
|
+
def self.call(name, adapter, connection)
|
6
|
+
new(name, adapter, connection).call
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(name, adapter, connection)
|
10
|
+
@name = name
|
11
|
+
@adapter = adapter
|
12
|
+
@connection = connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
dependencies.each do |dependency|
|
17
|
+
adapter.refresh_materialized_view(dependency)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :name, :adapter, :connection
|
24
|
+
|
25
|
+
class DependencyParser
|
26
|
+
def initialize(raw_dependencies, view_to_refresh)
|
27
|
+
@raw_dependencies = raw_dependencies
|
28
|
+
@view_to_refresh = view_to_refresh
|
29
|
+
end
|
30
|
+
|
31
|
+
# We're given an array from the SQL query that looks kind of like this
|
32
|
+
# [["view_name", "{'dependency_1', 'dependency_2'}"]]
|
33
|
+
#
|
34
|
+
# We need to parse that into a more easy to understand data type so we
|
35
|
+
# can use the Tsort module from the Standard Library to topologically
|
36
|
+
# sort those out so we can refresh in the correct order, so we parse
|
37
|
+
# that raw data into a hash.
|
38
|
+
#
|
39
|
+
# Then, once Tsort has worked it magic, we're given a sorted 1-D array
|
40
|
+
# ["dependency_1", "dependency_2", "view_name"]
|
41
|
+
#
|
42
|
+
# So we then need to slice off just the bit leading up to the view
|
43
|
+
# that we're refreshing, so we find where in the topologically sorted
|
44
|
+
# array our given view is, and return all the dependencies up to that
|
45
|
+
# point.
|
46
|
+
def to_sorted_array
|
47
|
+
dependency_hash = parse_to_hash(raw_dependencies)
|
48
|
+
sorted_arr = tsort(dependency_hash)
|
49
|
+
idx = sorted_arr.find_index do |dep|
|
50
|
+
dep.include?(view_to_refresh.to_s)
|
51
|
+
end
|
52
|
+
sorted_arr[0...idx]
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_reader :raw_dependencies, :view_to_refresh
|
58
|
+
|
59
|
+
def parse_to_hash(dependency_rows)
|
60
|
+
dependency_rows.each_with_object({}) do |row, hash|
|
61
|
+
formatted_dependencies = row.last.tr("{}", "").split(",")
|
62
|
+
formatted_dependencies.each do |dependency|
|
63
|
+
hash[dependency] = [] unless hash[dependency]
|
64
|
+
end
|
65
|
+
hash[row.first] = formatted_dependencies
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def tsort(hash)
|
70
|
+
each_node = lambda { |&b| hash.each_key(&b) }
|
71
|
+
each_child = lambda { |n, &b| hash[n].each(&b) }
|
72
|
+
TSort.tsort(each_node, each_child)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
DEPENDENCY_SQL = <<-SQL.freeze
|
77
|
+
SELECT rewrite_namespace.nspname || '.' || class_for_rewrite.relname AS materialized_view,
|
78
|
+
array_agg(depend_namespace.nspname || '.' || class_for_depend.relname) AS depends_on
|
79
|
+
FROM pg_rewrite AS rewrite
|
80
|
+
JOIN pg_class AS class_for_rewrite ON rewrite.ev_class = class_for_rewrite.oid
|
81
|
+
JOIN pg_depend AS depend ON rewrite.oid = depend.objid
|
82
|
+
JOIN pg_class AS class_for_depend ON depend.refobjid = class_for_depend.oid
|
83
|
+
JOIN pg_namespace AS rewrite_namespace ON rewrite_namespace.oid = class_for_rewrite.relnamespace
|
84
|
+
JOIN pg_namespace AS depend_namespace ON depend_namespace.oid = class_for_depend.relnamespace
|
85
|
+
WHERE class_for_depend.relkind = 'm'
|
86
|
+
AND class_for_rewrite.relkind = 'm'
|
87
|
+
AND class_for_depend.relname != class_for_rewrite.relname
|
88
|
+
GROUP BY class_for_rewrite.relname, rewrite_namespace.nspname
|
89
|
+
ORDER BY class_for_rewrite.relname;
|
90
|
+
SQL
|
91
|
+
|
92
|
+
private_constant "DependencyParser"
|
93
|
+
private_constant "DEPENDENCY_SQL"
|
94
|
+
|
95
|
+
def dependencies
|
96
|
+
raw_dependency_info = connection.select_rows(DEPENDENCY_SQL)
|
97
|
+
DependencyParser.new(raw_dependency_info, name).to_sorted_array
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -43,9 +43,9 @@ module Scenic
|
|
43
43
|
namespace, viewname = result.values_at "namespace", "viewname"
|
44
44
|
|
45
45
|
if namespace != "public"
|
46
|
-
namespaced_viewname = "#{namespace}.#{viewname}"
|
46
|
+
namespaced_viewname = "#{pg_identifier(namespace)}.#{pg_identifier(viewname)}"
|
47
47
|
else
|
48
|
-
namespaced_viewname = viewname
|
48
|
+
namespaced_viewname = pg_identifier(viewname)
|
49
49
|
end
|
50
50
|
|
51
51
|
Scenic::View.new(
|
@@ -54,6 +54,11 @@ module Scenic
|
|
54
54
|
materialized: result["kind"] == "m",
|
55
55
|
)
|
56
56
|
end
|
57
|
+
|
58
|
+
def pg_identifier(name)
|
59
|
+
return name if name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
60
|
+
PGconn.quote_ident(name)
|
61
|
+
end
|
57
62
|
end
|
58
63
|
end
|
59
64
|
end
|
data/lib/scenic/schema_dumper.rb
CHANGED
@@ -27,7 +27,7 @@ module Scenic
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
unless ActiveRecord::SchemaDumper.
|
30
|
+
unless ActiveRecord::SchemaDumper.private_instance_methods(false).include?(:ignored?)
|
31
31
|
# This method will be present in Rails 4.2.0 and can be removed then.
|
32
32
|
def ignored?(table_name)
|
33
33
|
["schema_migrations", ignore_tables].flatten.any? do |ignored|
|
data/lib/scenic/statements.rb
CHANGED
@@ -6,9 +6,9 @@ module Scenic
|
|
6
6
|
# @param name [String, Symbol] The name of the database view.
|
7
7
|
# @param version [Fixnum] The version number of the view, used to find the
|
8
8
|
# definition file in `db/views`. This defaults to `1` if not provided.
|
9
|
-
# @param sql_definition [String] The SQL query for the view schema.
|
10
|
-
# `
|
11
|
-
#
|
9
|
+
# @param sql_definition [String] The SQL query for the view schema. An error
|
10
|
+
# will be raised if `sql_definition` and `version` are both set,
|
11
|
+
# as they are mutually exclusive.
|
12
12
|
# @param materialized [Boolean] Set to true to create a materialized view.
|
13
13
|
# Defaults to false.
|
14
14
|
# @return The database response from executing the create statement.
|
@@ -21,14 +21,18 @@ module Scenic
|
|
21
21
|
# SELECT * FROM users WHERE users.active = 't'
|
22
22
|
# SQL
|
23
23
|
#
|
24
|
-
def create_view(name, version:
|
25
|
-
if version.
|
24
|
+
def create_view(name, version: nil, sql_definition: nil, materialized: false)
|
25
|
+
if version.present? && sql_definition.present?
|
26
26
|
raise(
|
27
27
|
ArgumentError,
|
28
|
-
"
|
28
|
+
"sql_definition and version cannot both be set",
|
29
29
|
)
|
30
30
|
end
|
31
31
|
|
32
|
+
if version.blank? && sql_definition.blank?
|
33
|
+
version = 1
|
34
|
+
end
|
35
|
+
|
32
36
|
sql_definition ||= definition(name, version)
|
33
37
|
|
34
38
|
if materialized
|
@@ -66,6 +70,9 @@ module Scenic
|
|
66
70
|
#
|
67
71
|
# @param name [String, Symbol] The name of the database view.
|
68
72
|
# @param version [Fixnum] The version number of the view.
|
73
|
+
# @param sql_definition [String] The SQL query for the view schema. An error
|
74
|
+
# will be raised if `sql_definition` and `version` are both set,
|
75
|
+
# as they are mutually exclusive.
|
69
76
|
# @param revert_to_version [Fixnum] The version number to rollback to on
|
70
77
|
# `rake db rollback`
|
71
78
|
# @param materialized [Boolean] True if updating a materialized view.
|
@@ -75,12 +82,22 @@ module Scenic
|
|
75
82
|
# @example
|
76
83
|
# update_view :engagement_reports, version: 3, revert_to_version: 2
|
77
84
|
#
|
78
|
-
def update_view(name, version: nil, revert_to_version: nil, materialized: false)
|
79
|
-
if version.blank?
|
80
|
-
raise
|
85
|
+
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false)
|
86
|
+
if version.blank? && sql_definition.blank?
|
87
|
+
raise(
|
88
|
+
ArgumentError,
|
89
|
+
"sql_definition or version must be specified",
|
90
|
+
)
|
81
91
|
end
|
82
92
|
|
83
|
-
|
93
|
+
if version.present? && sql_definition.present?
|
94
|
+
raise(
|
95
|
+
ArgumentError,
|
96
|
+
"sql_definition and version cannot both be set",
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
sql_definition ||= definition(name, version)
|
84
101
|
|
85
102
|
if materialized
|
86
103
|
Scenic.database.update_materialized_view(name, sql_definition)
|
data/lib/scenic/version.rb
CHANGED
data/lib/scenic/view.rb
CHANGED
@@ -43,10 +43,9 @@ module Scenic
|
|
43
43
|
# @api private
|
44
44
|
def to_schema
|
45
45
|
materialized_option = materialized ? "materialized: true, " : ""
|
46
|
-
safe_to_symbolize_name = name.include?(".") ? "'#{name}'" : name
|
47
46
|
|
48
47
|
<<-DEFINITION
|
49
|
-
create_view
|
48
|
+
create_view #{name.inspect}, #{materialized_option} sql_definition: <<-\SQL
|
50
49
|
#{definition.indent(2)}
|
51
50
|
SQL
|
52
51
|
|
@@ -51,6 +51,7 @@ describe "User manages views" do
|
|
51
51
|
|
52
52
|
it "handles plural view names gracefully during generation" do
|
53
53
|
successfully "rails generate scenic:model search_results --materialized"
|
54
|
+
successfully "rails destroy scenic:model search_results --materialized"
|
54
55
|
end
|
55
56
|
|
56
57
|
def successfully(command)
|
data/spec/acceptance_helper.rb
CHANGED
@@ -22,7 +22,8 @@ RSpec.configure do |config|
|
|
22
22
|
config.after(:suite) do
|
23
23
|
Dir.chdir("spec/dummy") do
|
24
24
|
system <<-CMD
|
25
|
-
|
25
|
+
echo &&
|
26
|
+
rake db:environment:set db:drop db:create &&
|
26
27
|
git add -A &&
|
27
28
|
git reset --hard HEAD 1>/dev/null &&
|
28
29
|
rm -rf .git/ 1>/dev/null
|
data/spec/dummy/Rakefile
CHANGED
@@ -4,3 +4,10 @@
|
|
4
4
|
require File.expand_path('../config/application', __FILE__)
|
5
5
|
|
6
6
|
Rails.application.load_tasks
|
7
|
+
|
8
|
+
unless Rake::Task.task_defined?('db:environment:set')
|
9
|
+
desc 'dummy task for rails versions where this task does not exist'
|
10
|
+
task 'db:environment:set' do
|
11
|
+
#no op
|
12
|
+
end
|
13
|
+
end
|
@@ -29,7 +29,7 @@ describe "Reverting scenic schema statements", :db do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def migration_for_create
|
32
|
-
Class.new(
|
32
|
+
Class.new(migration_class) do
|
33
33
|
def change
|
34
34
|
create_view :greetings
|
35
35
|
end
|
@@ -37,7 +37,7 @@ describe "Reverting scenic schema statements", :db do
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def migration_for_drop
|
40
|
-
Class.new(
|
40
|
+
Class.new(migration_class) do
|
41
41
|
def change
|
42
42
|
drop_view :greetings, revert_to_version: 1
|
43
43
|
end
|
@@ -45,13 +45,21 @@ describe "Reverting scenic schema statements", :db do
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def migration_for_update
|
48
|
-
Class.new(
|
48
|
+
Class.new(migration_class) do
|
49
49
|
def change
|
50
50
|
update_view :greetings, version: 2, revert_to_version: 1
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
def migration_class
|
56
|
+
if Rails::VERSION::MAJOR >= 5
|
57
|
+
::ActiveRecord::Migration[5.0]
|
58
|
+
else
|
59
|
+
::ActiveRecord::Migration
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
55
63
|
def run_migration(migration, directions)
|
56
64
|
silence_stream(STDOUT) do
|
57
65
|
Array.wrap(directions).each do |direction|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Scenic
|
4
|
+
module Adapters
|
5
|
+
describe Postgres::RefreshDependencies, :db do
|
6
|
+
it "refreshes dependecies in the correct order" do
|
7
|
+
adapter = Postgres.new
|
8
|
+
|
9
|
+
adapter.create_materialized_view(
|
10
|
+
"first",
|
11
|
+
"SELECT text 'hi' AS greeting",
|
12
|
+
)
|
13
|
+
|
14
|
+
adapter.create_materialized_view(
|
15
|
+
"second",
|
16
|
+
"SELECT * from first",
|
17
|
+
)
|
18
|
+
|
19
|
+
adapter.create_materialized_view(
|
20
|
+
"third",
|
21
|
+
"SELECT * from first UNION SELECT * from second",
|
22
|
+
)
|
23
|
+
|
24
|
+
adapter.create_materialized_view(
|
25
|
+
"fourth",
|
26
|
+
"SELECT * from third",
|
27
|
+
)
|
28
|
+
|
29
|
+
expect(adapter).to receive(:refresh_materialized_view).
|
30
|
+
with("public.first").ordered
|
31
|
+
|
32
|
+
expect(adapter).to receive(:refresh_materialized_view).
|
33
|
+
with("public.second").ordered
|
34
|
+
|
35
|
+
expect(adapter).to receive(:refresh_materialized_view).
|
36
|
+
with("public.third").ordered
|
37
|
+
|
38
|
+
described_class.call(:fourth, adapter, ActiveRecord::Base.connection)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -100,6 +100,15 @@ module Scenic
|
|
100
100
|
.to raise_error err
|
101
101
|
end
|
102
102
|
|
103
|
+
it "can refresh the views dependencies first" do
|
104
|
+
connection = double("Connection").as_null_object
|
105
|
+
connectable = double("Connectable", connection: connection)
|
106
|
+
adapter = Postgres.new(connectable)
|
107
|
+
expect(Scenic::Adapters::Postgres::RefreshDependencies).
|
108
|
+
to receive(:call).with(:tests, adapter, connection)
|
109
|
+
adapter.refresh_materialized_view(:tests, cascade: true)
|
110
|
+
end
|
111
|
+
|
103
112
|
context "refreshing concurrently" do
|
104
113
|
it "raises descriptive error if concurrent refresh is not possible" do
|
105
114
|
adapter = Postgres.new
|
@@ -2,6 +2,10 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
class Search < ActiveRecord::Base; end
|
4
4
|
|
5
|
+
class SearchInAHaystack < ActiveRecord::Base
|
6
|
+
self.table_name = '"search in a haystack"'
|
7
|
+
end
|
8
|
+
|
5
9
|
describe Scenic::SchemaDumper, :db do
|
6
10
|
it "dumps a create_view for a view in the database" do
|
7
11
|
view_definition = "SELECT 'needle'::text AS haystack"
|
@@ -11,7 +15,7 @@ describe Scenic::SchemaDumper, :db do
|
|
11
15
|
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
12
16
|
|
13
17
|
output = stream.string
|
14
|
-
expect(output).to include
|
18
|
+
expect(output).to include 'create_view "searches"'
|
15
19
|
expect(output).to include view_definition
|
16
20
|
|
17
21
|
Search.connection.drop_view :searches
|
@@ -31,9 +35,66 @@ describe Scenic::SchemaDumper, :db do
|
|
31
35
|
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
32
36
|
|
33
37
|
output = stream.string
|
34
|
-
expect(output).to include
|
38
|
+
expect(output).to include 'create_view "scenic.searches",'
|
35
39
|
|
36
40
|
Search.connection.drop_view :'scenic.searches'
|
37
41
|
end
|
38
42
|
end
|
43
|
+
|
44
|
+
it "ignores tables internal to Rails" do
|
45
|
+
view_definition = "SELECT 'needle'::text AS haystack"
|
46
|
+
Search.connection.create_view :searches, sql_definition: view_definition
|
47
|
+
stream = StringIO.new
|
48
|
+
|
49
|
+
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
50
|
+
|
51
|
+
output = stream.string
|
52
|
+
|
53
|
+
expect(output).to include 'create_view "searches"'
|
54
|
+
expect(output).not_to include "ar_internal_metadata"
|
55
|
+
expect(output).not_to include "schema_migrations"
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with views using unexpected characters in name" do
|
59
|
+
it "dumps a create_view for a view in the database" do
|
60
|
+
view_definition = "SELECT 'needle'::text AS haystack"
|
61
|
+
Search.connection.create_view '"search in a haystack"', sql_definition: view_definition
|
62
|
+
stream = StringIO.new
|
63
|
+
|
64
|
+
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
65
|
+
|
66
|
+
output = stream.string
|
67
|
+
expect(output).to include 'create_view "\"search in a haystack\"",'
|
68
|
+
expect(output).to include view_definition
|
69
|
+
|
70
|
+
Search.connection.drop_view :'"search in a haystack"'
|
71
|
+
|
72
|
+
silence_stream(STDOUT) { eval(output) }
|
73
|
+
|
74
|
+
expect(SearchInAHaystack.take.haystack).to eq "needle"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "with views using unexpected characters, name including namespace" do
|
79
|
+
it "dumps a create_view for a view in the database" do
|
80
|
+
view_definition = "SELECT 'needle'::text AS haystack"
|
81
|
+
Search.connection.execute(
|
82
|
+
"CREATE SCHEMA scenic; SET search_path TO scenic, public")
|
83
|
+
Search.connection.create_view 'scenic."search in a haystack"',
|
84
|
+
sql_definition: view_definition
|
85
|
+
stream = StringIO.new
|
86
|
+
|
87
|
+
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
88
|
+
|
89
|
+
output = stream.string
|
90
|
+
expect(output).to include 'create_view "scenic.\"search in a haystack\"",'
|
91
|
+
expect(output).to include view_definition
|
92
|
+
|
93
|
+
Search.connection.drop_view :'scenic."search in a haystack"'
|
94
|
+
|
95
|
+
silence_stream(STDOUT) { eval(output) }
|
96
|
+
|
97
|
+
expect(SearchInAHaystack.take.haystack).to eq "needle"
|
98
|
+
end
|
99
|
+
end
|
39
100
|
end
|
@@ -30,9 +30,22 @@ module Scenic
|
|
30
30
|
.with(:views, sql_definition)
|
31
31
|
end
|
32
32
|
|
33
|
-
it "
|
33
|
+
it "creates version 1 of the view if neither version nor sql_defintion are provided" do
|
34
|
+
version = 1
|
35
|
+
definition_stub = instance_double("Definition", to_sql: "foo")
|
36
|
+
allow(Definition).to receive(:new).
|
37
|
+
with(:views, version).
|
38
|
+
and_return(definition_stub)
|
39
|
+
|
40
|
+
connection.create_view :views
|
41
|
+
|
42
|
+
expect(Scenic.database).to have_received(:create_view).
|
43
|
+
with(:views, definition_stub.to_sql)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "raises an error if both version and sql_defintion are provided" do
|
34
47
|
expect do
|
35
|
-
connection.create_view :foo, version:
|
48
|
+
connection.create_view :foo, version: 1, sql_definition: "a defintion"
|
36
49
|
end.to raise_error ArgumentError
|
37
50
|
end
|
38
51
|
end
|
@@ -77,6 +90,15 @@ module Scenic
|
|
77
90
|
.with(:name, definition.to_sql)
|
78
91
|
end
|
79
92
|
|
93
|
+
it "updates a view from a text definition" do
|
94
|
+
sql_definition = "a defintion"
|
95
|
+
|
96
|
+
connection.update_view(:name, sql_definition: sql_definition)
|
97
|
+
|
98
|
+
expect(Scenic.database).to have_received(:update_view).
|
99
|
+
with(:name, sql_definition)
|
100
|
+
end
|
101
|
+
|
80
102
|
it "updates the materialized view in the database" do
|
81
103
|
definition = instance_double("Definition", to_sql: "definition")
|
82
104
|
allow(Definition).to receive(:new)
|
@@ -85,13 +107,23 @@ module Scenic
|
|
85
107
|
|
86
108
|
connection.update_view(:name, version: 3, materialized: true)
|
87
109
|
|
88
|
-
expect(Scenic.database).to have_received(:update_materialized_view)
|
89
|
-
|
110
|
+
expect(Scenic.database).to have_received(:update_materialized_view).
|
111
|
+
with(:name, definition.to_sql)
|
90
112
|
end
|
91
113
|
|
92
|
-
it "raises an error if not supplied a version" do
|
93
|
-
expect { connection.update_view :views }
|
94
|
-
|
114
|
+
it "raises an error if not supplied a version or sql_defintion" do
|
115
|
+
expect { connection.update_view :views }.to raise_error(
|
116
|
+
ArgumentError,
|
117
|
+
/sql_definition or version must be specified/)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "raises an error if both version and sql_defintion are provided" do
|
121
|
+
expect do
|
122
|
+
connection.update_view(
|
123
|
+
:views,
|
124
|
+
version: 1,
|
125
|
+
sql_definition: "a defintion")
|
126
|
+
end.to raise_error ArgumentError, /cannot both be set/
|
95
127
|
end
|
96
128
|
end
|
97
129
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scenic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Derek Prior
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2017-05-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: appraisal
|
@@ -201,12 +201,17 @@ files:
|
|
201
201
|
- README.md
|
202
202
|
- Rakefile
|
203
203
|
- bin/appraisal
|
204
|
+
- bin/rake
|
205
|
+
- bin/rspec
|
204
206
|
- bin/setup
|
205
207
|
- bin/yard
|
206
208
|
- gemfiles/rails40.gemfile
|
207
209
|
- gemfiles/rails41.gemfile
|
208
210
|
- gemfiles/rails42.gemfile
|
211
|
+
- gemfiles/rails42_with_fg_rails.gemfile
|
209
212
|
- gemfiles/rails50.gemfile
|
213
|
+
- gemfiles/rails51.gemfile
|
214
|
+
- gemfiles/rails_edge.gemfile
|
210
215
|
- lib/generators/scenic/generators.rb
|
211
216
|
- lib/generators/scenic/materializable.rb
|
212
217
|
- lib/generators/scenic/model/USAGE
|
@@ -222,6 +227,7 @@ files:
|
|
222
227
|
- lib/scenic/adapters/postgres/errors.rb
|
223
228
|
- lib/scenic/adapters/postgres/index_reapplication.rb
|
224
229
|
- lib/scenic/adapters/postgres/indexes.rb
|
230
|
+
- lib/scenic/adapters/postgres/refresh_dependencies.rb
|
225
231
|
- lib/scenic/adapters/postgres/views.rb
|
226
232
|
- lib/scenic/command_recorder.rb
|
227
233
|
- lib/scenic/command_recorder/statement_arguments.rb
|
@@ -252,6 +258,7 @@ files:
|
|
252
258
|
- spec/generators/scenic/view/view_generator_spec.rb
|
253
259
|
- spec/integration/revert_spec.rb
|
254
260
|
- spec/scenic/adapters/postgres/connection_spec.rb
|
261
|
+
- spec/scenic/adapters/postgres/refresh_dependencies_spec.rb
|
255
262
|
- spec/scenic/adapters/postgres/views_spec.rb
|
256
263
|
- spec/scenic/adapters/postgres_spec.rb
|
257
264
|
- spec/scenic/command_recorder/statement_arguments_spec.rb
|
@@ -283,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
283
290
|
version: '0'
|
284
291
|
requirements: []
|
285
292
|
rubyforge_project:
|
286
|
-
rubygems_version: 2.
|
293
|
+
rubygems_version: 2.4.8
|
287
294
|
signing_key:
|
288
295
|
specification_version: 4
|
289
296
|
summary: Support for database views in Rails migrations
|
@@ -306,6 +313,7 @@ test_files:
|
|
306
313
|
- spec/generators/scenic/view/view_generator_spec.rb
|
307
314
|
- spec/integration/revert_spec.rb
|
308
315
|
- spec/scenic/adapters/postgres/connection_spec.rb
|
316
|
+
- spec/scenic/adapters/postgres/refresh_dependencies_spec.rb
|
309
317
|
- spec/scenic/adapters/postgres/views_spec.rb
|
310
318
|
- spec/scenic/adapters/postgres_spec.rb
|
311
319
|
- spec/scenic/command_recorder/statement_arguments_spec.rb
|
@@ -317,4 +325,3 @@ test_files:
|
|
317
325
|
- spec/spec_helper.rb
|
318
326
|
- spec/support/generator_spec_setup.rb
|
319
327
|
- spec/support/view_definition_helpers.rb
|
320
|
-
has_rdoc:
|