scenic 1.4.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.devcontainer/Dockerfile +6 -0
  3. data/.devcontainer/devcontainer.json +11 -0
  4. data/.devcontainer/docker-compose.yml +24 -0
  5. data/.github/workflows/ci.yml +71 -0
  6. data/.gitignore +2 -0
  7. data/.hound.yml +2 -4
  8. data/.rubocop.yml +129 -0
  9. data/{NEWS.md → CHANGELOG.md} +122 -13
  10. data/CODE_OF_CONDUCT.md +76 -0
  11. data/CONTRIBUTING.md +7 -9
  12. data/Gemfile +13 -1
  13. data/LICENSE.txt +1 -1
  14. data/README.md +44 -42
  15. data/Rakefile +2 -2
  16. data/SECURITY.md +14 -0
  17. data/bin/setup +9 -4
  18. data/lib/generators/scenic/materializable.rb +18 -0
  19. data/lib/generators/scenic/model/model_generator.rb +19 -8
  20. data/lib/generators/scenic/view/USAGE +1 -0
  21. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  22. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +4 -3
  23. data/lib/generators/scenic/view/view_generator.rb +19 -9
  24. data/lib/scenic/adapters/postgres/index_reapplication.rb +1 -1
  25. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
  26. data/lib/scenic/adapters/postgres/views.rb +11 -1
  27. data/lib/scenic/adapters/postgres.rb +20 -7
  28. data/lib/scenic/command_recorder/statement_arguments.rb +20 -2
  29. data/lib/scenic/command_recorder.rb +6 -1
  30. data/lib/scenic/configuration.rb +1 -1
  31. data/lib/scenic/definition.rb +4 -2
  32. data/lib/scenic/schema_dumper.rb +2 -2
  33. data/lib/scenic/statements.rb +24 -6
  34. data/lib/scenic/unaffixed_name.rb +31 -0
  35. data/lib/scenic/version.rb +1 -1
  36. data/lib/scenic/view.rb +7 -4
  37. data/lib/scenic.rb +1 -0
  38. data/scenic.gemspec +21 -23
  39. data/spec/acceptance/user_manages_views_spec.rb +2 -1
  40. data/spec/dummy/app/models/application_record.rb +5 -0
  41. data/spec/dummy/config/database.yml +5 -0
  42. data/spec/dummy/db/migrate/20220112154220_add_pg_stat_statements_extension.rb +5 -0
  43. data/spec/dummy/db/schema.rb +19 -0
  44. data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
  45. data/spec/generators/scenic/view/view_generator_spec.rb +22 -4
  46. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
  47. data/spec/scenic/adapters/postgres_spec.rb +23 -3
  48. data/spec/scenic/command_recorder_spec.rb +15 -1
  49. data/spec/scenic/definition_spec.rb +15 -1
  50. data/spec/scenic/schema_dumper_spec.rb +48 -3
  51. data/spec/scenic/statements_spec.rb +49 -14
  52. data/spec/spec_helper.rb +5 -1
  53. data/spec/support/generator_spec_setup.rb +1 -1
  54. data/spec/support/rails_configuration_helpers.rb +10 -0
  55. metadata +32 -41
  56. data/.travis.yml +0 -44
  57. data/Appraisals +0 -33
  58. data/bin/appraisal +0 -16
  59. data/gemfiles/rails40.gemfile +0 -8
  60. data/gemfiles/rails41.gemfile +0 -8
  61. data/gemfiles/rails42.gemfile +0 -8
  62. data/gemfiles/rails42_with_fg_rails.gemfile +0 -10
  63. data/gemfiles/rails50.gemfile +0 -8
  64. data/gemfiles/rails51.gemfile +0 -8
  65. data/gemfiles/rails_edge.gemfile +0 -8
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/actions/workflows/ci.yml/badge.svg)](https://github.com/scenic-views/scenic/actions/workflows/ci.yml)
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
@@ -86,30 +92,19 @@ a new version of it.
86
92
  This is not desirable when you have complicated hierarchies of views, especially
87
93
  when some of those views may be materialized and take a long time to recreate.
88
94
 
89
- You can use `replace_view` to generate a CREATE OR REPLACE VIEW SQL statement.
90
-
91
- See postgresql documentation on how this works:
92
- http://www.postgresql.org/docs/current/static/sql-createview.html
93
-
94
- To start replacing a view run the generator like for a regular change:
95
+ You can use `replace_view` to generate a CREATE OR REPLACE VIEW SQL statement
96
+ instead by adding the `--replace` option to the generate command:
95
97
 
96
98
  ```sh
97
- $ rails generate scenic:view search_results
99
+ $ rails generate scenic:view search_results --replace
98
100
  create db/views/search_results_v02.sql
99
101
  create db/migrate/[TIMESTAMP]_update_search_results_to_version_2.rb
100
102
  ```
101
103
 
102
- Now, edit the migration. It should look something like:
103
-
104
- ```ruby
105
- class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
106
- def change
107
- update_view :search_results, version: 2, revert_to_version: 1
108
- end
109
- end
110
- ```
104
+ See Postgres documentation on how this works:
105
+ http://www.postgresql.org/docs/current/static/sql-createview.html
111
106
 
112
- Update it to use replace view:
107
+ The migration will look something like this:
113
108
 
114
109
  ```ruby
115
110
  class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
@@ -119,7 +114,7 @@ class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
119
114
  end
120
115
  ```
121
116
 
122
- Now you can run the migration like normal.
117
+ You can run the migration and the view will be replaced instead.
123
118
 
124
119
  ## Can I use this view to back a model?
125
120
 
@@ -129,11 +124,9 @@ ActiveRecord or ARel queries. As far as ActiveRecord is concerned, a view is
129
124
  no different than a table.
130
125
 
131
126
  ```ruby
132
- class SearchResult < ActiveRecord::Base
127
+ class SearchResult < ApplicationRecord
133
128
  belongs_to :searchable, polymorphic: true
134
129
 
135
- private
136
-
137
130
  # this isn't strictly necessary, but it will prevent
138
131
  # rails from calling save, which would fail anyway.
139
132
  def readonly?
@@ -198,6 +191,7 @@ Scenic gives you `drop_view` too:
198
191
  ```ruby
199
192
  def change
200
193
  drop_view :search_results, revert_to_version: 2
194
+ drop_view :materialized_admin_reports, revert_to_version: 3, materialized: true
201
195
  end
202
196
  ```
203
197
 
@@ -213,7 +207,7 @@ You can get around these issues by setting the primary key column on your Rails
213
207
  model like so:
214
208
 
215
209
  ```ruby
216
- class People < ActiveRecord::Base
210
+ class People < ApplicationRecord
217
211
  self.primary_key = :my_unique_identifier_field
218
212
  end
219
213
  ```
@@ -231,31 +225,39 @@ add_column :posts, :title, :string
231
225
  update_view :posts_with_aggregate_data, version: 2, revert_to_version: 2
232
226
  ```
233
227
 
234
- **When will you support MySQL?**
228
+ **When will you support MySQL, SQLite, or other databases?**
235
229
 
236
- We have no plans to add first-party support for MySQL at this time because we
237
- (the maintainers) do not currently have a use for it. It's our experience that
238
- maintaining a library effectively requires regular use of its features. We're
239
- not in a good position to support MySQL users.
230
+ We have no plans to add first-party adapters for other relational databases at
231
+ this time because we (the maintainers) do not currently have a use for them.
232
+ It's our experience that maintaining a library effectively requires regular use
233
+ of its features. We're not in a good position to support MySQL, SQLite or other
234
+ database users.
240
235
 
241
236
  Scenic *does* support configuring different database adapters and should be
242
237
  extendable with adapter libraries. If you implement such an adapter, we're happy
243
238
  to review and link to it. We're also happy to make changes that would better
244
239
  accommodate adapter gems.
245
240
 
246
- ## About
241
+ We are aware of the following existing adapter libraries for Scenic which may
242
+ meet your needs:
247
243
 
248
- Scenic is maintained by [Derek Prior] and [Caleb Thompson], funded by
249
- thoughtbot, inc. The names and logos for thoughtbot are trademarks of
250
- thoughtbot, inc.
244
+ * [`scenic_sqlite_adapter`](<https://github.com/pdebelak/scenic_sqlite_adapter>)
245
+ * [`scenic-mysql_adapter`](<https://github.com/EmpaticoOrg/scenic-mysql_adapter>)
246
+ * [`scenic-sqlserver-adapter`](<https://github.com/ClickMechanic/scenic_sqlserver_adapter>)
247
+ * [`scenic-oracle_adapter`](<https://github.com/cdinger/scenic-oracle_adapter>)
251
248
 
252
- [Derek Prior]: http://prioritized.net
253
- [Caleb Thompson]: http://calebthompson.io
249
+ Please note that the maintainers of Scenic make no assertions about the
250
+ quality or security of the above adapters.
254
251
 
255
- ![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg)
252
+ ## About
256
253
 
257
- We love open source software! See [our other projects][community] or [hire
258
- us][hire] to help build your product.
254
+ Scenic is used by some popular open source Rails apps:
255
+ [Mastodon](<https://github.com/mastodon/mastodon/>),
256
+ [Code.org](<https://github.com/code-dot-org/code-dot-org>), and
257
+ [Lobste.rs](<https://github.com/lobsters/lobsters/>).
259
258
 
260
- [community]: https://thoughtbot.com/community?utm_source=github
261
- [hire]: https://thoughtbot.com/hire-us?utm_source=github
259
+ Scenic is maintained by [Derek Prior], [Caleb Hearth], and you, our
260
+ contributors.
261
+
262
+ [Derek Prior]: http://prioritized.net
263
+ [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
 
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,16 @@ 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
18
+ class_option :replace,
19
+ type: :boolean,
20
+ required: false,
21
+ desc: "Uses replace_view instead of update_view",
22
+ default: false
13
23
  end
14
24
 
15
25
  private
@@ -17,6 +27,14 @@ module Scenic
17
27
  def materialized?
18
28
  options[:materialized]
19
29
  end
30
+
31
+ def replace_view?
32
+ options[:replace]
33
+ end
34
+
35
+ def no_data?
36
+ options[:no_data]
37
+ end
20
38
  end
21
39
  end
22
40
  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,13 +35,24 @@ 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")
39
- ERB.new(
40
- ::File.binread(source),
41
- nil,
42
- "-",
43
- "@output_buffer",
44
- ).result(context)
38
+ context = instance_eval("binding", __FILE__, __LINE__)
39
+
40
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
41
+ erb = ERB.new(
42
+ ::File.binread(source),
43
+ trim_mode: "-",
44
+ eoutvar: "@output_buffer",
45
+ )
46
+ else
47
+ erb = ERB.new(
48
+ ::File.binread(source),
49
+ nil,
50
+ "-",
51
+ "@output_buffer",
52
+ )
53
+ end
54
+
55
+ erb.result(context)
45
56
  end
46
57
 
47
58
  def generating?
@@ -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
@@ -1,12 +1,13 @@
1
1
  class <%= migration_class_name %> < <%= activerecord_migration_class %>
2
2
  def change
3
+ <% method_name = replace_view? ? 'replace_view' : 'update_view' %>
3
4
  <%- if materialized? -%>
4
- update_view <%= formatted_plural_name %>,
5
+ <%= method_name %> <%= formatted_plural_name %>,
5
6
  version: <%= version %>,
6
7
  revert_to_version: <%= previous_version %>,
7
- materialized: true
8
+ materialized: <%= no_data? ? "{ no_data: true }" : true %>
8
9
  <%- else -%>
9
- update_view <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
10
+ <%= method_name %> <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
10
11
  <%- end -%>
11
12
  end
12
13
  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
@@ -64,7 +64,7 @@ module Scenic
64
64
 
65
65
  def activerecord_migration_class
66
66
  if ActiveRecord::Migration.respond_to?(:current_version)
67
- "ActiveRecord::Migration[5.0]"
67
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
68
68
  else
69
69
  "ActiveRecord::Migration"
70
70
  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
@@ -13,7 +13,7 @@ module Scenic
13
13
  # @param connection [Connection] The connection to execute SQL against.
14
14
  # @param speaker [#say] (ActiveRecord::Migration) The object used for
15
15
  # logging the results of reapplying indexes.
16
- def initialize(connection:, speaker: ActiveRecord::Migration)
16
+ def initialize(connection:, speaker: ActiveRecord::Migration.new)
17
17
  @connection = connection
18
18
  @speaker = speaker
19
19
  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
@@ -34,6 +34,7 @@ module Scenic
34
34
  WHERE
35
35
  c.relkind IN ('m', 'v')
36
36
  AND c.relname NOT IN (SELECT extname FROM pg_extension)
37
+ AND c.relname != 'pg_stat_statements_info'
37
38
  AND n.nspname = ANY (current_schemas(false))
38
39
  ORDER BY c.oid
39
40
  SQL
@@ -57,7 +58,16 @@ module Scenic
57
58
 
58
59
  def pg_identifier(name)
59
60
  return name if name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
60
- PGconn.quote_ident(name)
61
+
62
+ pgconn.quote_ident(name)
63
+ end
64
+
65
+ def pgconn
66
+ if defined?(PG::Connection)
67
+ PG::Connection
68
+ else
69
+ PGconn
70
+ end
61
71
  end
62
72
  end
63
73
  end
@@ -33,7 +33,7 @@ module Scenic
33
33
  #
34
34
  # @example
35
35
  # Scenic.configure do |config|
36
- # config.adapter = Scenic::Adapters::Postgres.new
36
+ # config.database = Scenic::Adapters::Postgres.new
37
37
  # end
38
38
  def initialize(connectable = ActiveRecord::Base)
39
39
  @connectable = connectable
@@ -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
@@ -22,8 +22,12 @@ module Scenic
22
22
  StatementArguments.new([view, options_for_revert])
23
23
  end
24
24
 
25
+ def remove_version
26
+ StatementArguments.new([view, options_without_version])
27
+ end
28
+
25
29
  def to_a
26
- @args.to_a
30
+ @args.to_a.dup.delete_if(&:empty?)
27
31
  end
28
32
 
29
33
  private
@@ -32,11 +36,25 @@ module Scenic
32
36
  @options ||= @args[1] || {}
33
37
  end
34
38
 
39
+ def keyword_hash(hash)
40
+ if Hash.respond_to? :ruby2_keywords_hash
41
+ Hash.ruby2_keywords_hash(hash)
42
+ else
43
+ hash
44
+ end
45
+ end
46
+
35
47
  def options_for_revert
36
- options.clone.tap do |revert_options|
48
+ opts = options.clone.tap do |revert_options|
37
49
  revert_options[:version] = revert_to_version
38
50
  revert_options.delete(:revert_to_version)
39
51
  end
52
+
53
+ keyword_hash(opts)
54
+ end
55
+
56
+ def options_without_version
57
+ keyword_hash(options.except(:version))
40
58
  end
41
59
  end
42
60
  end
@@ -6,21 +6,26 @@ module Scenic
6
6
  def create_view(*args)
7
7
  record(:create_view, args)
8
8
  end
9
+ ruby2_keywords :create_view if respond_to?(:ruby2_keywords, true)
9
10
 
10
11
  def drop_view(*args)
11
12
  record(:drop_view, args)
12
13
  end
14
+ ruby2_keywords :drop_view if respond_to?(:ruby2_keywords, true)
13
15
 
14
16
  def update_view(*args)
15
17
  record(:update_view, args)
16
18
  end
19
+ ruby2_keywords :update_view if respond_to?(:ruby2_keywords, true)
17
20
 
18
21
  def replace_view(*args)
19
22
  record(:replace_view, args)
20
23
  end
24
+ ruby2_keywords :replace_view if respond_to?(:ruby2_keywords, true)
21
25
 
22
26
  def invert_create_view(args)
23
- [:drop_view, args]
27
+ drop_view_args = StatementArguments.new(args).remove_version.to_a
28
+ [:drop_view, drop_view_args]
24
29
  end
25
30
 
26
31
  def invert_drop_view(args)
@@ -2,7 +2,7 @@ module Scenic
2
2
  class Configuration
3
3
  # The Scenic database adapter instance to use when executing SQL.
4
4
  #
5
- # Defualts to an instance of {Adapters::Postgres}
5
+ # Defaults to an instance of {Adapters::Postgres}
6
6
  # @return Scenic adapter
7
7
  attr_accessor :database
8
8
 
@@ -2,7 +2,7 @@ module Scenic
2
2
  # @api private
3
3
  class Definition
4
4
  def initialize(name, version)
5
- @name = name
5
+ @name = name.to_s
6
6
  @version = version.to_i
7
7
  end
8
8
 
@@ -28,8 +28,10 @@ module Scenic
28
28
 
29
29
  private
30
30
 
31
+ attr_reader :name
32
+
31
33
  def filename
32
- "#{@name}_v#{version}.sql"
34
+ "#{UnaffixedName.for(name).tr('.', '_')}_v#{version}.sql"
33
35
  end
34
36
  end
35
37
  end