scenic 1.4.1 → 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +78 -0
- data/.hound.yml +2 -4
- data/.rubocop.yml +129 -0
- data/{NEWS.md → CHANGELOG.md} +80 -15
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +7 -9
- data/Gemfile +13 -1
- data/LICENSE.txt +1 -1
- data/README.md +22 -20
- data/Rakefile +2 -2
- data/SECURITY.md +14 -0
- data/bin/setup +9 -4
- data/lib/generators/scenic/materializable.rb +9 -0
- data/lib/generators/scenic/model/model_generator.rb +2 -2
- data/lib/generators/scenic/view/USAGE +1 -0
- 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 +18 -8
- data/lib/scenic/adapters/postgres.rb +19 -6
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
- data/lib/scenic/adapters/postgres/views.rb +10 -1
- data/lib/scenic/command_recorder.rb +2 -1
- data/lib/scenic/command_recorder/statement_arguments.rb +9 -1
- data/lib/scenic/configuration.rb +1 -1
- data/lib/scenic/definition.rb +1 -1
- data/lib/scenic/schema_dumper.rb +2 -2
- data/lib/scenic/statements.rb +24 -6
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +1 -2
- data/scenic.gemspec +21 -23
- data/spec/acceptance/user_manages_views_spec.rb +2 -1
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/config/database.yml +5 -0
- data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
- data/spec/generators/scenic/view/view_generator_spec.rb +10 -4
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
- data/spec/scenic/adapters/postgres_spec.rb +23 -3
- data/spec/scenic/command_recorder_spec.rb +15 -1
- data/spec/scenic/definition_spec.rb +7 -1
- data/spec/scenic/schema_dumper_spec.rb +17 -2
- data/spec/scenic/statements_spec.rb +48 -13
- data/spec/spec_helper.rb +1 -1
- data/spec/support/generator_spec_setup.rb +1 -1
- metadata +22 -40
- data/.travis.yml +0 -44
- data/Appraisals +0 -33
- data/bin/appraisal +0 -16
- data/gemfiles/rails40.gemfile +0 -8
- data/gemfiles/rails41.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails50.gemfile +0 -8
- data/gemfiles/rails51.gemfile +0 -8
- data/gemfiles/rails_edge.gemfile +0 -8
data/CONTRIBUTING.md
CHANGED
@@ -1,24 +1,22 @@
|
|
1
1
|
# Contributing
|
2
2
|
|
3
3
|
We love contributions from everyone. By participating in this project, you
|
4
|
-
agree to abide by
|
4
|
+
agree to abide by our [code of conduct].
|
5
5
|
|
6
|
-
[code of conduct]:
|
7
|
-
|
8
|
-
We expect everyone to follow the code of conduct anywhere in thoughtbot's
|
9
|
-
project codebases, issue trackers, chatrooms, and mailing lists.
|
6
|
+
[code of conduct]: CODE_OF_CONDUCT.md
|
10
7
|
|
11
8
|
## Contributing Code
|
12
9
|
|
13
10
|
1. Fork the repository.
|
14
11
|
2. Run `bin/setup`, which will install dependencies and create the dummy
|
15
12
|
application database.
|
16
|
-
3. Run `
|
17
|
-
|
18
|
-
4. Make your change with new passing tests, following
|
13
|
+
3. Run `rake` to verify that the tests pass against the version of Rails you are
|
14
|
+
running locally.
|
15
|
+
4. Make your change with new passing tests, following existing style.
|
19
16
|
5. Write a [good commit message], push your fork, and submit a pull request.
|
17
|
+
6. CI will run the test suite on all configured versions of Ruby and Rails.
|
18
|
+
Address any failures.
|
20
19
|
|
21
|
-
[style guide]: https://github.com/thoughtbot/guides/tree/master/style
|
22
20
|
[good commit message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
23
21
|
|
24
22
|
Others will give constructive feedback. This is a time for discussion and
|
data/Gemfile
CHANGED
@@ -1,4 +1,16 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in scenic.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
rails_version = ENV.fetch("RAILS_VERSION", "6.0")
|
7
|
+
|
8
|
+
if rails_version == "master"
|
9
|
+
rails_constraint = { github: "rails/rails" }
|
10
|
+
else
|
11
|
+
rails_constraint = "~> #{rails_version}.0"
|
12
|
+
end
|
13
|
+
|
14
|
+
gem "rails", rails_constraint
|
15
|
+
gem "sprockets", "< 4.0.0"
|
16
|
+
gem "pg", "~> 1.1"
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Scenic
|
2
2
|
|
3
|
-
![Scenic Landscape](https://images.
|
3
|
+
![Scenic Landscape](https://user-images.githubusercontent.com/152152/49344534-a8817480-f646-11e8-8431-3d95d349c070.png)
|
4
4
|
|
5
|
-
[![Build Status](https://
|
6
|
-
[![
|
7
|
-
[![
|
5
|
+
[![Build Status](https://github.com/scenic-views/scenic/workflows/CI/badge.svg)](https://github.com/scenic-views/scenic/actions?query=workflow%3ACI+branch%3Amaster)
|
6
|
+
[![Documentation Quality](http://inch-ci.org/github/scenic-views/scenic.svg?branch=master)](http://inch-ci.org/github/scenic-views/scenic)
|
7
|
+
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
|
8
8
|
|
9
9
|
Scenic adds methods to `ActiveRecord::Migration` to create and manage database
|
10
10
|
views in Rails.
|
@@ -21,6 +21,12 @@ Scenic ships with support for PostgreSQL. The adapter is configurable (see
|
|
21
21
|
`Scenic::Configuration`) and has a minimal interface (see
|
22
22
|
`Scenic::Adapters::Postgres`) that other gems can provide.
|
23
23
|
|
24
|
+
## So how do I install this?
|
25
|
+
|
26
|
+
If you're using Postgres, Add `gem "scenic"` to your Gemfile and run `bundle
|
27
|
+
install`. If you're using something other than Postgres, check out the available
|
28
|
+
[third party adapters](https://github.com/scenic-views/scenic#faqs).
|
29
|
+
|
24
30
|
## Great, how do I create a view?
|
25
31
|
|
26
32
|
You've got this great idea for a view you'd like to call `search_results`. You
|
@@ -88,7 +94,7 @@ when some of those views may be materialized and take a long time to recreate.
|
|
88
94
|
|
89
95
|
You can use `replace_view` to generate a CREATE OR REPLACE VIEW SQL statement.
|
90
96
|
|
91
|
-
See
|
97
|
+
See Postgres documentation on how this works:
|
92
98
|
http://www.postgresql.org/docs/current/static/sql-createview.html
|
93
99
|
|
94
100
|
To start replacing a view run the generator like for a regular change:
|
@@ -129,7 +135,7 @@ ActiveRecord or ARel queries. As far as ActiveRecord is concerned, a view is
|
|
129
135
|
no different than a table.
|
130
136
|
|
131
137
|
```ruby
|
132
|
-
class SearchResult <
|
138
|
+
class SearchResult < ApplicationRecord
|
133
139
|
belongs_to :searchable, polymorphic: true
|
134
140
|
|
135
141
|
# this isn't strictly necessary, but it will prevent
|
@@ -212,7 +218,7 @@ You can get around these issues by setting the primary key column on your Rails
|
|
212
218
|
model like so:
|
213
219
|
|
214
220
|
```ruby
|
215
|
-
class People <
|
221
|
+
class People < ApplicationRecord
|
216
222
|
self.primary_key = :my_unique_identifier_field
|
217
223
|
end
|
218
224
|
```
|
@@ -247,21 +253,17 @@ We are aware of the following existing adapter libraries for Scenic which may
|
|
247
253
|
meet your needs:
|
248
254
|
|
249
255
|
* [scenic_sqlite_adapter](https://github.com/pdebelak/scenic_sqlite_adapter)
|
250
|
-
* [scenic-mysql_adapter](https://github.com/EmpaticoOrg/scenic-mysql_adapter
|
256
|
+
* [scenic-mysql_adapter](https://github.com/EmpaticoOrg/scenic-mysql_adapter)
|
257
|
+
* [scenic-sqlserver-adapter](https://github.com/ClickMechanic/scenic_sqlserver_adapter)
|
258
|
+
* [scenic-oracle_adapter](https://github.com/cdinger/scenic-oracle_adapter)
|
259
|
+
|
260
|
+
Please note that the maintainers of Scenic make no assertions about the
|
261
|
+
quality or security of the above adapters.
|
251
262
|
|
252
263
|
## About
|
253
264
|
|
254
|
-
Scenic is maintained by [Derek Prior]
|
255
|
-
|
256
|
-
thoughtbot, inc.
|
265
|
+
Scenic is maintained by [Derek Prior], [Caleb Hearth], and you, our
|
266
|
+
contributors.
|
257
267
|
|
258
268
|
[Derek Prior]: http://prioritized.net
|
259
|
-
[Caleb
|
260
|
-
|
261
|
-
![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg)
|
262
|
-
|
263
|
-
We love open source software! See [our other projects][community] or [hire
|
264
|
-
us][hire] to help build your product.
|
265
|
-
|
266
|
-
[community]: https://thoughtbot.com/community?utm_source=github
|
267
|
-
[hire]: https://thoughtbot.com/hire-us?utm_source=github
|
269
|
+
[Caleb Hearth]: http://calebhearth.com
|
data/Rakefile
CHANGED
data/SECURITY.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
Scenic maintainenance is a volunteer effort. We will do our best to fix
|
6
|
+
forward but do not offer backported fixes. As such, the only "supported" version of Scenic is whichever was most recently released.
|
7
|
+
|
8
|
+
## Reporting a Vulnerability
|
9
|
+
|
10
|
+
Please report any discovered security vulnerabilities to Scenic's primary
|
11
|
+
volunteer maintainers, derekprior@gmail.com and caleb@calebhearth.com.
|
12
|
+
|
13
|
+
We will respond as soon as possible with any follow-up questions or details
|
14
|
+
on how we plan to handle the issue.
|
data/bin/setup
CHANGED
@@ -2,12 +2,17 @@
|
|
2
2
|
|
3
3
|
set -e
|
4
4
|
|
5
|
+
# CI-specific setup
|
6
|
+
if [ -n "$GITHUB_ACTIONS" ]; then
|
7
|
+
bundle config path vendor/bundle
|
8
|
+
bundle config jobs 4
|
9
|
+
bundle config retry 3
|
10
|
+
git config --global user.name 'GitHub Actions'
|
11
|
+
git config --global user.email 'github-actions@example.com'
|
12
|
+
fi
|
13
|
+
|
5
14
|
gem install bundler --conservative
|
6
15
|
bundle check || bundle install
|
7
16
|
|
8
|
-
if [ -z "$CI" ]; then
|
9
|
-
bundle exec appraisal install
|
10
|
-
fi
|
11
|
-
|
12
17
|
bundle exec rake dummy:db:drop
|
13
18
|
bundle exec rake dummy:db:create
|
@@ -10,6 +10,11 @@ module Scenic
|
|
10
10
|
required: false,
|
11
11
|
desc: "Makes the view materialized",
|
12
12
|
default: false
|
13
|
+
class_option :no_data,
|
14
|
+
type: :boolean,
|
15
|
+
required: false,
|
16
|
+
desc: "Adds WITH NO DATA when materialized view creates/updates",
|
17
|
+
default: false
|
13
18
|
end
|
14
19
|
|
15
20
|
private
|
@@ -17,6 +22,10 @@ module Scenic
|
|
17
22
|
def materialized?
|
18
23
|
options[:materialized]
|
19
24
|
end
|
25
|
+
|
26
|
+
def no_data?
|
27
|
+
options[:no_data]
|
28
|
+
end
|
20
29
|
end
|
21
30
|
end
|
22
31
|
end
|
@@ -8,7 +8,7 @@ module Scenic
|
|
8
8
|
# @api private
|
9
9
|
class ModelGenerator < Rails::Generators::NamedBase
|
10
10
|
include Scenic::Generators::Materializable
|
11
|
-
source_root File.expand_path("
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
13
|
def invoke_rails_model_generator
|
14
14
|
invoke "model",
|
@@ -35,7 +35,7 @@ module Scenic
|
|
35
35
|
|
36
36
|
def evaluate_template(source)
|
37
37
|
source = File.expand_path(find_in_source_paths(source.to_s))
|
38
|
-
context = instance_eval("binding")
|
38
|
+
context = instance_eval("binding", __FILE__, __LINE__)
|
39
39
|
ERB.new(
|
40
40
|
::File.binread(source),
|
41
41
|
nil,
|
@@ -4,7 +4,7 @@ class <%= migration_class_name %> < <%= activerecord_migration_class %>
|
|
4
4
|
update_view <%= formatted_plural_name %>,
|
5
5
|
version: <%= version %>,
|
6
6
|
revert_to_version: <%= previous_version %>,
|
7
|
-
materialized: true
|
7
|
+
materialized: <%= no_data? ? "{ no_data: true }" : true %>
|
8
8
|
<%- else -%>
|
9
9
|
update_view <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
|
10
10
|
<%- end -%>
|
@@ -8,7 +8,7 @@ module Scenic
|
|
8
8
|
class ViewGenerator < Rails::Generators::NamedBase
|
9
9
|
include Rails::Generators::Migration
|
10
10
|
include Scenic::Generators::Materializable
|
11
|
-
source_root File.expand_path("
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
13
|
def create_views_directory
|
14
14
|
unless views_directory_path.exist?
|
@@ -56,7 +56,7 @@ module Scenic
|
|
56
56
|
|
57
57
|
def migration_class_name
|
58
58
|
if creating_new_view?
|
59
|
-
"Create#{class_name.
|
59
|
+
"Create#{class_name.tr('.', '').pluralize}"
|
60
60
|
else
|
61
61
|
"Update#{class_name.pluralize}ToVersion#{version}"
|
62
62
|
end
|
@@ -73,8 +73,14 @@ module Scenic
|
|
73
73
|
|
74
74
|
private
|
75
75
|
|
76
|
+
alias singular_name file_name
|
77
|
+
|
78
|
+
def file_name
|
79
|
+
super.tr(".", "_")
|
80
|
+
end
|
81
|
+
|
76
82
|
def views_directory_path
|
77
|
-
@views_directory_path ||= Rails.root.join(
|
83
|
+
@views_directory_path ||= Rails.root.join("db", "views")
|
78
84
|
end
|
79
85
|
|
80
86
|
def version_regex
|
@@ -82,7 +88,7 @@ module Scenic
|
|
82
88
|
end
|
83
89
|
|
84
90
|
def creating_new_view?
|
85
|
-
previous_version
|
91
|
+
previous_version.zero?
|
86
92
|
end
|
87
93
|
|
88
94
|
def definition
|
@@ -93,10 +99,6 @@ module Scenic
|
|
93
99
|
Scenic::Definition.new(plural_file_name, previous_version)
|
94
100
|
end
|
95
101
|
|
96
|
-
def plural_file_name
|
97
|
-
@plural_file_name ||= file_name.pluralize.gsub(".", "_")
|
98
|
-
end
|
99
|
-
|
100
102
|
def destroying?
|
101
103
|
behavior == :revoke
|
102
104
|
end
|
@@ -109,6 +111,14 @@ module Scenic
|
|
109
111
|
end
|
110
112
|
end
|
111
113
|
|
114
|
+
def create_view_options
|
115
|
+
if materialized?
|
116
|
+
", materialized: #{no_data? ? '{ no_data: true }' : true}"
|
117
|
+
else
|
118
|
+
""
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
112
122
|
def destroying_initial_view?
|
113
123
|
destroying? && version == 1
|
114
124
|
end
|
@@ -122,6 +122,9 @@ module Scenic
|
|
122
122
|
#
|
123
123
|
# @param name The name of the materialized view to create
|
124
124
|
# @param sql_definition The SQL schema that defines the materialized view.
|
125
|
+
# @param no_data [Boolean] Default: false. Set to true to create
|
126
|
+
# materialized view without running the associated query. You will need
|
127
|
+
# to perform a non-concurrent refresh to populate with data.
|
125
128
|
#
|
126
129
|
# This is typically called in a migration via {Statements#create_view}.
|
127
130
|
#
|
@@ -129,9 +132,14 @@ module Scenic
|
|
129
132
|
# in use does not support materialized views.
|
130
133
|
#
|
131
134
|
# @return [void]
|
132
|
-
def create_materialized_view(name, sql_definition)
|
135
|
+
def create_materialized_view(name, sql_definition, no_data: false)
|
133
136
|
raise_unless_materialized_views_supported
|
134
|
-
|
137
|
+
|
138
|
+
execute <<-SQL
|
139
|
+
CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS
|
140
|
+
#{sql_definition.rstrip.chomp(';')}
|
141
|
+
#{'WITH NO DATA' if no_data};
|
142
|
+
SQL
|
135
143
|
end
|
136
144
|
|
137
145
|
# Updates a materialized view in the database.
|
@@ -144,17 +152,20 @@ module Scenic
|
|
144
152
|
#
|
145
153
|
# @param name The name of the view to update
|
146
154
|
# @param sql_definition The SQL schema for the updated view.
|
155
|
+
# @param no_data [Boolean] Default: false. Set to true to create
|
156
|
+
# materialized view without running the associated query. You will need
|
157
|
+
# to perform a non-concurrent refresh to populate with data.
|
147
158
|
#
|
148
159
|
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
149
160
|
# in use does not support materialized views.
|
150
161
|
#
|
151
162
|
# @return [void]
|
152
|
-
def update_materialized_view(name, sql_definition)
|
163
|
+
def update_materialized_view(name, sql_definition, no_data: false)
|
153
164
|
raise_unless_materialized_views_supported
|
154
165
|
|
155
166
|
IndexReapplication.new(connection: connection).on(name) do
|
156
167
|
drop_materialized_view(name)
|
157
|
-
create_materialized_view(name, sql_definition)
|
168
|
+
create_materialized_view(name, sql_definition, no_data: no_data)
|
158
169
|
end
|
159
170
|
end
|
160
171
|
|
@@ -198,8 +209,9 @@ module Scenic
|
|
198
209
|
# @return [void]
|
199
210
|
def refresh_materialized_view(name, concurrently: false, cascade: false)
|
200
211
|
raise_unless_materialized_views_supported
|
212
|
+
|
201
213
|
if cascade
|
202
|
-
refresh_dependencies_for(name)
|
214
|
+
refresh_dependencies_for(name, concurrently: concurrently)
|
203
215
|
end
|
204
216
|
|
205
217
|
if concurrently
|
@@ -231,11 +243,12 @@ module Scenic
|
|
231
243
|
end
|
232
244
|
end
|
233
245
|
|
234
|
-
def refresh_dependencies_for(name)
|
246
|
+
def refresh_dependencies_for(name, concurrently: false)
|
235
247
|
Scenic::Adapters::Postgres::RefreshDependencies.call(
|
236
248
|
name,
|
237
249
|
self,
|
238
250
|
connection,
|
251
|
+
concurrently: concurrently,
|
239
252
|
)
|
240
253
|
end
|
241
254
|
end
|
@@ -2,25 +2,29 @@ module Scenic
|
|
2
2
|
module Adapters
|
3
3
|
class Postgres
|
4
4
|
class RefreshDependencies
|
5
|
-
def self.call(name, adapter, connection)
|
6
|
-
new(name, adapter, connection).call
|
5
|
+
def self.call(name, adapter, connection, concurrently: false)
|
6
|
+
new(name, adapter, connection, concurrently: concurrently).call
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize(name, adapter, connection)
|
9
|
+
def initialize(name, adapter, connection, concurrently:)
|
10
10
|
@name = name
|
11
11
|
@adapter = adapter
|
12
12
|
@connection = connection
|
13
|
+
@concurrently = concurrently
|
13
14
|
end
|
14
15
|
|
15
16
|
def call
|
16
17
|
dependencies.each do |dependency|
|
17
|
-
adapter.refresh_materialized_view(
|
18
|
+
adapter.refresh_materialized_view(
|
19
|
+
dependency,
|
20
|
+
concurrently: concurrently,
|
21
|
+
)
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
25
|
private
|
22
26
|
|
23
|
-
attr_reader :name, :adapter, :connection
|
27
|
+
attr_reader :name, :adapter, :connection, :concurrently
|
24
28
|
|
25
29
|
class DependencyParser
|
26
30
|
def initialize(raw_dependencies, view_to_refresh)
|
@@ -46,10 +50,20 @@ module Scenic
|
|
46
50
|
def to_sorted_array
|
47
51
|
dependency_hash = parse_to_hash(raw_dependencies)
|
48
52
|
sorted_arr = tsort(dependency_hash)
|
53
|
+
|
49
54
|
idx = sorted_arr.find_index do |dep|
|
50
|
-
|
55
|
+
if view_to_refresh.to_s.include?(".")
|
56
|
+
dep == view_to_refresh.to_s
|
57
|
+
else
|
58
|
+
dep.ends_with?(".#{view_to_refresh}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if idx.present?
|
63
|
+
sorted_arr[0...idx]
|
64
|
+
else
|
65
|
+
[]
|
51
66
|
end
|
52
|
-
sorted_arr[0...idx]
|
53
67
|
end
|
54
68
|
|
55
69
|
private
|