# 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