scenic 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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