# frozen-string-literal: true
#
# This adds a Sequel::Dataset#to_dot method. The +to_dot+ method
# returns a string that can be processed by graphviz's +dot+ program in
# order to get a visualization of the dataset. Basically, it shows a version
# of the dataset's abstract syntax tree.
#
# To load the extension:
#
# Sequel.extension :to_dot
#
# Related module: Sequel::ToDot
#
module Sequel
class ToDot
module DatasetMethods
# Return a string that can be processed by the +dot+ program (included
# with graphviz) in order to see a visualization of the dataset's
# abstract syntax tree.
def to_dot
ToDot.output(self)
end
end
# The option keys that should be included in the dot output.
TO_DOT_OPTIONS = [:with, :distinct, :select, :from, :join, :where, :group, :having, :compounds, :order, :limit, :offset, :lock].freeze
# Given a +Dataset+, return a string in +dot+ format that will
# generate a visualization of the dataset.
def self.output(ds)
new(ds).output
end
# Given a +Dataset+, parse the internal structure to generate
# a dataset visualization.
def initialize(ds)
@i = 0
@stack = [@i]
@dot = ["digraph G {", "0 [label=\"self\"];"]
v(ds, "")
@dot << "}"
end
# Output the dataset visualization as a string in +dot+ format.
def output
@dot.join("\n")
end
private
# Add an entry to the +dot+ output with the given label. If +j+
# is given, it is used directly as the node or transition. Otherwise
# a node is created for the current object.
def dot(label, j=nil)
label = case label
when nil
""
else
label.to_s
end
@dot << "#{j||@i} [label=#{label.inspect}];"
end
# Recursive method that parses all of Sequel's internal datastructures,
# adding the appropriate nodes and transitions to the internal +dot+
# structure.
def v(e, l)
@i += 1
dot(l, "#{@stack.last} -> #{@i}")
@stack.push(@i)
case e
when LiteralString
dot "Sequel.lit(#{e.to_s.inspect})"
when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
dot e.inspect
when Array
dot "Array"
e.each_with_index do |val, j|
v(val, j)
end
when Hash
dot "Hash"
e.each do |k, val|
v(val, k)
end
when SQL::ComplexExpression
dot "ComplexExpression: #{e.op}"
e.args.each_with_index do |val, j|
v(val, j)
end
when SQL::Identifier
dot "Identifier"
v(e.value, :value)
when SQL::QualifiedIdentifier
dot "QualifiedIdentifier"
v(e.table, :table)
v(e.column, :column)
when SQL::OrderedExpression
dot "OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}"
v(e.expression, :expression)
when SQL::AliasedExpression
dot "AliasedExpression"
v(e.expression, :expression)
v(e.alias, :alias)
v(e.columns, :columns) if e.columns
when SQL::CaseExpression
dot "CaseExpression"
v(e.expression, :expression) if e.expression
v(e.conditions, :conditions)
v(e.default, :default)
when SQL::Cast
dot "Cast"
v(e.expr, :expr)
v(e.type, :type)
when SQL::Function
dot "Function: #{e.name}"
e.args.each_with_index do |val, j|
v(val, j)
end
v(e.args, :args)
v(e.opts, :opts)
when SQL::Subscript
dot "Subscript"
v(e.f, :f)
v(e.sub, :sub)
when SQL::Window
dot "Window"
v(e.opts, :opts)
when SQL::PlaceholderLiteralString
str = e.str
str = "(#{str})" if e.parens
dot "PlaceholderLiteralString: #{str.inspect}"
v(e.args, :args)
when SQL::JoinClause
str = "#{e.join_type.to_s.upcase} JOIN".dup
if e.is_a?(SQL::JoinOnClause)
str << " ON"
elsif e.is_a?(SQL::JoinUsingClause)
str << " USING"
end
dot str
v(e.table_expr, :table)
if e.is_a?(SQL::JoinOnClause)
v(e.on, :on)
elsif e.is_a?(SQL::JoinUsingClause)
v(e.using, :using)
end
when Dataset
dot "Dataset"
TO_DOT_OPTIONS.each do |k|
if val = e.opts[k]
v(val, k)
end
end
else
dot "Unhandled: #{e.inspect}"
end
@stack.pop
end
end
Dataset.register_extension(:to_dot, ToDot::DatasetMethods)
end