scenic 1.8.0 → 1.9.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -8
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +48 -19
  5. data/FUNDING.yml +1 -0
  6. data/Gemfile +2 -2
  7. data/README.md +71 -18
  8. data/lib/generators/scenic/materializable.rb +27 -1
  9. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
  10. data/lib/scenic/adapters/postgres/index_creation.rb +68 -0
  11. data/lib/scenic/adapters/postgres/index_migration.rb +70 -0
  12. data/lib/scenic/adapters/postgres/index_reapplication.rb +3 -28
  13. data/lib/scenic/adapters/postgres/side_by_side.rb +50 -0
  14. data/lib/scenic/adapters/postgres/temporary_name.rb +34 -0
  15. data/lib/scenic/adapters/postgres/views.rb +83 -10
  16. data/lib/scenic/adapters/postgres.rb +41 -18
  17. data/lib/scenic/schema_dumper.rb +0 -14
  18. data/lib/scenic/statements.rb +46 -13
  19. data/lib/scenic/version.rb +1 -1
  20. data/scenic.gemspec +5 -1
  21. data/spec/acceptance/user_manages_views_spec.rb +11 -0
  22. data/spec/dummy/config/application.rb +4 -0
  23. data/spec/generators/scenic/view/view_generator_spec.rb +26 -0
  24. data/spec/scenic/adapters/postgres/index_creation_spec.rb +54 -0
  25. data/spec/scenic/adapters/postgres/index_migration_spec.rb +24 -0
  26. data/spec/scenic/adapters/postgres/side_by_side_spec.rb +24 -0
  27. data/spec/scenic/adapters/postgres/temporary_name_spec.rb +23 -0
  28. data/spec/scenic/adapters/postgres_spec.rb +44 -3
  29. data/spec/scenic/command_recorder_spec.rb +18 -0
  30. data/spec/scenic/schema_dumper_spec.rb +29 -8
  31. data/spec/scenic/statements_spec.rb +62 -4
  32. data/spec/spec_helper.rb +19 -4
  33. data/spec/support/database_schema_helpers.rb +28 -0
  34. metadata +19 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9085d1f24783682a52d5874ce82bf44543b2a60d6edbfd72b026bf84b4bc0ab
4
- data.tar.gz: fd7efd82eb88c0550c410f50e363db99eaf3e7fc6b5e3ac4eb374bd681afd7cd
3
+ metadata.gz: 772babaa9c177e3f24e45e0b61c7bed0475b5267806bea563d4a7ec1e97830c9
4
+ data.tar.gz: af0d81ab175baa0b0ac35d11216f76bb9d1663f3483fde733a862cff92067a01
5
5
  SHA512:
6
- metadata.gz: d0c624619ae62c1c9e8186b8cde870e81dde6f47c8eacc93faad863c38d8fc57c3f15b81c0f1a515bad57318d2bef56f8a57b83934346542e4fbe9bbc6315209
7
- data.tar.gz: 6a4e9ae269ee6ffb86a4e23fa1d0e741ee2b3ed12fabca23696d2d462c4359d9fceea53ffbf696f72357bfa10a4abf66ede72b1bfa1a37ce7fcf5fdc5f6aeca7
6
+ metadata.gz: ac9d6aa1abb1bea6d69d50de6aad86e0a567abd2b1631645b2f62d1bbd687b362e5af7ebe6fde7ad94fc6203a76df57fb3893a4f677b0def61740bc7af1f92a5
7
+ data.tar.gz: 59b77cb6f2b51334a3cefb6ef121a25354c7d202bef2c9898202da84d4a6b889898cc6d6b5bcd7e38b37b020501cff80628c843467d6d3a03dee6f309fb0a860
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  steps:
15
15
  - name: Checkout
16
- uses: actions/checkout@v3
16
+ uses: actions/checkout@v4
17
17
 
18
18
  - name: Run standardrb
19
19
  uses: standardrb/standard-ruby-action@f533e61f461ccb766b2d9c235abf59be02aea793
@@ -26,16 +26,21 @@ jobs:
26
26
 
27
27
  build:
28
28
  name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
29
+ continue-on-error: ${{ matrix.continue-on-error }}
29
30
 
30
31
  strategy:
31
32
  fail-fast: false
32
33
  matrix:
33
- ruby: ["2.7", "3.0", "3.1", "3.2"]
34
- rails: ["6.1", "7.0"]
34
+ ruby: ["3.4", "3.3"]
35
+ rails: ["8.0", "7.2"]
35
36
  continue-on-error: [false]
36
- exclude:
37
- - ruby: "3.2"
38
- rails: "6.1"
37
+ include:
38
+ - ruby: "3.4"
39
+ rails: "main"
40
+ continue-on-error: true
41
+ - ruby: "head"
42
+ rails: "main"
43
+ continue-on-error: true
39
44
 
40
45
  runs-on: ubuntu-latest
41
46
 
@@ -60,7 +65,7 @@ jobs:
60
65
 
61
66
  steps:
62
67
  - name: Checkout
63
- uses: actions/checkout@v3
68
+ uses: actions/checkout@v4
64
69
 
65
70
  - name: Install Ruby ${{ matrix.ruby }}
66
71
  uses: ruby/setup-ruby@v1
@@ -74,7 +79,7 @@ jobs:
74
79
  run: bundle lock
75
80
 
76
81
  - name: Cache dependencies
77
- uses: actions/cache@v3
82
+ uses: actions/cache@v4
78
83
  with:
79
84
  path: vendor/bundle
80
85
  key: bundle-${{ hashFiles('Gemfile.lock') }}
data/.gitignore CHANGED
@@ -18,3 +18,4 @@ tmp
18
18
  gemfiles/*.lock
19
19
  .DS_Store
20
20
  .ruby-version
21
+ .vscode/
data/CHANGELOG.md CHANGED
@@ -5,14 +5,30 @@ changelog, see the [commits] for each version via the version links.
5
5
 
6
6
  [commits]: https://github.com/scenic-views/scenic/commits/master
7
7
 
8
+ ## [1.9.0] - June 30, 2025
9
+
10
+ [1.9.0]: https://github.com/scenic-views/scenic/compare/v1.8.0...v1.9.0
11
+
12
+ ### Added
13
+
14
+ - Added topological sorting of views when dumping schema. This should limit
15
+ unrelated changes to schema.rb once the views are sorted the first time.
16
+ - Added `side_by_side` mode to materialized view updates. This creates the
17
+ updated materialized view with a temporary name before swapping it with the
18
+ live view, minimizing the time the view is locked for updating.
19
+
20
+ ### Fixed
21
+
22
+ - Only refresh concurrently if the materialized view is populated.
23
+
8
24
  ## [1.8.0] - March 28, 2024
9
25
 
10
26
  [1.8.0]: https://github.com/scenic-views/scenic/compare/v1.7.0...v1.8.0
11
27
 
12
28
  ### Added
13
29
 
14
- * Added `#populated?` to check the state of materialized views - *Daisuke
15
- Fujimura*, *Dr Nic Williams*
30
+ - Added `#populated?` to check the state of materialized views - _Daisuke
31
+ Fujimura_, _Dr Nic Williams_
16
32
 
17
33
  ## [1.7.0] - December 8, 2022
18
34
 
@@ -20,13 +36,13 @@ changelog, see the [commits] for each version via the version links.
20
36
 
21
37
  ### Added
22
38
 
23
- * Added the `--replace` CLI flag to generate a migration that uses the
24
- `replace_view` schema statement - *Dan Hixon*
39
+ - Added the `--replace` CLI flag to generate a migration that uses the
40
+ `replace_view` schema statement - _Dan Hixon_
25
41
 
26
42
  ### Fixed
27
43
 
28
- * Fixed deprecation notice from newer versions of ERB when using scenic
29
- generators - *Ali Ismayilov*
44
+ - Fixed deprecation notice from newer versions of ERB when using scenic
45
+ generators - _Ali Ismayilov_
30
46
 
31
47
  ## [1.6.0] - February 13, 2022
32
48
 
@@ -34,9 +50,9 @@ changelog, see the [commits] for each version via the version links.
34
50
 
35
51
  ### Fixed
36
52
 
37
- * Exclude pg_stat_statements_info (#349) 76bface - *Caleb Hearth*
38
- * Fix serialization of views with backslashes c625d1b - *Ben Sheldon*
39
- * Handle ActiveRecord table name prefix and suffix b1544dc - *Derek Prior*
53
+ - Exclude pg*stat_statements_info (#349) 76bface - \_Caleb Hearth*
54
+ - Fix serialization of views with backslashes c625d1b - _Ben Sheldon_
55
+ - Handle ActiveRecord table name prefix and suffix b1544dc - _Derek Prior_
40
56
 
41
57
  ## [1.5.5] - December 15, 2021
42
58
 
@@ -109,7 +125,6 @@ changelog, see the [commits] for each version via the version links.
109
125
  - Fixed a cascading refresh issue when the name of the view to trigger the
110
126
  refresh is a substring of one of its dependencies.
111
127
 
112
-
113
128
  [1.5.0]: https://github.com/scenic-views/scenic/compare/v1.4.1...v1.5.0
114
129
 
115
130
  ## [1.4.1] - December 15, 2017
@@ -144,26 +159,30 @@ changelog, see the [commits] for each version via the version links.
144
159
  ## [1.3.0] - May 27, 2016
145
160
 
146
161
  ### Added
162
+
147
163
  - Add `replace_view` migration statement, which issues `CREATE OR REPLACE
148
- VIEW` rather than `CREATE VIEW` or `DROP VIEW` and `CREATE VIEW`.
164
+ VIEW` rather than `CREATE VIEW` or `DROP VIEW` and `CREATE VIEW`.
149
165
  - Schema-qualify views outside the 'public' namespace, such as
150
166
  `scenic.searches`
151
167
 
152
168
  ### Fixed
153
- * Singularize generated model name when injecting into class.
169
+
170
+ - Singularize generated model name when injecting into class.
154
171
  Previously, pluralized names would issue a warning and Scenic would
155
172
  attempt to insert model code into the pluralized model file.
156
- * Convert shell-based smoke tests to RSpec syntax.
173
+ - Convert shell-based smoke tests to RSpec syntax.
157
174
 
158
175
  [1.3.0]: https://github.com/scenic-views/scenic/compare/v1.2.0...v1.3.0
159
176
 
160
177
  ## [1.2.0] - February 5, 2016
161
178
 
162
179
  ### Added
180
+
163
181
  - The generators now accept namespaced view definitions. For example: `rails
164
- generate scenic:view my_app.users`.
182
+ generate scenic:view my_app.users`.
165
183
 
166
184
  ### Fixed
185
+
167
186
  - Materialized view indexes are now properly dumped to `db/schema.rb`. This was
168
187
  an oversight in previous releases, meaning `rake db:schema:load` was missing
169
188
  indexes.
@@ -172,7 +191,7 @@ changelog, see the [commits] for each version via the version links.
172
191
  returning no indexes.
173
192
 
174
193
  **Note**: Dumping materialized view indexes will produce an invalid
175
- `db/schema.rb` file under Rails 5 beta 1 and beta 2. This is fixed on Rails
194
+ `db/schema.rb` file under Rails 5 beta 1 and beta 2. This is fixed on Rails
176
195
  master.
177
196
 
178
197
  [1.2.0]: https://github.com/scenic-views/scenic/compare/v1.1.1...v1.2.0
@@ -180,8 +199,9 @@ master.
180
199
  ## [1.1.1] - January 29, 2016
181
200
 
182
201
  ### Fixed
202
+
183
203
  - Some schema operations were failing with a `PG::ConnectionBad: connection is
184
- closed` error. This has been fixed by ensuring we grab a fresh connection for
204
+ closed` error. This has been fixed by ensuring we grab a fresh connection for
185
205
  all operations.
186
206
 
187
207
  [1.1.1]: https://github.com/scenic-views/scenic/compare/v1.1.0...v1.1.1
@@ -189,12 +209,14 @@ master.
189
209
  ## [1.1.0] - January 8, 2016
190
210
 
191
211
  ### Added
212
+
192
213
  - Added support for updating materialized view definitions while maintaining
193
214
  existing indexes that are still applicable after the update.
194
215
  - Added support for refreshing materialized views concurrently (requires
195
216
  Postgres 9.4 or newer).
196
217
 
197
218
  ### Fixed
219
+
198
220
  - The schema dumper will now dump views and materialized views together in the
199
221
  order they are returned by Postgres. This fixes issues when loading views that
200
222
  depend on other views via `rake db:schema:load`.
@@ -209,26 +231,30 @@ master.
209
231
  ## [1.0.0] - November 23, 2015
210
232
 
211
233
  ### Added
234
+
212
235
  - Added support for [materialized views].
213
236
  - Allow changing the database adapter via `Scenic::Configuration`.
214
237
 
215
238
  ### Fixed
239
+
216
240
  - Improved formatting of the view when dumped to `schema.rb`.
217
241
  - Fixed generation of namespaced models by using ActiveRecord's own model
218
242
  generator.
219
243
  - Eliminated `alias_method_chain` deprecation when running with Rails master
220
244
  (5.0).
221
245
 
222
- [materialized views]:https://github.com/scenic-views/scenic/blob/v1.0.0/README.md
246
+ [materialized views]: https://github.com/scenic-views/scenic/blob/v1.0.0/README.md
223
247
  [1.0.0]: https://github.com/scenic-views/scenic/compare/v0.3.0...v1.0.0
224
248
 
225
249
  ## [0.3.0] - January 23, 2015
226
250
 
227
251
  ### Added
252
+
228
253
  - Previous view definition is copied into new view definition file when updating
229
254
  an existing view.
230
255
 
231
256
  ### Fixed
257
+
232
258
  - We avoid dumping views that belong to Postgres extensions.
233
259
  - `db/schema.rb` is prettier thanks to a blank line after each view definition.
234
260
 
@@ -237,6 +263,7 @@ master.
237
263
  ## [0.2.1] - January 5, 2015
238
264
 
239
265
  ### Fixed
266
+
240
267
  - View generator will now create `db/views` directory if necessary.
241
268
 
242
269
  [0.2.1]: https://github.com/scenic-views/scenic/compare/v0.2.0...v0.2.1
@@ -244,9 +271,11 @@ master.
244
271
  ## [0.2.0] - August 11, 2014
245
272
 
246
273
  ### Added
274
+
247
275
  - Teach view generator to update existing views.
248
276
 
249
277
  ### Fixed
278
+
250
279
  - Raise an error if view definition is empty.
251
280
 
252
281
  [0.2.0]: https://github.com/scenic-views/scenic/compare/v0.1.0...v0.2.0
@@ -256,8 +285,8 @@ master.
256
285
  Scenic makes it easier to work with Postgres views in Rails.
257
286
 
258
287
  It introduces view methods to ActiveRecord::Migration and allows views to be
259
- dumped to db/schema.rb. It provides generators for models, view definitions,
260
- and migrations. It is built around a basic versioning system for view
288
+ dumped to db/schema.rb. It provides generators for models, view definitions,
289
+ and migrations. It is built around a basic versioning system for view
261
290
  definition files.
262
291
 
263
292
  In short, go add a view to your app.
data/FUNDING.yml ADDED
@@ -0,0 +1 @@
1
+ github: [calebhearth, derekprior]
data/Gemfile CHANGED
@@ -3,9 +3,9 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in scenic.gemspec
4
4
  gemspec
5
5
 
6
- rails_version = ENV.fetch("RAILS_VERSION", "6.1")
6
+ rails_version = ENV.fetch("RAILS_VERSION", "8.0")
7
7
 
8
- rails_constraint = if rails_version == "master"
8
+ rails_constraint = if rails_version == "main"
9
9
  {github: "rails/rails"}
10
10
  else
11
11
  "~> #{rails_version}.0"
data/README.md CHANGED
@@ -24,7 +24,7 @@ Scenic ships with support for PostgreSQL. The adapter is configurable (see
24
24
 
25
25
  If you're using Postgres, Add `gem "scenic"` to your Gemfile and run `bundle
26
26
  install`. If you're using something other than Postgres, check out the available
27
- [third party adapters](https://github.com/scenic-views/scenic#faqs).
27
+ [third-party adapters](https://github.com/scenic-views/scenic#when-will-you-support-mysql-sqlite-or-other-databases).
28
28
 
29
29
  ## Great, how do I create a view?
30
30
 
@@ -91,7 +91,8 @@ hierarchies of dependent views.
91
91
 
92
92
  Scenic offers a `replace_view` schema statement, resulting in a `CREATE OR
93
93
  REPLACE VIEW` SQL query which will update the supplied view in place, retaining
94
- all dependencies. Materialized views cannot be replaced in this fashion.
94
+ all dependencies. Materialized views cannot be replaced in this fashion, though
95
+ the `side_by_side` update strategy may yield similar results (see below).
95
96
 
96
97
  You can generate a migration that uses the `replace_view` schema statement by
97
98
  passing the `--replace` option to the `scenic:view` generator:
@@ -137,7 +138,7 @@ end
137
138
  ```
138
139
 
139
140
  Scenic even provides a `scenic:model` generator that is a superset of
140
- `scenic:view`. It will act identically to the Rails `model` generator except
141
+ `scenic:view`. It will act identically to the Rails `model` generator except
141
142
  that it will create a Scenic view migration rather than a table migration.
142
143
 
143
144
  There is no special base class or mixin needed. If desired, any code the model
@@ -185,6 +186,44 @@ you would need to refresh view B first, then right after refresh view A. If you
185
186
  would like this cascading refresh of materialized views, set `cascade: true`
186
187
  when you refresh your materialized view.
187
188
 
189
+ ## Can I update the definition of a materialized view without dropping it?
190
+
191
+ No, but Scenic can help you approximate this behavior with its `side_by_side`
192
+ update strategy.
193
+
194
+ Generally, changing the definition of a materialized view requires dropping it
195
+ and recreating it, either without data or with a non-concurrent refresh. The
196
+ materialized view will be locked for selects during the refresh process, which
197
+ can cause problems in your application if the refresh is not fast.
198
+
199
+ The `side_by_side` update strategy prepares the new version of the view under a
200
+ temporary name. This includes copying the indexes from the original view and
201
+ refreshing the data. Once prepared, the original view is dropped and the new
202
+ view is renamed to the original view's name. This process minimizes the time the
203
+ view is locked for selects at the cost of additional disk space.
204
+
205
+ You can generate a migration that uses the `side_by_side` strategy by passing
206
+ the `--side-by-side` option to the `scenic:view` generator:
207
+
208
+ ```sh
209
+ $ rails generate scenic:view search_results --materialized --side-by-side
210
+ create db/views/search_results_v02.sql
211
+ create db/migrate/[TIMESTAMP]_update_search_results_to_version_2.rb
212
+ ```
213
+
214
+ The migration will look something like this:
215
+
216
+ ```ruby
217
+ class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
218
+ def change
219
+ update_view :search_results,
220
+ version: 2,
221
+ revert_to_version: 1,
222
+ materialized: { side_by_side: true }
223
+ end
224
+ end
225
+ ```
226
+
188
227
  ## I don't need this view anymore. Make it go away.
189
228
 
190
229
  Scenic gives you `drop_view` too:
@@ -198,7 +237,7 @@ end
198
237
 
199
238
  ## FAQs
200
239
 
201
- **Why do I get an error when querying a view-backed model with `find`, `last`, or `first`?**
240
+ ### Why do I get an error when querying a view-backed model with `find`, `last`, or `first`?
202
241
 
203
242
  ActiveRecord's `find` method expects to query based on your model's primary key,
204
243
  but views do not have primary keys. Additionally, the `first` and `last` methods
@@ -213,7 +252,7 @@ class People < ApplicationRecord
213
252
  end
214
253
  ```
215
254
 
216
- **Why is my view missing columns from the underlying table?**
255
+ ### Why is my view missing columns from the underlying table?
217
256
 
218
257
  Did you create the view with `SELECT [table_name].*`? Most (possibly all)
219
258
  relational databases freeze the view definition at the time of creation. New
@@ -226,7 +265,7 @@ add_column :posts, :title, :string
226
265
  update_view :posts_with_aggregate_data, version: 2, revert_to_version: 2
227
266
  ```
228
267
 
229
- **When will you support MySQL, SQLite, or other databases?**
268
+ ### When will you support MySQL, SQLite, or other databases?
230
269
 
231
270
  We have no plans to add first-party adapters for other relational databases at
232
271
  this time because we (the maintainers) do not currently have a use for them.
@@ -234,7 +273,7 @@ It's our experience that maintaining a library effectively requires regular use
234
273
  of its features. We're not in a good position to support MySQL, SQLite or other
235
274
  database users.
236
275
 
237
- Scenic *does* support configuring different database adapters and should be
276
+ Scenic _does_ support configuring different database adapters and should be
238
277
  extendable with adapter libraries. If you implement such an adapter, we're happy
239
278
  to review and link to it. We're also happy to make changes that would better
240
279
  accommodate adapter gems.
@@ -242,25 +281,39 @@ accommodate adapter gems.
242
281
  We are aware of the following existing adapter libraries for Scenic which may
243
282
  meet your needs:
244
283
 
245
- * [`scenic_sqlite_adapter`](<https://github.com/pdebelak/scenic_sqlite_adapter>)
246
- * [`scenic-mysql_adapter`](<https://github.com/EmpaticoOrg/scenic-mysql_adapter>)
247
- * [`scenic-sqlserver-adapter`](<https://github.com/ClickMechanic/scenic_sqlserver_adapter>)
248
- * [`scenic-oracle_adapter`](<https://github.com/cdinger/scenic-oracle_adapter>)
284
+ - [`scenic_sqlite_adapter`](https://github.com/pdebelak/scenic_sqlite_adapter)
285
+ - [`scenic-mysql_adapter`](https://github.com/cainlevy/scenic-mysql_adapter)
286
+ - [`scenic-sqlserver-adapter`](https://github.com/ClickMechanic/scenic_sqlserver_adapter)
287
+ - [`scenic-oracle_adapter`](https://github.com/cdinger/scenic-oracle_adapter)
249
288
 
250
289
  Please note that the maintainers of Scenic make no assertions about the
251
290
  quality or security of the above adapters.
252
291
 
253
- **Related projects**
292
+ ## About
293
+
294
+ ### Used By
254
295
 
255
- - [`fx`](<https://github.com/teoljungberg/fx>) Versioned database functions and
296
+ Scenic is used by some popular open source Rails apps:
297
+ [Mastodon](https://github.com/mastodon/mastodon/),
298
+ [Code.org](https://github.com/code-dot-org/code-dot-org), and
299
+ [Lobste.rs](https://github.com/lobsters/lobsters/).
300
+
301
+ ### Related projects
302
+
303
+ - [`fx`](https://github.com/teoljungberg/fx) Versioned database functions and
256
304
  triggers for Rails
257
305
 
258
- ## About
306
+ ### Media
259
307
 
260
- Scenic is used by some popular open source Rails apps:
261
- [Mastodon](<https://github.com/mastodon/mastodon/>),
262
- [Code.org](<https://github.com/code-dot-org/code-dot-org>), and
263
- [Lobste.rs](<https://github.com/lobsters/lobsters/>).
308
+ Here are a few posts we've seen discussing Scenic:
309
+
310
+ - [Announcing Scenic - Versioned Database Views for Rails](https://thoughtbot.com/blog/announcing-scenic--versioned-database-views-for-rails) by Derek Prior for thoughtbot
311
+ - [Effectively Using Materialized Views in Ruby on Rails](https://pganalyze.com/blog/materialized-views-ruby-rails) by Leigh Halliday for pganalyze
312
+ - [Optimizing String Concatenation in Ruby on Rails](https://dev.to/pimp_my_ruby/from-slow-to-lightning-fast-optimizing-string-concatenation-in-ruby-on-rails-28nk)
313
+ - [Materialized Views In Ruby On Rails With Scenic](https://www.ideamotive.co/blog/materialized-views-ruby-rails-scenic) by Dawid Karczewski for Ideamotive
314
+ - [Using Scenic and SQL views to aggregate data](https://dev.to/weareredlight/using-scenic-and-sql-views-to-aggregate-data-226k) by André Perdigão for Redlight Software
315
+
316
+ ### Maintainers
264
317
 
265
318
  Scenic is maintained by [Derek Prior], [Caleb Hearth], and you, our
266
319
  contributors.
@@ -14,7 +14,14 @@ module Scenic
14
14
  type: :boolean,
15
15
  required: false,
16
16
  desc: "Adds WITH NO DATA when materialized view creates/updates",
17
- default: false
17
+ default: false,
18
+ aliases: ["--no-data"]
19
+ class_option :side_by_side,
20
+ type: :boolean,
21
+ required: false,
22
+ desc: "Uses side-by-side strategy to update materialized view",
23
+ default: false,
24
+ aliases: ["--side-by-side"]
18
25
  class_option :replace,
19
26
  type: :boolean,
20
27
  required: false,
@@ -35,6 +42,25 @@ module Scenic
35
42
  def no_data?
36
43
  options[:no_data]
37
44
  end
45
+
46
+ def side_by_side?
47
+ options[:side_by_side]
48
+ end
49
+
50
+ def materialized_view_update_options
51
+ set_options = {no_data: no_data?, side_by_side: side_by_side?}
52
+ .select { |_, v| v }
53
+
54
+ if set_options.empty?
55
+ "true"
56
+ else
57
+ string_options = set_options.reduce("") do |memo, (key, value)|
58
+ memo + "#{key}: #{value}, "
59
+ end
60
+
61
+ "{ #{string_options.chomp(", ")} }"
62
+ end
63
+ end
38
64
  end
39
65
  end
40
66
  end
@@ -5,7 +5,7 @@ class <%= migration_class_name %> < <%= activerecord_migration_class %>
5
5
  <%= method_name %> <%= formatted_plural_name %>,
6
6
  version: <%= version %>,
7
7
  revert_to_version: <%= previous_version %>,
8
- materialized: <%= no_data? ? "{ no_data: true }" : true %>
8
+ materialized: <%= materialized_view_update_options %>
9
9
  <%- else -%>
10
10
  <%= method_name %> <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
11
11
  <%- end -%>
@@ -0,0 +1,68 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Used to resiliently create indexes on a materialized view. If the index
5
+ # cannot be applied to the view (e.g. the columns don't exist any longer),
6
+ # we log that information and continue rather than raising an error. It is
7
+ # left to the user to judge whether the index is necessary and recreate
8
+ # it.
9
+ #
10
+ # Used when updating a materialized view to ensure the new version has all
11
+ # apprioriate indexes.
12
+ #
13
+ # @api private
14
+ class IndexCreation
15
+ # Creates the index creation object.
16
+ #
17
+ # @param connection [Connection] The connection to execute SQL against.
18
+ # @param speaker [#say] (ActiveRecord::Migration) The object used for
19
+ # logging the results of creating indexes.
20
+ def initialize(connection:, speaker: ActiveRecord::Migration.new)
21
+ @connection = connection
22
+ @speaker = speaker
23
+ end
24
+
25
+ # Creates the provided indexes. If an index cannot be created, it is
26
+ # logged and the process continues.
27
+ #
28
+ # @param indexes [Array<Scenic::Index>] The indexes to create.
29
+ #
30
+ # @return [void]
31
+ def try_create(indexes)
32
+ Array(indexes).each(&method(:try_index_create))
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :connection, :speaker
38
+
39
+ def try_index_create(index)
40
+ success = with_savepoint(index.index_name) do
41
+ connection.execute(index.definition)
42
+ end
43
+
44
+ if success
45
+ say "index '#{index.index_name}' on '#{index.object_name}' has been created"
46
+ else
47
+ say "index '#{index.index_name}' on '#{index.object_name}' is no longer valid and has been dropped."
48
+ end
49
+ end
50
+
51
+ def with_savepoint(name)
52
+ connection.execute("SAVEPOINT #{name}")
53
+ yield
54
+ connection.execute("RELEASE SAVEPOINT #{name}")
55
+ true
56
+ rescue
57
+ connection.execute("ROLLBACK TO SAVEPOINT #{name}")
58
+ false
59
+ end
60
+
61
+ def say(message)
62
+ subitem = true
63
+ speaker.say(message, subitem)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,70 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Used during side-by-side materialized view updates to migrate indexes
5
+ # from the original view to the new view.
6
+ #
7
+ # @api private
8
+ class IndexMigration
9
+ # Creates the index migration object.
10
+ #
11
+ # @param connection [Connection] The connection to execute SQL against.
12
+ # @param speaker [#say] (ActiveRecord::Migration) The object used for
13
+ # logging the results of migrating indexes.
14
+ def initialize(connection:, speaker: ActiveRecord::Migration.new)
15
+ @connection = connection
16
+ @speaker = speaker
17
+ end
18
+
19
+ # Retreives the indexes on the original view, renames them to avoid
20
+ # collisions, retargets the indexes to the destination view, and then
21
+ # creates the retargeted indexes.
22
+ #
23
+ # @param from [String] The name of the original view.
24
+ # @param to [String] The name of the destination view.
25
+ #
26
+ # @return [void]
27
+ def migrate(from:, to:)
28
+ source_indexes = Indexes.new(connection: connection).on(from)
29
+ retargeted_indexes = source_indexes.map { |i| retarget(i, to: to) }
30
+ source_indexes.each(&method(:rename))
31
+
32
+ if source_indexes.any?
33
+ say "indexes on '#{from}' have been renamed to avoid collisions"
34
+ end
35
+
36
+ IndexCreation
37
+ .new(connection: connection, speaker: speaker)
38
+ .try_create(retargeted_indexes)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :connection, :speaker
44
+
45
+ def retarget(index, to:)
46
+ new_definition = index.definition.sub(
47
+ /ON (.*)\.#{index.object_name}/,
48
+ 'ON \1.' + to + " "
49
+ )
50
+
51
+ Scenic::Index.new(
52
+ object_name: to,
53
+ index_name: index.index_name,
54
+ definition: new_definition
55
+ )
56
+ end
57
+
58
+ def rename(index)
59
+ temporary_name = TemporaryName.new(index.index_name).to_s
60
+ connection.rename_index(index.object_name, index.index_name, temporary_name)
61
+ end
62
+
63
+ def say(message)
64
+ subitem = true
65
+ speaker.say(message, subitem)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end