scenic 1.0.0 → 1.2.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +14 -4
  4. data/.yardopts +0 -1
  5. data/Appraisals +26 -0
  6. data/CONTRIBUTING.md +5 -4
  7. data/LICENSE.txt +1 -1
  8. data/NEWS.md +49 -0
  9. data/README.md +71 -28
  10. data/bin/appraisal +16 -0
  11. data/bin/setup +5 -0
  12. data/bin/yard +16 -0
  13. data/gemfiles/rails40.gemfile +8 -0
  14. data/gemfiles/rails41.gemfile +8 -0
  15. data/gemfiles/rails42.gemfile +8 -0
  16. data/gemfiles/rails50.gemfile +13 -0
  17. data/lib/generators/scenic/generators.rb +1 -0
  18. data/lib/generators/scenic/model/templates/model.erb +1 -1
  19. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  20. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
  21. data/lib/generators/scenic/view/view_generator.rb +13 -1
  22. data/lib/scenic/adapters/postgres/connection.rb +57 -0
  23. data/lib/scenic/adapters/postgres/errors.rb +26 -0
  24. data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
  25. data/lib/scenic/adapters/postgres/indexes.rb +53 -0
  26. data/lib/scenic/adapters/postgres/views.rb +51 -0
  27. data/lib/scenic/adapters/postgres.rb +144 -33
  28. data/lib/scenic/configuration.rb +2 -2
  29. data/lib/scenic/index.rb +36 -0
  30. data/lib/scenic/schema_dumper.rb +12 -5
  31. data/lib/scenic/statements.rb +7 -13
  32. data/lib/scenic/version.rb +1 -1
  33. data/lib/scenic/view.rb +1 -4
  34. data/lib/scenic.rb +1 -0
  35. data/scenic.gemspec +4 -1
  36. data/spec/dummy/config/application.rb +3 -0
  37. data/spec/generators/scenic/view/view_generator_spec.rb +12 -0
  38. data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
  39. data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
  40. data/spec/scenic/adapters/postgres_spec.rb +88 -26
  41. data/spec/scenic/statements_spec.rb +14 -15
  42. data/spec/smoke +24 -3
  43. data/spec/spec_helper.rb +4 -0
  44. metadata +64 -8
  45. data/spec/dummy/config/environments/development.rb +0 -6
  46. data/spec/dummy/config/environments/test.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aad0218a7caab0f7bb48d964a0b2fb7b45d3da68
4
- data.tar.gz: 567bf836c969e9a5287a19c10a5dcd40b9a0d03d
3
+ metadata.gz: e1617d9008cabf9f0117f71f812735e8f89bf932
4
+ data.tar.gz: 31babdfa7c8fc2bc200f9457bc6f8fcab9f16a8e
5
5
  SHA512:
6
- metadata.gz: 57c9ba2f5803667e7f256730881e75f95ffa113b6a75d3cb94b1ef884d9e258a26c696697840a4118a88ba010fb759d339e2c89f30a903ad967e700aeb227690
7
- data.tar.gz: bb6f28f0b4a24ac0e737c98fe4081e4f2a67bf9205bf89701a4deb2d1fc7ccdb1bc5d4fe5d52a002b5cc4f99154ff64d7906ea53ccd49f2e420a87c96f5bce31
6
+ metadata.gz: aebbd7f6d99c20b35420a68977a4231849543393c01d2a37487cfcde775cc26b33af792d13b61a4618db12dfe4eef0fc0a581ad27a5b76842d4701c0452302db
7
+ data.tar.gz: 05ea1809e4c4777903a297a9ee1e8c83d59ac3550aa455a82ef2d0e52f0a5f715211ed721e1f425aa87f4d08e7f04c7e7c9b1bec284861ab667e555eefe226c0
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ gemfiles/*.lock
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  addons:
2
- postgresql: "9.3"
2
+ postgresql: "9.4"
3
3
  before_install:
4
4
  - "echo '--colour' > ~/.rspec"
5
5
  - "echo 'gem: --no-document' > ~/.gemrc"
@@ -15,7 +15,17 @@ language:
15
15
  notifications:
16
16
  email:
17
17
  - false
18
- rvm:
19
- - 2.1.7
20
- - 2.2.3
21
18
  sudo: false
19
+ rvm:
20
+ - 2.3.0
21
+ - 2.2.4
22
+ - 2.1.8
23
+ gemfile:
24
+ - gemfiles/rails40.gemfile
25
+ - gemfiles/rails41.gemfile
26
+ - gemfiles/rails42.gemfile
27
+ - gemfiles/rails50.gemfile
28
+ matrix:
29
+ exclude:
30
+ - rvm: 2.1.8
31
+ gemfile: gemfiles/rails50.gemfile
data/.yardopts CHANGED
@@ -1,5 +1,4 @@
1
1
  --hide-api private
2
- --hide-api extension
3
2
  --exclude templates
4
3
  --markup markdown
5
4
  --markup-provider redcarpet
data/Appraisals ADDED
@@ -0,0 +1,26 @@
1
+ appraise "rails40" do
2
+ gem "activerecord", "~> 4.0.0"
3
+ gem "railties", "~> 4.0.0"
4
+ end
5
+
6
+ appraise "rails41" do
7
+ gem "activerecord", "~> 4.1.0"
8
+ gem "railties", "~> 4.1.0"
9
+ end
10
+
11
+ appraise "rails42" do
12
+ gem "activerecord", "~> 4.2.0"
13
+ gem "railties", "~> 4.2.0"
14
+ end
15
+
16
+ if RUBY_VERSION > "2.2.0"
17
+ appraise "rails50" do
18
+ gem "rails", github: "rails/rails"
19
+ gem "rspec-rails", github: "rspec/rspec-rails"
20
+ gem "rspec-support", github: "rspec/rspec-support"
21
+ gem "rspec-core", github: "rspec/rspec-core"
22
+ gem "rspec-mocks", github: "rspec/rspec-mocks"
23
+ gem "rspec-expectations", github: "rspec/rspec-expectations"
24
+ gem "rspec", github: "rspec/rspec"
25
+ end
26
+ end
data/CONTRIBUTING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Contributing
2
2
 
3
- We love pull requests from everyone. By participating in this project, you
3
+ We love contributions from everyone. By participating in this project, you
4
4
  agree to abide by the thoughtbot [code of conduct].
5
5
 
6
6
  [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
@@ -8,12 +8,13 @@ agree to abide by the thoughtbot [code of conduct].
8
8
  We expect everyone to follow the code of conduct anywhere in thoughtbot's
9
9
  project codebases, issue trackers, chatrooms, and mailing lists.
10
10
 
11
- ## Setting Up for Development
11
+ ## Contributing Code
12
12
 
13
- 1. For the repository.
13
+ 1. Fork the repository.
14
14
  2. Run `bin/setup`, which will install dependencies and create the dummy
15
15
  application database.
16
- 3. Run `rake` to verify that the tests pass.
16
+ 3. Run `bin/appraisal rake` to verify that the tests pass against all
17
+ supported versions of Rails.
17
18
  4. Make your change with new passing tests, following the [style guide].
18
19
  5. Write a [good commit message], push your fork, and submit a pull request.
19
20
 
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Derek Prior, Caleb Thompson, and thoughtbot.
1
+ Copyright (c) 2014-2016 Derek Prior, Caleb Thompson, and thoughtbot.
2
2
 
3
3
  MIT License
4
4
 
data/NEWS.md CHANGED
@@ -5,6 +5,55 @@ 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.2.0] - February 5, 2016
9
+
10
+ ### Added
11
+ - The generators now accept namespaced view definitions. For example: `rails
12
+ generate scenic:view my_app.users`.
13
+
14
+ ### Fixed
15
+ - Materialized view indexes are now properly dumped to `db/schema.rb`. This was
16
+ an oversight in previous releases, meaning `rake db:schema:load` was missing
17
+ indexes.
18
+ - Calling `update_view` for a materialized view now properly finds associated
19
+ indexes for automatic reapplication. An issue in the previous index query was
20
+ returning no indexes.
21
+
22
+ **Note**: Dumping materialized view indexes will produce an invalid
23
+ `db/schema.rb` file under Rails 5 beta 1 and beta 2. This is fixed on Rails
24
+ master.
25
+
26
+ [1.2.0]: https://github.com/thoughtbot/scenic/compare/v1.1.1...v1.2.0
27
+
28
+ ## [1.1.1] - January 29, 2016
29
+
30
+ ### Fixed
31
+ - Some schema operations were failing with a `PG::ConnectionBad: connection is
32
+ closed` error. This has been fixed by ensuring we grab a fresh connection for
33
+ all operations.
34
+
35
+ [1.1.1]: https://github.com/thoughtbot/scenic/compare/v1.1.0...v1.1.1
36
+
37
+ ## [1.1.0] - January 8, 2016
38
+
39
+ ### Added
40
+ - Added support for updating materialized view definitions while maintaining
41
+ existing indexes that are still applicable after the update.
42
+ - Added support for refreshing materialized views concurrently (requires
43
+ Postgres 9.4 or newer).
44
+
45
+ ### Fixed
46
+ - The schema dumper will now dump views and materialized views together in the
47
+ order they are returned by Postgres. This fixes issues when loading views that
48
+ depend on other views via `rake db:schema:load`.
49
+ - Scenic now works on [supported versions of Postgres] older than 9.3.0.
50
+ Attempts to use database features not supported by your specific version of
51
+ Postgres will raise descriptive errors.
52
+ - Fixed inability to dump materialized views in Rails 5.0.0.beta1.
53
+
54
+ [supported versions of Postgres]: http://www.postgresql.org/support/versioning/
55
+ [1.1.0]: https://github.com/thoughtbot/scenic/compare/v1.0.0...v1.1.0
56
+
8
57
  ## [1.0.0] - November 23, 2015
9
58
 
10
59
  ### Added
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Scenic
2
2
 
3
+ ![Scenic Landscape](https://images.thoughtbot.com/announcing-scenic--versioned-database-views-for-rails/MRUcPsxrTGCeWKyE59Zg_landscape.png)
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)
8
+
3
9
  Scenic adds methods to `ActiveRecord::Migration` to create and manage database
4
10
  views in Rails.
5
11
 
@@ -17,18 +23,18 @@ Scenic ships with support for PostgreSQL. The adapter is configurable (see
17
23
 
18
24
  ## Great, how do I create a view?
19
25
 
20
- You've got this great idea for a view you'd like to call `searches`. You can
21
- create the migration and the corresponding view definition file with the
26
+ You've got this great idea for a view you'd like to call `search_results`. You
27
+ can create the migration and the corresponding view definition file with the
22
28
  following command:
23
29
 
24
30
  ```sh
25
- $ rails generate scenic:view searches
26
- create db/views/searches_v01.sql
27
- create db/migrate/[TIMESTAMP]_create_searches.rb
31
+ $ rails generate scenic:view search_results
32
+ create db/views/search_results_v01.sql
33
+ create db/migrate/[TIMESTAMP]_create_search_results.rb
28
34
  ```
29
35
 
30
- Edit the `db/views/searches_v01.sql` file with the SQL statement that defines
31
- your view. In our example, this might look something like this:
36
+ Edit the `db/views/search_results_v01.sql` file with the SQL statement that
37
+ defines your view. In our example, this might look something like this:
32
38
 
33
39
  ```sql
34
40
  SELECT
@@ -62,13 +68,13 @@ $ rake db:migrate
62
68
  Here's where Scenic really shines. Run that same view generator once more:
63
69
 
64
70
  ```sh
65
- $ rails generate scenic:view searches
66
- create db/views/searches_v02.sql
67
- create db/migrate/[TIMESTAMP]_update_searches_to_version_2.rb
71
+ $ rails generate scenic:view search_results
72
+ create db/views/search_results_v02.sql
73
+ create db/migrate/[TIMESTAMP]_update_search_results_to_version_2.rb
68
74
  ```
69
75
 
70
- Scenic detected that we already had an existing `searches` view at version 1,
71
- created a copy of that definition as version 2, and created a migration to
76
+ Scenic detected that we already had an existing `search_results` view at version
77
+ 1, created a copy of that definition as version 2, and created a migration to
72
78
  update to the version 2 schema. All that's left for you to do is tweak the
73
79
  schema in the new definition and run the `update_view` migration.
74
80
 
@@ -76,11 +82,13 @@ schema in the new definition and run the `update_view` migration.
76
82
 
77
83
  You bet! Using view-backed models can help promote concepts hidden in your
78
84
  relational data to first-class domain objects and can clean up complex
79
- ActiveRecord or ARel queries. As far as ActiveRecord is concerned, you a view is
85
+ ActiveRecord or ARel queries. As far as ActiveRecord is concerned, a view is
80
86
  no different than a table.
81
87
 
82
88
  ```ruby
83
- class Search < ActiveRecord::Base
89
+ class SearchResult < ActiveRecord::Base
90
+ belongs_to :searchable, polymorphic: true
91
+
84
92
  private
85
93
 
86
94
  # this isn't strictly necessary, but it will prevent
@@ -109,18 +117,6 @@ $ rails generate scenic:model recent_status
109
117
  create db/migrate/20151112015036_create_recent_statuses.rb
110
118
  ```
111
119
 
112
- ### When I query that model with `find` I get an error. What gives?
113
-
114
- Your view cannot have a primary key, but ActiveRecord's `find` method expects to
115
- query based on one. You can use `find_by!` or you can explicitly set the primary
116
- key column on your model like so:
117
-
118
- ```ruby
119
- class People < ActiveRecord::Base
120
- self.primary_key = :id
121
- end
122
- ```
123
-
124
120
  ## What about materialized views?
125
121
 
126
122
  Materialized views are essentially SQL queries whose results can be cached to a
@@ -134,20 +130,67 @@ refreshes:
134
130
 
135
131
  ```ruby
136
132
  def self.refresh
137
- Scenic.database.refresh_materialized_view(table_name)
133
+ Scenic.database.refresh_materialized_view(table_name, concurrently: false)
138
134
  end
139
135
  ```
140
136
 
137
+ This will perform a non-concurrent refresh, locking the view for selects until
138
+ the refresh is complete. You can avoid locking the view by passing
139
+ `concurrently: true` but this requires both PostgreSQL 9.4 and your view to have
140
+ at least one unique index that covers all rows.
141
+
141
142
  ## I don't need this view anymore. Make it go away.
142
143
 
143
144
  Scenic gives you `drop_view` too:
144
145
 
145
146
  ```ruby
146
147
  def change
147
- drop_view :searches, revert_to_version: 2
148
+ drop_view :search_results, revert_to_version: 2
149
+ end
150
+ ```
151
+
152
+ ## FAQs
153
+
154
+ **Why do I get an error when querying a view-backed model with `find`, `last`, or `first`?**
155
+
156
+ ActiveRecord's `find` method expects to query based on your model's primary key,
157
+ but views do not have primary keys. Additionally, the `first` and `last` methods
158
+ will produce queries that attempt to sort based on the primary key.
159
+
160
+ You can get around these issues by setting the primary key column on your Rails
161
+ model like so:
162
+
163
+ ```ruby
164
+ class People < ActiveRecord::Base
165
+ self.primary_key = :my_unique_identifier_field
148
166
  end
149
167
  ```
150
168
 
169
+ **Why is my view missing columns from the underlying table?**
170
+
171
+ Did you create the view with `SELECT [table_name].*`? Most (possibly all)
172
+ relational databases freeze the view definition at the time of creation. New
173
+ columns will not be available in the view until the definition is updated once
174
+ again. This can be accomplished by "updating" the view to its current definition
175
+ to bake in the new meaning of `*`.
176
+
177
+ ```ruby
178
+ add_column :posts, :title, :string
179
+ update_view :posts_with_aggregate_data, version: 2, revert_to_version: 2
180
+ ```
181
+
182
+ **When will you support MySQL?**
183
+
184
+ We have no plans to add first-party support for MySQL at this time because we
185
+ (the maintainers) do not currently have a use for it. It's our experience that
186
+ maintaining a library effectively requires regular use of its features. We're
187
+ not in a good position to support MySQL users.
188
+
189
+ Scenic *does* support configuring different database adapters and should be
190
+ extendable with adapter libraries. If you implement such an adapter, we're happy
191
+ to review and link to it. We're also happy to make changes that would better
192
+ accommodate adapter gems.
193
+
151
194
  ## About
152
195
 
153
196
  Scenic is maintained by [Derek Prior] and [Caleb Thompson], funded by
data/bin/appraisal ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'appraisal' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('appraisal', 'appraisal')
data/bin/setup CHANGED
@@ -4,4 +4,9 @@ set -e
4
4
 
5
5
  gem install bundler --conservative
6
6
  bundle check || bundle install
7
+
8
+ if [ -z "$CI" ]; then
9
+ bundle exec appraisal install
10
+ fi
11
+
7
12
  bundle exec rake dummy:db:create
data/bin/yard ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'yard' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('yard', 'yard')
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0.0"
6
+ gem "railties", "~> 4.0.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.1.0"
6
+ gem "railties", "~> 4.1.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.2.0"
6
+ gem "railties", "~> 4.2.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", :github => "rails/rails"
6
+ gem "rspec-rails", :github => "rspec/rspec-rails"
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"
12
+
13
+ gemspec :path => "../"
@@ -3,6 +3,7 @@ module Scenic
3
3
  # models that are backed by views.
4
4
  #
5
5
  # See:
6
+ #
6
7
  # * {file:lib/generators/scenic/model/USAGE Model Generator}
7
8
  # * {file:lib/generators/scenic/view/USAGE View Generator}
8
9
  # * {file:README.md README}
@@ -1,3 +1,3 @@
1
1
  def self.refresh
2
- Scenic.database.refresh_materialized_view(table_name)
2
+ Scenic.database.refresh_materialized_view(table_name, concurrently: false)
3
3
  end
@@ -1,5 +1,5 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration
2
2
  def change
3
- create_view :<%= plural_name %><%= ", materialized: true" if materialized? %>
3
+ create_view <%= formatted_plural_name %><%= ", materialized: true" if materialized? %>
4
4
  end
5
5
  end
@@ -1,12 +1,12 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration
2
2
  def change
3
3
  <%- if materialized? -%>
4
- update_view :<%= plural_name %>,
4
+ update_view <%= formatted_plural_name %>,
5
5
  version: <%= version %>,
6
6
  revert_to_version: <%= previous_version %>,
7
7
  materialized: true
8
8
  <%- else -%>
9
- update_view :<%= plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
9
+ update_view <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
10
10
  <%- end -%>
11
11
  end
12
12
  end
@@ -56,7 +56,7 @@ module Scenic
56
56
 
57
57
  def migration_class_name
58
58
  if creating_new_view?
59
- super
59
+ "Create#{class_name.gsub('.', '').pluralize}"
60
60
  else
61
61
  "Update#{class_name.pluralize}ToVersion#{version}"
62
62
  end
@@ -85,10 +85,22 @@ module Scenic
85
85
  Scenic::Definition.new(plural_file_name, previous_version)
86
86
  end
87
87
 
88
+ def plural_file_name
89
+ @plural_file_name ||= file_name.pluralize.gsub(".", "_")
90
+ end
91
+
88
92
  def destroying?
89
93
  behavior == :revoke
90
94
  end
91
95
 
96
+ def formatted_plural_name
97
+ if plural_name.include?(".")
98
+ "\"#{plural_name}\""
99
+ else
100
+ ":#{plural_name}"
101
+ end
102
+ end
103
+
92
104
  def destroying_initial_view?
93
105
  destroying? && version == 1
94
106
  end
@@ -0,0 +1,57 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Decorates an ActiveRecord connection with methods that help determine
5
+ # the connections capabilities.
6
+ #
7
+ # Every attempt is made to use the versions of these methods defined by
8
+ # Rails where they are available and public before falling back to our own
9
+ # implementations for older Rails versions.
10
+ #
11
+ # @api private
12
+ class Connection < SimpleDelegator
13
+ # True if the connection supports materialized views.
14
+ #
15
+ # Delegates to the method of the same name if it is already defined on
16
+ # the connection. This is the case for Rails 4.2 or higher.
17
+ #
18
+ # @return [Boolean]
19
+ def supports_materialized_views?
20
+ if undecorated_connection.respond_to?(:supports_materialized_views?)
21
+ super
22
+ else
23
+ postgresql_version >= 90300
24
+ end
25
+ end
26
+
27
+ # True if the connection supports concurrent refreshes of materialized
28
+ # views.
29
+ #
30
+ # @return [Boolean]
31
+ def supports_concurrent_refreshes?
32
+ postgresql_version >= 90400
33
+ end
34
+
35
+ # An integer representing the version of Postgres we're connected to.
36
+ #
37
+ # postgresql_version is public in Rails 5, but protected in earlier
38
+ # versions.
39
+ #
40
+ # @return [Integer]
41
+ def postgresql_version
42
+ if undecorated_connection.respond_to?(:postgresql_version)
43
+ super
44
+ else
45
+ undecorated_connection.send(:postgresql_version)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def undecorated_connection
52
+ __getobj__
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Raised when a materialized view operation is attempted on a database
5
+ # version that does not support materialized views.
6
+ #
7
+ # Materialized views are supported on Postgres 9.3 or newer.
8
+ class MaterializedViewsNotSupportedError < StandardError
9
+ def initialize
10
+ super("Materialized views require Postgres 9.3 or newer")
11
+ end
12
+ end
13
+
14
+ # Raised when attempting a concurrent materialized view refresh on a
15
+ # database version that does not support that.
16
+ #
17
+ # Concurrent materialized view refreshes are supported on Postgres 9.4 or
18
+ # newer.
19
+ class ConcurrentRefreshesNotSupportedError < StandardError
20
+ def initialize
21
+ super("Concurrent materialized view refreshes require Postgres 9.4 or newer")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,71 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Updatine a materialized view causes the view to be dropped and
5
+ # recreated. This causes any associated indexes to be dropped as well.
6
+ # This object can be used to capture the existing indexes before the drop
7
+ # and then reapply appropriate indexes following the create.
8
+ #
9
+ # @api private
10
+ class IndexReapplication
11
+ # Creates the index reapplication object.
12
+ #
13
+ # @param connection [Connection] The connection to execute SQL against.
14
+ # @param speaker [#say] (ActiveRecord::Migration) The object used for
15
+ # logging the results of reapplying indexes.
16
+ def initialize(connection:, speaker: ActiveRecord::Migration)
17
+ @connection = connection
18
+ @speaker = speaker
19
+ end
20
+
21
+ # Caches indexes on the provided object before executing the block and
22
+ # then reapplying the indexes. Each recreated or skipped index is
23
+ # announced to STDOUT by default. This can be overridden in the
24
+ # constructor.
25
+ #
26
+ # @param name The name of the object we are reapplying indexes on.
27
+ # @yield Operations to perform before reapplying indexes.
28
+ #
29
+ # @return [void]
30
+ def on(name)
31
+ indexes = Indexes.new(connection: connection).on(name)
32
+
33
+ yield
34
+
35
+ indexes.each(&method(:try_index_create))
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :connection, :speaker
41
+
42
+ def try_index_create(index)
43
+ success = with_savepoint(index.index_name) do
44
+ connection.execute(index.definition)
45
+ end
46
+
47
+ if success
48
+ say "index '#{index.index_name}' on '#{index.object_name}' has been recreated"
49
+ else
50
+ say "index '#{index.index_name}' on '#{index.object_name}' is no longer valid and has been dropped."
51
+ end
52
+ end
53
+
54
+ def with_savepoint(name)
55
+ connection.execute("SAVEPOINT #{name}")
56
+ yield
57
+ connection.execute("RELEASE SAVEPOINT #{name}")
58
+ true
59
+ rescue
60
+ connection.execute("ROLLBACK TO SAVEPOINT #{name}")
61
+ false
62
+ end
63
+
64
+ def say(message)
65
+ subitem = true
66
+ speaker.say(message, subitem)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end