-
-
Notifications
You must be signed in to change notification settings - Fork 226
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for CASCADE in drop_view and update_view #218
Add support for CASCADE in drop_view and update_view #218
Conversation
def connection | ||
ActiveRecord::Base.connection | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra empty line detected at module body end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -0,0 +1,27 @@ | |||
module MigrationHelpers | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra empty line detected at module body beginning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -87,7 +87,7 @@ module Scenic | |||
connection.update_view(:name, version: 3) | |||
|
|||
expect(Scenic.database).to have_received(:update_view) | |||
.with(:name, definition.to_sql) | |||
.with(:name, definition.to_sql, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Place the . on the previous line, together with the method call receiver.
args = [:users, { cascade: true, revert_to_version: 3 }] | ||
|
||
expect { recorder.revert { recorder.drop_view(*args) } } | ||
.to raise_error(ActiveRecord::IrreversibleMigration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Place the . on the previous line, together with the method call receiver.
end | ||
end | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra blank line detected.
Extra empty line detected at block body end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spec/integration/cascade_spec.rb
Outdated
end | ||
end | ||
|
||
it 'recreates the dependent view' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
|
||
describe 'as part of updating a view' do | ||
around do |example| | ||
with_view_definition :greetings, 2, "SELECT text 'good day' AS greeting" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [81/80]
}.to_not raise_error | ||
end | ||
|
||
describe 'as part of updating a view' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
|
||
it 'works' do | ||
run_migration(migration_for_create, :up) | ||
expect { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using {...} for multi-line blocks.
end | ||
end | ||
|
||
it 'works' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
Oh, also, this is related to #96 |
# @return The database response from executing the create statement. | ||
# | ||
# @example | ||
# update_view :engagement_reports, version: 3, revert_to_version: 2 | ||
# | ||
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false) | ||
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false, cascade: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused method argument - revert_to_version.
Line is too long. [121/80]
# @return The database response from executing the drop statement. | ||
# | ||
# @example Drop a view, rolling back to version 3 on rollback | ||
# drop_view(:users_who_recently_logged_in, revert_to_version: 3) | ||
# | ||
def drop_view(name, revert_to_version: nil, materialized: false) | ||
def drop_view(name, revert_to_version: nil, materialized: false, cascade: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused method argument - revert_to_version.
Line is too long. [84/80]
# @return The database response from executing the create statement. | ||
# | ||
# @example | ||
# update_view :engagement_reports, version: 3, revert_to_version: 2 | ||
# | ||
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false) | ||
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false, cascade: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused method argument - revert_to_version.
Line is too long. [121/80]
# @return The database response from executing the drop statement. | ||
# | ||
# @example Drop a view, rolling back to version 3 on rollback | ||
# drop_view(:users_who_recently_logged_in, revert_to_version: 3) | ||
# | ||
def drop_view(name, revert_to_version: nil, materialized: false) | ||
def drop_view(name, revert_to_version: nil, materialized: false, cascade: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused method argument - revert_to_version.
Line is too long. [84/80]
I've found a bug where this isn't working properly if you're updating a materialized view. Will push a fix! |
@@ -45,6 +45,11 @@ def perform_scenic_inversion(method, args) | |||
raise ActiveRecord::IrreversibleMigration, message | |||
end | |||
|
|||
if method == :create_view && scenic_args.cascade | |||
message = "#{method} is not reversible if dependent objects were also dropped with CASCADE" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [99/80]
create_materialized_view view.name, view.definition | ||
# Also recreate any indexes that were lost | ||
lost_indexes = indexes.select{|index| index.object_name == view.name} | ||
lost_indexes.each{|index| index_reapplier.try_index_create index} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing to the left of {.
Space between { and | missing.
Space missing inside }.
if view.materialized | ||
create_materialized_view view.name, view.definition | ||
# Also recreate any indexes that were lost | ||
lost_indexes = indexes.select{|index| index.object_name == view.name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing to the left of {.
Space between { and | missing.
Space missing inside }.
Line is too long. [81/80]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lost_indexes = indexes.select{|index| index.object_name == view.name} | |
lost_indexes = indexes.select { |index| index.object_name == view.name } |
index_reapplier = IndexReapplication.new(connection: connection) | ||
|
||
# Find any views that were lost | ||
dropped_views = old_views.reject{|ov| current_views.any?{|cv| ov.name == cv.name}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing to the left of {.
Space between { and | missing.
Line is too long. [90/80]
Space missing inside }.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dropped_views = old_views.reject{|ov| current_views.any?{|cv| ov.name == cv.name}} | |
dropped_views = old_views.reject do |ov| | |
current_views.any? { |cv| ov.name == cv.name } | |
end |
@@ -238,6 +268,24 @@ def refresh_dependencies_for(name) | |||
connection, | |||
) | |||
end | |||
|
|||
def recreate_dropped_views(old_views, current_views, indexes=[]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surrounding space missing in default value assignment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def recreate_dropped_views(old_views, current_views, indexes=[]) | |
def recreate_dropped_views(old_views, current_views, indexes = []) |
|
||
# Get indexes of existing materialized views | ||
indexes = Indexes.new(connection: connection) | ||
view_indexes = existing_views.select(&:materialized).flat_map do |view| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [81/80]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
view_indexes = existing_views.select(&:materialized).flat_map do |view| | |
potential_dependent_view_indexes = potential_dependent_views. | |
select(&:materialized). | |
flat_map { |view| indexes.on(view.name) } |
# Get existing views that could be dependent on this one. | ||
existing_views = views.reject{|v| v.name == name} | ||
|
||
# Get indexes of existing materialized views |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing whitespace detected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Get indexes of existing materialized views | |
# Get indexes of existing materialized views |
raise_unless_materialized_views_supported | ||
|
||
if cascade | ||
# Get existing views that could be dependent on this one. | ||
existing_views = views.reject{|v| v.name == name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing to the left of {.
Space between { and | missing.
Space missing inside }.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existing_views = views.reject{|v| v.name == name} | |
existing_views = views.reject { |v| v.name == name } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existing_views = views.reject{|v| v.name == name} | |
potential_dependent_views = views.reject{|v| v.name == name} |
# | ||
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres | ||
# in use does not support materialized views. | ||
# | ||
# @return [void] | ||
def update_materialized_view(name, sql_definition) | ||
def update_materialized_view(name, sql_definition, cascade=false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surrounding space missing in default value assignment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def update_materialized_view(name, sql_definition, cascade=false) | |
def update_materialized_view(name, sql_definition, cascade = false) |
def drop_view(name) | ||
execute "DROP VIEW #{quote_table_name(name)};" | ||
def drop_view(name, cascade=false) | ||
execute "DROP VIEW #{quote_table_name(name)}#{" CASCADE" if cascade};" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer single-quoted strings inside interpolations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execute "DROP VIEW #{quote_table_name(name)}#{" CASCADE" if cascade};" | |
execute "DROP VIEW #{quote_table_name(name)}#{' CASCADE' if cascade};" |
Hound violations aside, I'd appreciate some suggestions on how to refactor the duplicated code, and also my use of |
We've found an issue with the order in which views are captured and recreated; they are not in dependency order. Does anyone know of a way of asking for PSQL to return you views in order of dependency, or get the dependency tree for a bunch of views? |
Hello folks! This feature looks like a great addition 👍 Based on this article, I took a stab at a query to find PSQL views with their dependency order: WITH RECURSIVE view_dependency_tree(parent_schema, parent_obj, child_schema, child_obj, ind, ord) AS
(
SELECT
vtu_parent.view_schema,
vtu_parent.view_name,
vtu_parent.table_schema,
vtu_parent.table_name,
'',
array[row_number() over (ORDER BY view_schema, view_name)]
FROM information_schema.view_table_usage vtu_parent
WHERE vtu_parent.view_schema = 'public'
UNION ALL
SELECT
vtu_child.view_schema,
vtu_child.view_name,
vtu_child.table_schema,
vtu_child.table_name,
vtu_parent.ind || ' ',
vtu_parent.ord || (row_number() over (ORDER BY view_schema, view_name))
FROM view_dependency_tree vtu_parent, information_schema.view_table_usage vtu_child
WHERE vtu_child.view_schema = vtu_parent.child_schema
AND vtu_child.view_name = vtu_parent.child_obj
)
SELECT
tree.parent_obj AS parent,
tree.child_obj AS child,
tree.ord
FROM view_dependency_tree tree
INNER JOIN information_schema.views view
ON view.table_name = tree.child_obj
ORDER BY ord; It seems to work for my views locally, hope it's helpful! |
@annarankin this looks super useful! I’ll see if I can get it wired in |
I had a problem with the This gives me a set of views (materialized or otherwise) and the 'level' on the tree they are on. This could be combined with our 'what views disappeared?' code to recreate them in the correct order: WITH RECURSIVE t AS
-- Get every view & materialized view, assign a level 0
( SELECT c.oid,
c.relname,
0 AS LEVEL
FROM pg_class c
JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
WHERE c.relkind IN ('v', 'm') AND pg_namespace.nspname = 'public'
-- Union back on ourselves, increasing the level to indicate that the view is dependent
UNION ALL SELECT c.oid,
c.relname,
a.level+1
FROM t a
JOIN pg_depend d ON d.refobjid=a.oid
JOIN pg_rewrite w ON w.oid= d.objid AND w.ev_class!=a.oid
JOIN pg_class c ON c.oid=w.ev_class)
-- Take the max level for each view.
SELECT relname, MAX(level) AS level
FROM t
GROUP BY relname; It's slow, about 3s on my DB. It's also PSQL specific, so we'd still need some other solution for other versions of SQL that have dependent non-materialized views. |
Nice! Just for some qualitative feedback, I tried it on my local; it's pretty quick (~6ms) for 47 views and a max level of 5. |
That's a lot of dependent views! That must've been frustrating without cascade support... Another issue we've encountered: If we have dependent views with a |
whats the status of this? we have the same problem at my company. If this is going to go in, I'd be happy to work on fixing the hound/code climate violations, but I wouldn't want to waste that time if this won't be going in scenic |
drop_view(name) | ||
def update_view(name, sql_definition, cascade=false) | ||
if cascade | ||
# Get existing views that could be dependent on this one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Get existing views that could be dependent on this one. |
# Get existing views that could be dependent on this one. | ||
existing_views = views.reject{|v| v.name == name} | ||
|
||
# Get indexes of existing materialized views |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Get indexes of existing materialized views |
def update_view(name, sql_definition, cascade=false) | ||
if cascade | ||
# Get existing views that could be dependent on this one. | ||
existing_views = views.reject{|v| v.name == name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existing_views = views.reject{|v| v.name == name} | |
potential_dependent_views = views.reject{|v| v.name == name} |
|
||
# Get indexes of existing materialized views | ||
indexes = Indexes.new(connection: connection) | ||
view_indexes = existing_views.select(&:materialized).flat_map do |view| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
view_indexes = existing_views.select(&:materialized).flat_map do |view| | |
potential_dependent_view_indexes = potential_dependent_views.select(&:materialized).flat_map do |view| |
create_view(name, sql_definition) | ||
|
||
recreate_dropped_views(existing_views, views, view_indexes) if cascade |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
recreate_dropped_views(existing_views, views, view_indexes) if cascade | |
if cascade | |
recreate_dropped_views(potential_dependent_views, views, potential_dependent_view_indexes) | |
end |
end | ||
end | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
end | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -52,23 +52,4 @@ def change | |||
end | |||
end | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -0,0 +1,27 @@ | |||
module MigrationHelpers | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def connection | ||
ActiveRecord::Base.connection | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to see this merged in 👍 |
Are there any plans to merge this? |
Hey all, just wanted to follow up to see if there's any help needed to get this merged in? |
@jtemplet there are two problems with this PR that we identified, which makes it a bit more 'work-in-progress' than we originally thought:
So yes, it definitely needs some help, quite considerable help. |
We're having some issues with getting things in a stable, dependency order in a couple of different places in the codebase. I'm inclined to close this for now, but if we're able to confidently fix that issue then we could potentially revisit. See: dependencies |
I wonder if #398 would help when revisiting this problem. Context: We're doing a Ruby 2.7 -> 3.0 upgrade and our old fork isn't compatible, so we're thinking again about how to solve this problem. |
I would also be interested in this feature, are there any plans on picking this up again? |
@pixielabs <3 Scenic!
We use lots of materialized views with dependencies and it can be quite painful to update them in a migration, especially remembering to recreate indexes.
This PR:
cascade: true
todrop_view
andupdate_view
.update_view
recreate any views that are lost during the view being updated, including any indexes on those views.drop_view
if they usecascade: true
, because you can't tell what might've been lost. I've made the necessary change inCommandRecorder
.Things of note / bits I'm unsure about:
try_index_create
public because I'm using it to recreate indexes, but I'm not using it via theon()
method. This is a new use ofIndexReapplication
and I'm conscious of that; perhaps some bigger API changes could be done here, but I didn't want to mess around too much!If there's anything that needs changing, or any questions, let me know!
December 2020 update: Phew, the years just fly by don't they. There's a summary of outstanding known issues at the bottom of this thread.