diff --git a/lib/scenic/adapters/postgres/views.rb b/lib/scenic/adapters/postgres/views.rb index 02b93588..53736306 100644 --- a/lib/scenic/adapters/postgres/views.rb +++ b/lib/scenic/adapters/postgres/views.rb @@ -8,20 +8,86 @@ def initialize(connection) @connection = connection end - # All of the views that this connection has defined. + # All of the views that this connection has defined, sorted according to + # dependencies between the views to facilitate dumping and loading. # # This will include materialized views if those are supported by the # connection. # # @return [Array] def all - views_from_postgres.map(&method(:to_scenic_view)) + sort(views_from_postgres).map(&method(:to_scenic_view)) end private + def sort(existing_views) + 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 + + # 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 + attr_reader :connection + # 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 + private_constant :DEPENDENT_SQL + + class TSortableHash < Hash + include TSort + + alias_method :tsort_each_node, :each_key + def tsort_each_child(node, &) + fetch(node).each(&) + end + end + private_constant :TSortableHash + def views_from_postgres connection.execute(<<-SQL) SELECT diff --git a/lib/scenic/schema_dumper.rb b/lib/scenic/schema_dumper.rb index ebaf5232..340acfde 100644 --- a/lib/scenic/schema_dumper.rb +++ b/lib/scenic/schema_dumper.rb @@ -3,34 +3,6 @@ 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) @@ -50,58 +22,8 @@ def views(stream) private def dumpable_views_in_database - @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) - ["schema_migrations", ignore_tables].flatten.any? do |ignored| - case ignored - when String then remove_prefix_and_suffix(table_name) == ignored - when Regexp then remove_prefix_and_suffix(table_name) =~ ignored - else - raise StandardError, "ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values." - end - end + @dumpable_views_in_database ||= Scenic.database.views.reject do |view| + ignored?(view.name) end end end