Skip to content
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

Move SchemaDumper sorting behavior to adapter #431

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading