scenic 1.4.1 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +78 -0
  3. data/.hound.yml +2 -4
  4. data/.rubocop.yml +129 -0
  5. data/{NEWS.md → CHANGELOG.md} +80 -15
  6. data/CODE_OF_CONDUCT.md +76 -0
  7. data/CONTRIBUTING.md +7 -9
  8. data/Gemfile +13 -1
  9. data/LICENSE.txt +1 -1
  10. data/README.md +22 -20
  11. data/Rakefile +2 -2
  12. data/SECURITY.md +14 -0
  13. data/bin/setup +9 -4
  14. data/lib/generators/scenic/materializable.rb +9 -0
  15. data/lib/generators/scenic/model/model_generator.rb +2 -2
  16. data/lib/generators/scenic/view/USAGE +1 -0
  17. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  18. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
  19. data/lib/generators/scenic/view/view_generator.rb +18 -8
  20. data/lib/scenic/adapters/postgres.rb +19 -6
  21. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
  22. data/lib/scenic/adapters/postgres/views.rb +10 -1
  23. data/lib/scenic/command_recorder.rb +2 -1
  24. data/lib/scenic/command_recorder/statement_arguments.rb +9 -1
  25. data/lib/scenic/configuration.rb +1 -1
  26. data/lib/scenic/definition.rb +1 -1
  27. data/lib/scenic/schema_dumper.rb +2 -2
  28. data/lib/scenic/statements.rb +24 -6
  29. data/lib/scenic/version.rb +1 -1
  30. data/lib/scenic/view.rb +1 -2
  31. data/scenic.gemspec +21 -23
  32. data/spec/acceptance/user_manages_views_spec.rb +2 -1
  33. data/spec/dummy/app/models/application_record.rb +5 -0
  34. data/spec/dummy/config/database.yml +5 -0
  35. data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
  36. data/spec/generators/scenic/view/view_generator_spec.rb +10 -4
  37. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
  38. data/spec/scenic/adapters/postgres_spec.rb +23 -3
  39. data/spec/scenic/command_recorder_spec.rb +15 -1
  40. data/spec/scenic/definition_spec.rb +7 -1
  41. data/spec/scenic/schema_dumper_spec.rb +17 -2
  42. data/spec/scenic/statements_spec.rb +48 -13
  43. data/spec/spec_helper.rb +1 -1
  44. data/spec/support/generator_spec_setup.rb +1 -1
  45. metadata +22 -40
  46. data/.travis.yml +0 -44
  47. data/Appraisals +0 -33
  48. data/bin/appraisal +0 -16
  49. data/gemfiles/rails40.gemfile +0 -8
  50. data/gemfiles/rails41.gemfile +0 -8
  51. data/gemfiles/rails42.gemfile +0 -8
  52. data/gemfiles/rails50.gemfile +0 -8
  53. data/gemfiles/rails51.gemfile +0 -8
  54. data/gemfiles/rails_edge.gemfile +0 -8
@@ -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 the thoughtbot [code of conduct].
4
+ agree to abide by our [code of conduct].
5
5
 
6
- [code of conduct]: https://thoughtbot.com/open-source-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 `bin/appraisal rake` to verify that the tests pass against all
17
- supported versions of Rails.
18
- 4. Make your change with new passing tests, following the [style guide].
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 'https://rubygems.org'
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"
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2016 Derek Prior, Caleb Thompson, and thoughtbot.
1
+ Copyright (c) 2014-2020 Derek Prior, Caleb Hearth, and thoughtbot.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Scenic
2
2
 
3
- ![Scenic Landscape](https://images.thoughtbot.com/announcing-scenic--versioned-database-views-for-rails/MRUcPsxrTGCeWKyE59Zg_landscape.png)
3
+ ![Scenic Landscape](https://user-images.githubusercontent.com/152152/49344534-a8817480-f646-11e8-8431-3d95d349c070.png)
4
4
 
5
- [![Build Status](https://travis-ci.org/thoughtbot/scenic.svg)](https://travis-ci.org/thoughtbot/scenic)
6
- [![Code Climate](https://codeclimate.com/repos/53c9736269568066a3000c35/badges/85aa9b19f3037252c55d/gpa.svg)](https://codeclimate.com/repos/53c9736269568066a3000c35/feed)
7
- [![Documentation Quality](http://inch-ci.org/github/thoughtbot/scenic.svg?branch=master)](http://inch-ci.org/github/thoughtbot/scenic)
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 postgresql documentation on how this works:
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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] and [Caleb Thompson], funded by
255
- thoughtbot, inc. The names and logos for thoughtbot are trademarks of
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 Thompson]: http://calebthompson.io
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
@@ -1,5 +1,5 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
@@ -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("../templates", __FILE__)
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,
@@ -6,6 +6,7 @@ Description:
6
6
  and a migration to replace the old version with the new.
7
7
 
8
8
  To create a materialized view, pass the '--materialized' option.
9
+ To create a materialized view with NO DATA, pass '--no-data' option.
9
10
 
10
11
  Examples:
11
12
  rails generate scenic:view searches
@@ -1,5 +1,5 @@
1
1
  class <%= migration_class_name %> < <%= activerecord_migration_class %>
2
2
  def change
3
- create_view <%= formatted_plural_name %><%= ", materialized: true" if materialized? %>
3
+ create_view <%= formatted_plural_name %><%= create_view_options %>
4
4
  end
5
5
  end
@@ -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("../templates", __FILE__)
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.gsub('.', '').pluralize}"
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(*%w(db views))
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 == 0
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
- execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
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(dependency)
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
- dep.include?(view_to_refresh.to_s)
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