From 4ea9399e02964996c40482c7e3ccaf8cced99cd0 Mon Sep 17 00:00:00 2001 From: Caleb Hearth Date: Thu, 2 May 2024 09:04:05 -0600 Subject: [PATCH] Add topological sorting for dumped views using TSort Co-authored-by: Mykhaylo Sorochan Co-authored-by: Edward Loveall --- lib/scenic/schema_dumper.rb | 68 +++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/scenic/schema_dumper.rb b/lib/scenic/schema_dumper.rb index 68aab1b2..ebaf5232 100644 --- a/lib/scenic/schema_dumper.rb +++ b/lib/scenic/schema_dumper.rb @@ -3,6 +3,34 @@ module Scenic # @api private module SchemaDumper + # A hash to do topological sort + class TSortableHash < Hash + include TSort + + alias_method :tsort_each_node, :each_key + def tsort_each_child(node, &) + fetch(node).each(&) + end + end + + # Query for the dependencies between views + DEPENDENT_SQL = <<~SQL.freeze + 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 + 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; + SQL + def tables(stream) super views(stream) @@ -22,11 +50,47 @@ def views(stream) private def dumpable_views_in_database - @dumpable_views_in_database ||= Scenic.database.views.reject do |view| - ignored?(view.name) + @ordered_dumpable_views_in_database ||= begin + existing_views = Scenic.database.views.reject do |view| + ignored?(view.name) + end + + tsorted_views(existing_views.map(&:name)).map do |view_name| + existing_views.find do |ev| + ev.name == view_name || ev.name == view_name.split(".").last + end + end.compact end end + # When dumping the views, their order must be topologically + # sorted to take into account dependencies + def tsorted_views(views_names) + views_hash = TSortableHash.new + + ::Scenic.database.execute(DEPENDENT_SQL).each do |relation| + source_v = [ + relation["source_schema"], + relation["source_table"] + ].compact.join(".") + dependent = [ + relation["dependent_schema"], + relation["dependent_view"] + ].compact.join(".") + views_hash[dependent] ||= [] + views_hash[source_v] ||= [] + views_hash[dependent] << source_v + views_names.delete(relation["source_table"]) + views_names.delete(relation["dependent_view"]) + end + + # after dependencies, there might be some views left + # that don't have any dependencies + views_names.sort.each { |v| views_hash[v] ||= [] } + + views_hash.tsort + end + unless ActiveRecord::SchemaDumper.private_instance_methods(false).include?(:ignored?) # This method will be present in Rails 4.2.0 and can be removed then. def ignored?(table_name)