require 'yaml' require 'digest' module Postspec class FrameStack attr_reader :postspec forward_to :postspec, :type def initialize(postspec) constrain postspec, Postspec @postspec = postspec @stack = [] end forward_to :@stack, :empty?, :size, :each, :map def push(frame) @stack.push frame; frame end def pop() @stack.pop end def top() @stack.last end def dump() puts self.class indent { @stack.reverse.map(&:dump) } end end # TODO: Add #stack and forward #postspec to it class Frame attr_reader :postspec attr_reader :parent attr_reader :search_path def schema() @search_path.first end attr_reader :push_sql attr_reader :pop_sql attr_reader :ids attr_reader :anchors def initialize(postspec, parent, search_path, push_sql, pop_sql, ids, fox_anchors) constrain postspec, Postspec constrain parent, NilClass, Frame constrain search_path, [String], NilClass constrain push_sql, String, [String], NilClass constrain pop_sql, String, [String], NilClass constrain ids, String => Integer constrain fox_anchors, FixtureFox::Anchors, NilClass @postspec = postspec @parent = parent @search_path = search_path || %w(public) @push_sql = ["set search_path to #{@search_path.join(", ")}"] + Array(push_sql || []) @pop_sql = Array(pop_sql || []) + (parent ? ["set search_path to #{parent.search_path.join(", ")}"] : []) @ids = ids || {} @fox_anchors = fox_anchors || FixtureFox::Anchors.new(@postspec.type) anchors = @fox_anchors.values.map { |anchor| [anchor.name, anchor.id] }.to_h @anchors = (parent ? parent.anchors.merge(anchors) : anchors) end # Returns true if this frame is associated with a transaction def transaction?() true end def dump() puts self.class indent { puts "search_path: #{search_path}" puts "push_sql:" indent { puts push_sql } puts "pop:sql:" indent { puts pop_sql } } end protected attr_reader :fox_anchors end class NopFrame < Frame def initialize(parent, search_path) super(parent.postspec, parent, search_path, nil, nil, parent.ids, parent.fox_anchors) end end class FoxFrame < Frame attr_reader :fox attr_reader :data alias_method :sql, :push_sql # Note FoxFrame::new computes the fox object def initialize(parent, search_path, fox, data, push_sql) @fox = fox @data = data super(parent.postspec, parent, search_path, push_sql, nil, fox.ids, fox.anchors) end def self.new(parent, search_path, type, files) fox_signature = self.fox_signature(files) fox = @fox_pool[fox_signature] ||= FixtureFox::Fox.new(type, files, schema: search_path.last) # The IDs for the data signature is the minimal set of external IDs used by the Fox object ids = fox.tables.map { |table| (id = parent.ids[table.uid]) ? [table.uid, id] : nil }.compact.to_h data_signature = self.data_signature(fox_signature, ids, fox.referenced_anchors) data = @data_pool[data_signature] ||= fox.data(ids: parent.ids, anchors: parent.send(:fox_anchors)) push_sql = @sql_pool[data_signature] ||= data.to_sql(format: :exec, ids: parent.ids, delete: :none) object = FoxFrame.allocate object.send(:initialize, parent, search_path, fox, data, push_sql) object end private @fox_pool = {} @data_pool = {} @sql_pool = {} def self.fox_signature(files) sha256 = Digest::SHA256.new files.sort.each { |source| sha256 << source } sha256.base64digest end def self.data_signature(fox_signature, ids, anchors) sha256 = Digest::SHA256.new ids.sort_by(&:first).each { |key, value| sha256 << key << ":" << value.to_s << ";" } anchors.sort_by(&:uid).each { |anchor| sha256 << anchor.uid << "->" << anchor.id.to_s << ";" } fox_signature + ":" + sha256.base64digest end end class SeedFrame < Frame def transaction?() false end def initialize(postspec, ids, anchors = nil) constrain ids, String => Integer constrain anchors, FixtureFox::Anchors, NilClass super(postspec, nil, nil, [], [], ids, anchors) end end class EmptyFrame < Frame def transaction?() false end def initialize(postspec) push_sql = pop_sql = postspec.render.delete_tables(postspec.tables.map(&:uid)) super(postspec, nil, nil, push_sql, pop_sql, {}, nil) end end end