-
-
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 topological sorting for dumped views via TSort #398
Conversation
Hello! We (me and @thorncp) have been trying this code, but it hasn't worked for us. Here's exactly what we tried:
At this point, our schema can't be loaded into the database, because it will try to create rails db:schema:load
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "bs" does not exist
LINE 2: FROM bs;
^
/path/to/db/schema.rb:4416:in `block in <main>'
/path/to/db/schema.rb:13:in `<main>'
Caused by:
PG::UndefinedTable: ERROR: relation "bs" does not exist
LINE 2: FROM bs;
^ We also tried this with our existing views. Before we dumped the schema they were in a valid, topological order. After we dumped, they were in an invalid non-topological order. It this expected? As it stands, our app would break if this got merged. |
This is not expected. The idea of this PR is to provide both correct ordering (which we have today) and stable ordering (which we do not). Your manual steps are a good test case for us to consider including in our suite today (that |
Thanks for the confirmation. FWIW, I just wrote a test against this commit and it passes: it "sorts dependency order over alphabetical order" do
conn.create_view "as", sql_definition: <<-SQL
SELECT 1;
SQL
conn.create_view "bs", sql_definition: <<-SQL
SELECT 2;
SQL
conn.update_view "as", sql_definition: <<-SQL
SELECT * FROM bs;
SQL
expect(views).to eq(%w[bs as])
end I also tried Maybe it's our setup somehow? Maybe there's some untested behavior if you migrate in between creating and updating dependent views? |
Update: I tried this on a brand new app and it did put |
We (@heatherbooker and I) figured it out today. Our database views are not in the production:
database: app_production
schema_search_path: "abc" The SQL that generates the list of dependent views does not include the schema name, but the code to generate view names does include the non- This breaks the logic that prepares the views to be TSorted. We updated the select portion of the SQL and now our example case passes and puts - SELECT distinct dependent_ns.nspname AS dependent_schema
- , dependent_view.relname AS dependent_view
- , source_ns.nspname AS source_schema
- , source_table.relname AS source_table
+ SELECT DISTINCT
+ dependent_ns.nspname AS dependent_schema,
+ CASE
+ WHEN dependent_ns.nspname = 'public' THEN dependent_view.relname
+ ELSE (dependent_ns.nspname || '.' || dependent_view.relname)
+ END AS dependent_view,
+ source_ns.nspname AS source_schema,
+ CASE
+ WHEN source_ns.nspname = 'public' THEN source_table.relname
+ ELSE (source_ns.nspname || '.' || source_table.relname)
+ END AS source_table
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
JOIN pg_class as dependent_view ON pg_rewrite.ev_class = dependent_view.oid
JOIN pg_class as source_table ON pg_depend.refobjid = source_table.oid
JOIN pg_namespace dependent_ns ON dependent_ns.oid = dependent_view.relnamespace
JOIN pg_namespace source_ns ON source_ns.oid = source_table.relnamespace
WHERE dependent_ns.nspname = ANY (current_schemas(false)) AND source_ns.nspname = ANY (current_schemas(false))
AND source_table.relname != dependent_view.relname
AND source_table.relkind IN ('m', 'v') AND dependent_view.relkind IN ('m', 'v')
- ORDER BY dependent_view.relname;
+ ORDER BY dependent_view; This should replicate the current scenic logic. I also created a test, but it's failing when the new test just before it (the test added in this PR on line 152) runs before it. Not sure why: it "sorts dependency order when views exist in a schema" do
conn.execute("CREATE SCHEMA IF NOT EXISTS scenic; SET search_path TO scenic")
conn.execute("CREATE VIEW apples AS SELECT 1;")
conn.execute("CREATE VIEW bananas AS SELECT 2;")
conn.execute("CREATE OR REPLACE VIEW apples AS SELECT * FROM bananas;")
expect(views).to eq(%w[scenic.bananas scenic.apples])
conn.execute("DROP SCHEMA IF EXISTS scenic CASCADE;")
end |
At my jobby job we've been using this since 10/26 and have made several updates to views (though none that change the dependency structure) and have not seen any unnecessary reordering of views, so I'm inclined after ~2.5 months to call this a win. @edwardloveall I wonder if #401 helps with your use case at all? Also if you want to write up a PR to add specs that exercise view ordering I'd appreciate it. |
It's good enough for me. Merge when ready. When we had talked about this problem originally (years ago), we said we would make this a "breaking change" for 2.0 since it would likely change everyone's generated structure.sql once. Do you still feel that's appropriate? |
Unclear - I think we'd re-order the schema.rb one more time whenever it's next generated (with a view change?) which has already been happening, then it should stop. That may not be a breaking change. |
Upcoming changes like [topological sorting][1] have the potential to write views to `db/schema.rb` in an order that does not respect dependencies. This adds a test to ensure that views in all schemas are both included, and in the correct dependency order. [1]: scenic-views#398
It does, but only in combination with the changes here.
Done: #403 |
Upcoming changes like [topological sorting][1] have the potential to write views to `db/schema.rb` in an order that does not respect dependencies. This adds a test to ensure that views in all schemas are both included, and in the correct dependency order. [1]: #398
1431c49
to
37adcff
Compare
d689a56
to
3f56996
Compare
I'm seeing a failure on one of the specs here after @edwardloveall's PR adding sort order specs was merged. I'm fairly sure that the spec is correct and I'm not sure why the failure is happening. |
Looking at the branch, it's similar to my previous comment: the SQL that generates the dependent views does not contain the schema, but Scenic's views do. Here's one possible fix: def tsorted_views(views_names)
views_hash = TSortableHash.new
::Scenic.database.execute(DEPENDENT_SQL).each do |relation|
- source_v = relation["source_table"]
- dependent = relation["dependent_view"]
+ source_v = [relation["source_schema"], relation["source_table"]].join(".")
+ dependent = [relation["dependent_schema"], relation["dependent_view"]].join(".")
views_hash[dependent] ||= []
views_hash[source_v] ||= []
views_hash[dependent] << source_v
views_names.delete(source_v)
views_names.delete(dependent)
end
# after dependencies, there might be some views left
# that don't have any dependencies
views_names.sort.each { |v| views_hash[v] = [] }
binding.irb
views_hash.tsort
end |
Guess my comment on original PR was lost, mentioned in here #337 (review), we have been running with those changes since then without issues in apps that use cross schema views. |
- Fix some dependency conflicts / unneeded version locks - Fix some Rails 7.1 issues
6678e40
to
05aa517
Compare
Upcoming changes like [topological sorting][1] have the potential to write views to `db/schema.rb` in an order that does not respect dependencies. This adds a test to ensure that views in all schemas are both included, and in the correct dependency order. [1]: #398
Superseded by #416 |
Upcoming changes like [topological sorting][1] have the potential to write views to `db/schema.rb` in an order that does not respect dependencies. This adds a test to ensure that views in all schemas are both included, and in the correct dependency order. [1]: #398
Rebase of #337