Skip to content

Commit

Permalink
Move SchemaDumper sorting behavior to adapter
Browse files Browse the repository at this point in the history
This is Postgres-specific code, and it shouldn't have been here.

#416 (comment)
  • Loading branch information
calebhearth committed Nov 20, 2024
1 parent d504b40 commit c003cfa
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 82 deletions.
70 changes: 68 additions & 2 deletions lib/scenic/adapters/postgres/views.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Scenic::View>]
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
Expand Down
82 changes: 2 additions & 80 deletions lib/scenic/schema_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit c003cfa

Please sign in to comment.