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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +14 -4
- data/.yardopts +0 -1
- data/Appraisals +26 -0
- data/CONTRIBUTING.md +5 -4
- data/LICENSE.txt +1 -1
- data/NEWS.md +49 -0
- data/README.md +71 -28
- data/bin/appraisal +16 -0
- data/bin/setup +5 -0
- data/bin/yard +16 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails41.gemfile +8 -0
- data/gemfiles/rails42.gemfile +8 -0
- data/gemfiles/rails50.gemfile +13 -0
- data/lib/generators/scenic/generators.rb +1 -0
- data/lib/generators/scenic/model/templates/model.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
- data/lib/generators/scenic/view/view_generator.rb +13 -1
- data/lib/scenic/adapters/postgres/connection.rb +57 -0
- data/lib/scenic/adapters/postgres/errors.rb +26 -0
- data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
- data/lib/scenic/adapters/postgres/indexes.rb +53 -0
- data/lib/scenic/adapters/postgres/views.rb +51 -0
- data/lib/scenic/adapters/postgres.rb +144 -33
- data/lib/scenic/configuration.rb +2 -2
- data/lib/scenic/index.rb +36 -0
- data/lib/scenic/schema_dumper.rb +12 -5
- data/lib/scenic/statements.rb +7 -13
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +1 -4
- data/lib/scenic.rb +1 -0
- data/scenic.gemspec +4 -1
- data/spec/dummy/config/application.rb +3 -0
- data/spec/generators/scenic/view/view_generator_spec.rb +12 -0
- data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
- data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
- data/spec/scenic/adapters/postgres_spec.rb +88 -26
- data/spec/scenic/statements_spec.rb +14 -15
- data/spec/smoke +24 -3
- data/spec/spec_helper.rb +4 -0
- metadata +64 -8
- data/spec/dummy/config/environments/development.rb +0 -6
- data/spec/dummy/config/environments/test.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1617d9008cabf9f0117f71f812735e8f89bf932
|
|
4
|
+
data.tar.gz: 31babdfa7c8fc2bc200f9457bc6f8fcab9f16a8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aebbd7f6d99c20b35420a68977a4231849543393c01d2a37487cfcde775cc26b33af792d13b61a4618db12dfe4eef0fc0a581ad27a5b76842d4701c0452302db
|
|
7
|
+
data.tar.gz: 05ea1809e4c4777903a297a9ee1e8c83d59ac3550aa455a82ef2d0e52f0a5f715211ed721e1f425aa87f4d08e7f04c7e7c9b1bec284861ab667e555eefe226c0
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
addons:
|
|
2
|
-
postgresql: "9.
|
|
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
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
|
|
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
|
-
##
|
|
11
|
+
## Contributing Code
|
|
12
12
|
|
|
13
|
-
1.
|
|
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
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
|
+

|
|
4
|
+
|
|
5
|
+
[](https://travis-ci.org/thoughtbot/scenic)
|
|
6
|
+
[](https://codeclimate.com/repos/53c9736269568066a3000c35/feed)
|
|
7
|
+
[](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 `
|
|
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
|
|
26
|
-
create db/views/
|
|
27
|
-
create db/migrate/[TIMESTAMP]
|
|
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/
|
|
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
|
|
66
|
-
create db/views/
|
|
67
|
-
create db/migrate/[TIMESTAMP]
|
|
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 `
|
|
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,
|
|
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
|
|
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 :
|
|
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
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,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 => "../"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration
|
|
2
2
|
def change
|
|
3
3
|
<%- if materialized? -%>
|
|
4
|
-
update_view
|
|
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
|
|
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
|
-
|
|
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
|