scenic 1.0.0 → 1.1.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 (42) 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 +25 -0
  6. data/CONTRIBUTING.md +5 -4
  7. data/LICENSE.txt +1 -1
  8. data/NEWS.md +20 -0
  9. data/README.md +68 -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 +14 -0
  17. data/lib/generators/scenic/generators.rb +1 -0
  18. data/lib/generators/scenic/model/templates/model.erb +1 -1
  19. data/lib/scenic.rb +1 -0
  20. data/lib/scenic/adapters/postgres.rb +140 -33
  21. data/lib/scenic/adapters/postgres/connection.rb +57 -0
  22. data/lib/scenic/adapters/postgres/errors.rb +26 -0
  23. data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
  24. data/lib/scenic/adapters/postgres/indexes.rb +53 -0
  25. data/lib/scenic/adapters/postgres/views.rb +51 -0
  26. data/lib/scenic/configuration.rb +2 -2
  27. data/lib/scenic/index.rb +36 -0
  28. data/lib/scenic/schema_dumper.rb +1 -1
  29. data/lib/scenic/statements.rb +7 -13
  30. data/lib/scenic/version.rb +1 -1
  31. data/lib/scenic/view.rb +0 -3
  32. data/scenic.gemspec +4 -1
  33. data/spec/dummy/config/application.rb +3 -0
  34. data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
  35. data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
  36. data/spec/scenic/adapters/postgres_spec.rb +84 -26
  37. data/spec/scenic/statements_spec.rb +14 -15
  38. data/spec/smoke +16 -3
  39. data/spec/spec_helper.rb +4 -0
  40. metadata +64 -8
  41. data/spec/dummy/config/environments/development.rb +0 -6
  42. 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: d42b1d2b3e1f8fd304e1f6ba852a0cc54b6727b6
4
+ data.tar.gz: 3a84ebfd4b1a34ad03a68f232961c6381107cd74
5
5
  SHA512:
6
- metadata.gz: 57c9ba2f5803667e7f256730881e75f95ffa113b6a75d3cb94b1ef884d9e258a26c696697840a4118a88ba010fb759d339e2c89f30a903ad967e700aeb227690
7
- data.tar.gz: bb6f28f0b4a24ac0e737c98fe4081e4f2a67bf9205bf89701a4deb2d1fc7ccdb1bc5d4fe5d52a002b5cc4f99154ff64d7906ea53ccd49f2e420a87c96f5bce31
6
+ metadata.gz: e67ff566304f93fd0c70a6c4968d2551ea9a82c3111f9f8a95f4b1dbc4e43a3f7233d9b71fc0551b8ffd806f78c5967bbd43d7b0a3783a80cc1da68b99edd041
7
+ data.tar.gz: 8b19e9f8e3e93142a57c15ec0f191abeab1278395bb6a09fc9face3908e8039a58cc1704d32cbecb834f966f355a6b98256ed8d2ba0727e4209efe6c83cb52e9
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ gemfiles/*.lock
@@ -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
@@ -0,0 +1,25 @@
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
+ appraise "rails50" do
17
+ gem "activerecord", "~> 5.0.0.beta1"
18
+ gem "railties", "~> 5.0.0.beta1"
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
@@ -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
 
@@ -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,26 @@ 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.1.0] - January 8, 2016
9
+
10
+ ### Added
11
+ - Added support for updating materialized view definitions while maintaining
12
+ existing indexes that are still applicable after the update.
13
+ - Added support for refreshing materialized views concurrently (requires
14
+ Postgres 9.4 or newer).
15
+
16
+ ### Fixed
17
+ - The schema dumper will now dump views and materialized views together in the
18
+ order they are returned by Postgres. This fixes issues when loading views that
19
+ depend on other views via `rake db:schema:load`.
20
+ - Scenic now works on [supported versions of Postgres] older than 9.3.0.
21
+ Attempts to use database features not supported by your specific version of
22
+ Postgres will raise descriptive errors.
23
+ - Fixed inability to dump materialized views in Rails 5.0.0.beta1.
24
+
25
+ [supported versions of Postgres]: http://www.postgresql.org/support/versioning/
26
+ [1.1.0]: https://github.com/thoughtbot/scenic/compare/v1.0.0...v1.1.0
27
+
8
28
  ## [1.0.0] - November 23, 2015
9
29
 
10
30
  ### 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,64 @@ 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
148
149
  end
149
150
  ```
150
151
 
152
+ ## FAQs
153
+
154
+ **When I query a view-backed model with `find` I get an error. What gives?**
155
+
156
+ Your view cannot have a primary key, but ActiveRecord's `find` method expects to
157
+ query based on one. You can use `find_by!` or you can explicitly set the primary
158
+ key column on your model like so:
159
+
160
+ ```ruby
161
+ class People < ActiveRecord::Base
162
+ self.primary_key = :id
163
+ end
164
+ ```
165
+
166
+ **Why is my view missing columns from the underlying table?**
167
+
168
+ Did you create the view with `SELECT [table_name].*`? Most (possibly all)
169
+ relational databases freeze the view definition at the time of creation. New
170
+ columns will not be available in the view until the definition is updated once
171
+ again. This can be accomplished by "updating" the view to its current definition
172
+ to bake in the new meaning of `*`.
173
+
174
+ ```ruby
175
+ add_column :posts, :title, :string
176
+ update_view :posts_with_aggregate_data, version: 2, revert_to_version: 2
177
+ ```
178
+
179
+ **When will you support MySQL?**
180
+
181
+ We have no plans to add first-party support for MySQL at this time because we
182
+ (the maintainers) do not currently have a use for it. It's our experience that
183
+ maintaining a library effectively requires regular use of its features. We're
184
+ not in a good position to support MySQL users.
185
+
186
+ Scenic *does* support configuring different database adapters and should be
187
+ extendable with adapter libraries. If you implement such an adapter, we're happy
188
+ to review and link to it. We're also happy to make changes that would better
189
+ accommodate adapter gems.
190
+
151
191
  ## About
152
192
 
153
193
  Scenic is maintained by [Derek Prior] and [Caleb Thompson], funded by
@@ -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
@@ -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,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.0.0.beta1"
6
+ gem "railties", "~> 5.0.0.beta1"
7
+ gem "rspec-rails", :github => "rspec/rspec-rails"
8
+ gem "rspec-support", :github => "rspec/rspec-support"
9
+ gem "rspec-core", :github => "rspec/rspec-core"
10
+ gem "rspec-mocks", :github => "rspec/rspec-mocks"
11
+ gem "rspec-expectations", :github => "rspec/rspec-expectations"
12
+ gem "rspec", :github => "rspec/rspec"
13
+
14
+ 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
@@ -7,6 +7,7 @@ require "scenic/schema_dumper"
7
7
  require "scenic/statements"
8
8
  require "scenic/version"
9
9
  require "scenic/view"
10
+ require "scenic/index"
10
11
 
11
12
  # Scenic adds methods `ActiveRecord::Migration` to create and manage database
12
13
  # views in Rails applications.
@@ -1,3 +1,9 @@
1
+ require_relative "postgres/connection"
2
+ require_relative "postgres/errors"
3
+ require_relative "postgres/index_reapplication"
4
+ require_relative "postgres/indexes"
5
+ require_relative "postgres/views"
6
+
1
7
  module Scenic
2
8
  # Scenic database adapters.
3
9
  #
@@ -7,14 +13,31 @@ module Scenic
7
13
  module Adapters
8
14
  # An adapter for managing Postgres views.
9
15
  #
10
- # **This object is used internally by adapters and the schema dumper and is
11
- # not intended to be used by application code. It is documented here for
12
- # use by adapter gems.**
13
- #
14
- # For methods usable in migrations see {Statements}.
16
+ # These methods are used interally by Scenic and are not intended for direct
17
+ # use. Methods that alter database schema are intended to be called via
18
+ # {Statements}, while {#refresh_materialized_view} is called via
19
+ # {Scenic.database}.
15
20
  #
16
- # @api extension
21
+ # The methods are documented here for insight into specifics of how Scenic
22
+ # integrates with Postgres and the responsibilities of {Adapters}.
17
23
  class Postgres
24
+ # Creates an instance of the Scenic Postgres adapter.
25
+ #
26
+ # This is the default adapter for Scenic. Configuring it via
27
+ # {Scenic.configure} is not required, but the example below shows how one
28
+ # would explicitly set it.
29
+ #
30
+ # @param connection The database connection the adapter should use. This
31
+ # defaults to `ActiveRecord::Base.connection`
32
+ #
33
+ # @example
34
+ # Scenic.configure do |config|
35
+ # config.adapter = Scenic::Adapters::Postgres.new
36
+ # end
37
+ def initialize(connection = ActiveRecord::Base.connection)
38
+ @connection = Connection.new(connection)
39
+ end
40
+
18
41
  # Returns an array of views in the database.
19
42
  #
20
43
  # This collection of views is used by the [Scenic::SchemaDumper] to
@@ -22,73 +45,157 @@ module Scenic
22
45
  #
23
46
  # @return [Array<Scenic::View>]
24
47
  def views
25
- execute(<<-SQL).map { |result| view_from_database(result) }
26
- SELECT viewname, definition, FALSE AS materialized
27
- FROM pg_views
28
- WHERE schemaname = ANY (current_schemas(false))
29
- AND viewname NOT IN (SELECT extname FROM pg_extension)
30
- UNION
31
- SELECT matviewname AS viewname, definition, TRUE AS materialized
32
- FROM pg_matviews
33
- WHERE schemaname = ANY (current_schemas(false))
34
- ORDER BY viewname
35
- SQL
48
+ Views.new(connection).all
36
49
  end
37
50
 
38
51
  # Creates a view in the database.
39
52
  #
53
+ # This is typically called in a migration via {Statements#create_view}.
54
+ #
40
55
  # @param name The name of the view to create
41
- # @param sql_definition the SQL schema for the view.
56
+ # @param sql_definition The SQL schema for the view.
57
+ #
42
58
  # @return [void]
43
59
  def create_view(name, sql_definition)
44
- execute "CREATE VIEW #{name} AS #{sql_definition};"
60
+ execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
61
+ end
62
+
63
+ # Updates a view in the database.
64
+ #
65
+ # This results in a {#drop_view} followed by a {#create_view}. The
66
+ # explicitness of that two step process is preferred to `CREATE OR
67
+ # REPLACE VIEW` because the former ensures that the view you are trying to
68
+ # update did, in fact, already exist. Additionally, `CREATE OR REPLACE
69
+ # VIEW` is allowed only to add new columns to the end of an existing
70
+ # view schema. Existing columns cannot be re-ordered, removed, or have
71
+ # their types changed. Drop and create overcomes this limitation as well.
72
+ #
73
+ # This is typically called in a migration via {Statements#update_view}.
74
+ #
75
+ # @param name The name of the view to update
76
+ # @param sql_definition The SQL schema for the updated view.
77
+ #
78
+ # @return [void]
79
+ def update_view(name, sql_definition)
80
+ drop_view(name)
81
+ create_view(name, sql_definition)
45
82
  end
46
83
 
47
84
  # Drops the named view from the database
48
85
  #
86
+ # This is typically called in a migration via {Statements#drop_view}.
87
+ #
49
88
  # @param name The name of the view to drop
89
+ #
50
90
  # @return [void]
51
91
  def drop_view(name)
52
- execute "DROP VIEW #{name};"
92
+ execute "DROP VIEW #{quote_table_name(name)};"
53
93
  end
54
94
 
55
95
  # Creates a materialized view in the database
56
96
  #
57
97
  # @param name The name of the materialized view to create
58
98
  # @param sql_definition The SQL schema that defines the materialized view.
99
+ #
100
+ # This is typically called in a migration via {Statements#create_view}.
101
+ #
102
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
103
+ # in use does not support materialized views.
104
+ #
59
105
  # @return [void]
60
106
  def create_materialized_view(name, sql_definition)
61
- execute "CREATE MATERIALIZED VIEW #{name} AS #{sql_definition};"
107
+ raise_unless_materialized_views_supported
108
+ execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
109
+ end
110
+
111
+ # Updates a materialized view in the database.
112
+ #
113
+ # Drops and recreates the materialized view. Attempts to maintain all
114
+ # previously existing and still applicable indexes on the materialized
115
+ # view after the view is recreated.
116
+ #
117
+ # This is typically called in a migration via {Statements#update_view}.
118
+ #
119
+ # @param name The name of the view to update
120
+ # @param sql_definition The SQL schema for the updated view.
121
+ #
122
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
123
+ # in use does not support materialized views.
124
+ #
125
+ # @return [void]
126
+ def update_materialized_view(name, sql_definition)
127
+ raise_unless_materialized_views_supported
128
+
129
+ IndexReapplication.new(connection: connection).on(name) do
130
+ drop_materialized_view(name)
131
+ create_materialized_view(name, sql_definition)
132
+ end
62
133
  end
63
134
 
64
135
  # Drops a materialized view in the database
65
136
  #
137
+ # This is typically called in a migration via {Statements#update_view}.
138
+ #
66
139
  # @param name The name of the materialized view to drop.
140
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
141
+ # in use does not support materialized views.
142
+ #
67
143
  # @return [void]
68
144
  def drop_materialized_view(name)
69
- execute "DROP MATERIALIZED VIEW #{name};"
145
+ raise_unless_materialized_views_supported
146
+ execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};"
70
147
  end
71
148
 
72
149
  # Refreshes a materialized view from its SQL schema.
73
150
  #
74
- # @param name The name of the materialized view to refresh..
151
+ # This is typically called from application code via {Scenic.database}.
152
+ #
153
+ # @param name The name of the materialized view to refresh.
154
+ # @param concurrently [Boolean] Whether the refreshs hould happen
155
+ # concurrently or not. A concurrent refresh allows the view to be
156
+ # refreshed without locking the view for select but requires that the
157
+ # table have at least one unique index that covers all rows. Attempts to
158
+ # refresh concurrently without a unique index will raise a descriptive
159
+ # error.
160
+ #
161
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
162
+ # in use does not support materialized views.
163
+ # @raise [ConcurrentRefreshesNotSupportedError] when attempting a
164
+ # concurrent refresh on version of Postgres that does not support
165
+ # concurrent materialized view refreshes.
166
+ #
167
+ # @example Non-concurrent refresh
168
+ # Scenic.database.refresh_materialized_view(:search_results)
169
+ # @example Concurrent refresh
170
+ # Scenic.database.refresh_materialized_view(:posts, concurrent: true)
171
+ #
75
172
  # @return [void]
76
- def refresh_materialized_view(name)
77
- execute "REFRESH MATERIALIZED VIEW #{name};"
173
+ def refresh_materialized_view(name, concurrently: false)
174
+ raise_unless_materialized_views_supported
175
+
176
+ if concurrently
177
+ raise_unless_concurrent_refresh_supported
178
+ execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
179
+ else
180
+ execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
181
+ end
78
182
  end
79
183
 
80
184
  private
81
185
 
82
- def execute(sql, base = ActiveRecord::Base)
83
- base.connection.execute sql
186
+ attr_reader :connection
187
+ delegate :execute, :quote_table_name, to: :connection
188
+
189
+ def raise_unless_materialized_views_supported
190
+ unless connection.supports_materialized_views?
191
+ raise MaterializedViewsNotSupportedError
192
+ end
84
193
  end
85
194
 
86
- def view_from_database(result)
87
- Scenic::View.new(
88
- name: result["viewname"],
89
- definition: result["definition"].strip,
90
- materialized: result["materialized"] == "t",
91
- )
195
+ def raise_unless_concurrent_refresh_supported
196
+ unless connection.supports_concurrent_refreshes?
197
+ raise ConcurrentRefreshesNotSupportedError
198
+ end
92
199
  end
93
200
  end
94
201
  end