def tag(type, body, args = {})
if args[:style].is_a? Hash
args[:style] = args[:style].map { |k, v| "#{k}: #{v};" }.join(" ")
end
"<#{type} #{args.map { |k, v| "#{k}='#{v}'" }.join(" ")}>#{body}#{type}>"
end
def data_table(schema, data, params = {})
tablename_style = params.fetch(:tablename_style, { "font-weight" => "bold", "text-decoration" => "underline" })
rownum_style = params.fetch(:rownum_style, { "font-size" => "50%", "font-face" => "Courier, fixed-width", "vertical-align"=> "middle" })
annotation_style = params.fetch(:annotation_style, rownum_style.merge("text-align" => :left, "font-size" => "70%"))
separator_style = params.fetch(:separator_style, { "border-right" => "1px solid black" })
num_cols = data.map { |row| row.size if row.is_a? Array }.compact.max
data = data.map do |row|
case row
when Array
row.map do |cell|
tag("td", cell)
end
when String
[ tag("td", row, colspan: num_cols) ]
else
raise "Unknown rowtype: #{row}"
end
end
if params.has_key? :annotations and not params[:annotations].nil?
data = data.zip(params[:annotations]).map do |row, annot|
if annot.nil? then row
else row+[tag("td", "→ "+annot.to_s, style: annotation_style)] end
end
num_cols += 1
end
data = [schema.map { |cell| tag("th", cell) }] + data
if params.fetch(:rowids, false)
data = data.map.with_index do |row, idx|
if idx == 0 then [tag("th", "#", style: separator_style)]+row
else [tag("td", idx+params.fetch(:rowid_offset, 0), style: rownum_style.merge(separator_style))]+row
end
end
elsif params.has_key? :name
data = data.map { |row| [tag("td", "", style: separator_style)]+row }
end
if params.has_key? :name
data[0][0] = tag("th", params[:name], style: tablename_style.merge(separator_style))
end
row_args = [{}] * data.size
case params.fetch(:build_in, :none)
when :none then
when :by_row then row_args = [{}] + [{ "class" => "fragment" }]*(data.size-1)
when Array then row_args = [{}] + params[:build_in].map do |order|
if order <= 0 then {}
else { "class" => "fragment", "data-fragment-index" => order }
end end
end
return tag("table",
data.zip(row_args).map { |row, args| tag("tr", row.join, args) }.join("\n"),
params.fetch(:table_args, {})
)
end
class RATreeNode
def initialize(type, params, children = [])
@type = type
@params = params
@children = children
@self_width = 100
@self_height = 100
case @type
when :table then
@self_width = 40*params[:name].length
@self_height = 50
when :select, :join then
@self_width += 15*params[:pred].length
when :project then
@self_width += 15*params[:attrs].length
end
@height_above_children = 100
end
def subscript(x)
"#{x}"
end
def symbol
case @type
when :select then " 𝛔#{subscript @params[:pred]}"
when :project then " 𝛑#{subscript @params[:attrs]}"
when :aggregate then "#{subscript @params[:groupby] if @params.has_key? :groupby}𝛄#{subscript @params[:aggregates]}"
when :join then "⋈#{subscript @params[:pred]}"
when :cross then "⨉"
when :diff then " -"
when :union then "⊎"
when :table then "#{@params[:name]}"
else type.to_s
end
end
def height(config = {})
unless @height
if @children.nil?
@height = @self_height
else
@height = @children.map { |c| c.height(config) }.max + (@self_height + @height_above_children)
end
end
@height
end
def child_width(config = {})
return 0 if @children.nil?
unless @child_width
separator_x = 20
@child_width = @children.map { |c| c.width(config) }.sum + separator_x * (@children.size-1)
end
@child_width
end
def width(config = {})
unless @width
if @children.nil?
@width = @self_width
else
@width = [
child_width,
@self_width
].max
end
end
@width
end
def symbol_text(config)
symbol_x = width(config) / 2 - (@self_width / 2)
symbol_y = 0
debug = "#{config.fetch(:indent, "")}\n" if config.fetch(:debug, false)
"#{debug}#{config.fetch(:indent, "")}#{symbol}\n"
end
def render(config = {})
return symbol_text(config) if @children.nil?
indent = config.fetch(:indent, "")
separator_x = 20
separator_y = @height_above_children
children_x = [0]
children_x = [(width(config) - child_width(config)) / 2] if width(config) > child_width(config)
(1..@children.length).each { |i| children_x[i] = children_x[i-1] + @children[i-1].width + separator_x }
children_y = separator_y + @self_height
child_blobs = @children.map.with_index do |c, i|
rendered = c.render(config.merge( indent: indent+" " ))
p rendered if config.fetch(:debug, false)
p children_x[i] if config.fetch(:debug, false)
"#{indent} \n#{rendered}\n"
end
line_x = width(config) / 2
line_y = (@self_height) * 1.1
target_y = line_y + @height_above_children
child_lines = @children.map.with_index do |c, i|
target_x = (children_x[i] + children_x[i+1] - separator_x) / 2
"#{indent} \n"
end
symbol_text(config)+child_blobs.join+child_lines.join
end
end
def ra_table(name)
RATreeNode.new(:table, { name: name }, nil)
end
def ra_union(*children)
RATreeNode.new(:union, {}, children)
end
def ra_diff(*children)
RATreeNode.new(:diff, {}, children)
end
def ra_join(predicate, lhs, rhs)
RATreeNode.new(:table, { pred: predicate }, [lhs, rhs])
end
def ra_aggregate(groupby, aggregates, input)
RATreeNode.new(:aggregate, { groupby: groupby, aggregates: aggregates}, [input])
end
def ra_select(predicate, input)
RATreeNode.new(:select, { pred: predicate }, [input])
end
def ra_project(attrs, input)
attrs = attrs.map { |k, v| "#{k} ← #{v}"}.join("; ") if attrs.is_a? Hash
RATreeNode.new(:project, { attrs: attrs }, [input])
end
def relational_algebra(params = {})
indent = params.fetch(:indent, "")
ra = yield
scale = if ra.height > 500 then 500.0 / ra.height else 1 end
return (
"#{indent}\n"
)
end