module Postspec class Environment attr_reader :postspec forward_to :postspec, :conn, :render def uids() table_sequence_ids end # FIXME: What is this? And why not using @uids? def initialize(postspec) @postspec = postspec end def table_sequence_ids(all: false) ignore_list = "'" + (postspec.ignore + %w(prick)).join("', '") + "'" conn.map \ "select table_uid, record_id from postspec.table_sequence_ids(array[#{ignore_list}]::varchar[])" + (all ? "" : " where record_id is not null") end def table_seed_ids conn.map "select table_uid, record_id from postspec.seeds" end # Returns an array from table name to maximum ID used in the table or in # the root table if this is a subtable def table_max_ids result = table_sequence_ids(all: true) postspec.type.tables.select(&:sub_table?).each { |table| result[table.uid] = result[table.root_table.uid] or raise "Oops: #{table.uid}" } result end # Returns map from table UID to record ID of the last record to be kept so # that 'delete from table_uid where id is null or id > record_id' does the right thing # # # Note: This assumes that the change triggers has been installed beforehand def dirty_tables seed_ids = table_seed_ids table_max_ids.map { |table_uid, record_id| (seed_id = seed_ids[table_uid]) ? [table_uid, seed_id] : [table_uid, record_id] }.compact.to_h end def create() # puts "Environment#create" conn.execute IO.read(Postspec::SHARE_DIR + "/postspec_schema.sql") conn.execute render.change_triggers(:create) end def drop() # puts "Environment#drop" conn.execute "drop schema if exists postspec cascade" end def exist?() conn.exist? "select 1 from pg_namespace where nspname = 'postspec'" end # Removes excess data and meta data, and resets sequences. It doesn't change any triggers def clean # puts "Environment#clean" @uids = {} user_tables = dirty_tables postspec_tables = CHANGE_TABLE_UIDS sql = render.delete_tables(user_tables) + render.delete_tables(postspec_tables) conn.execute render.execution_unit(user_tables.keys, sql) end def setup(mode) # puts "Environment#setup" sql = case mode when :seed @uids = table_max_ids render.seed_triggers(:create, @uids) when :empty @uids = {} [] else raise ArgumentError end conn.execute sql end def teardown(mode) # puts "Environment#teardown" @uids = nil tables, reset_sql = reset_data sql = case mode when :seed; render.seed_triggers(:drop) + render.delete_tables(SEED_TABLE_UIDS) when :empty; [] else raise ArgumentError end + reset_sql conn.execute render.execution_unit(tables, sql) end def reset() # puts "Environment#reset" conn.execute render.execution_unit(*reset_data) end private # Generate SQL to delete data using the postspec.inserts table def reset_data uids = conn.map "select table_uid, min(record_id) from postspec.inserts group by table_uid" sql = render.delete_tables(uids) + render.delete_tables(CHANGE_TABLE_UIDS) [uids.keys, sql] end CHANGE_TABLE_UIDS = %w(postspec.inserts postspec.updates postspec.deletes) SEED_TABLE_UIDS = %w(postspec.seeds) RUN_TABLE_UIDS = %w(postspec.runs) TABLE_UIDS = CHANGE_TABLE_UIDS + SEED_TABLE_UIDS + RUN_TABLE_UIDS end end __END__ class Environment attr_reader :postspec forward_to :postspec, :conn, :render attr_reader :uids # UID to ID map. Only used by the seed environment def user_tables() @tables ||= postspec.tables.map(&:uid) end CHANGE_TABLE_UIDS = %w(postspec.inserts postspec.updates postspec.deletes) SEED_TABLE_UIDS = %w(postspec.seeds) RUN_TABLE_UIDS = %w(postspec.runs) TABLE_UIDS = CHANGE_TABLE_UIDS + SEED_TABLE_UIDS + RUN_TABLE_UIDS def initialize(postspec) @postspec = postspec @uids = nil setup_schema if !exist_schema? end # setup(mode); teardown(mode) is a NOP def setup(mode) self.send :"setup_#{mode}_environment" end def teardown(mode) self.send :"teardown_#{mode}_environment" end def reset(mode, *type) self.send :"reset_#{mode}_environment", *type.compact end def setup_schema conn.execute(IO.read(SHARE_DIR + "/postspec_schema.sql")) end def teardown_schema conn.execute "drop schema if exists postspec cascade" end def reset_schema teardown_seed_environment teardown_empty_environment end def exist_schema? conn.exist? "select 1 from pg_namespace where nspname = 'postspec'" end def setup_change_environment conn.exec render.change_triggers(:create) end def teardown_change_environment sql = render.change_triggers(:drop) + render.delete_tables(CHANGE_TABLE_UIDS) conn.exec render.execution_unit(CHANGE_TABLE_UIDS, sql) end # Not used - should be delegated to seed or empty environment # def reset_change_environment # end def setup_seed_environment() @uids = conn.map("select name, id from postspec.sequence_ids()") sql = render.seed_triggers(:create, uids) conn.exec sql end def teardown_seed_environment() @uids = nil uids = conn.map "select table_uid, min(record_id) from postspec.inserts" sql = render.seed_triggers(:drop) + render.delete_tables(uids) + render.delete_tables(CHANGE_TABLE_UIDS) + render.delete_tables(SEED_TABLE_UIDS) conn.exec render.execution_unit(uids.keys, sql) end def reset_seed_environment uids = conn.map "select table_uid, min(record_id) from postspec.inserts" sql = render.delete_tables(uids) + render.delete_tables(CHANGE_TABLE_UIDS) conn.exec render.execution_unit(uids.keys, sql) end def setup_empty_environment @uids = {} sql = render.delete_tables(user_tables + CHANGE_TABLE_UIDS) conn.exec render.execution_unit(tables, sql) end def teardown_empty_environment @uids = nil end def reset_empty_environment # Because we know the initial environment was empty we only need to # inspect the postspect.inserts tables to find records to delete changed_tables = conn.values "select distinct table_uid from inserts" sql = render.delete_tables(changed_tables + CHANGE_TABLE_UIDS) conn.exec render.execution_unit(tables, sql) end end end __END__ end def drop class PostspecEnvironment attr_reader :postspec forward_to :postspec, :conn, :render def initialize(postspec) @postspec = postspec self.ensure end def self.instance(postspec) @instance ||= PostspecEnvironment.new(postspec) end def create end def drop end def reset State.reset(postspec) end def ensure if !postspec.meta.schemas.key?("postspec") create else # meta knows about the postspec schema so we hide it postspec.meta.schemas["postspec"].hidden = true reset end end private @instance = nil end class Environment attr_reader :postspec_environment attr_reader :state forward_to :postspec_environment, :postspec, :conn, :render # :call-seq: # ::new(postspec, state) # ::new(postspec, mode) def self.new(postspec, arg) klass = case mode when :seed; SeedEnvironment when :empty; EmptyEnvironment else KeepEnvironment end object = klass.allocate state = arg.is_state? ? arg : State.create(postspec, arg) object.send(:initialize, PostspecEnvironment.instance(postspec), state) object end def terminate state.write end def create() raise NotThis end def drop() raise NotThis end def reset() postspec_environment.reset end protected def initialize(postspec_environment, state) @postspec_environment = postspec_environment @state = state end end class SeedEnvironment < Environment def create uids = @conn.map("select name, id from postspec.sequence_ids()", :name) conn.exec render.seed_triggers(:create, uids) conn.exec "delete from postspec.seeds" end def drop conn.exec render.seed_triggers(:drop) conn.exec "delete from postspec.seeds" end def reset # TODO: Merge with PgGraph::Data::SqlRender end end class EmptyEnvironment < Environment def create() end def drop() end def reset super conn.exec state.inserts.keys.map { |uid| "delete from #{uid}" } end end class KeepEnvironment < Environment end end __END__ class Environment attr_reader :postspec attr_reader :id attr_reader :mode attr_accessor :status def duration() @duraction ||= Time.now - created_at end attr_reader :created_at forward_to :postspec, :conn def initialize(postspec, id, mode, status, duration, created_at) @postspec = postspec @id = id @mode = mode @status = status @duration = duration @created_at = created_at end def update sql = %( update postspec.runs set status = #{status}, duration = #{duration} where id = #{id} ) conn.exec(sql) end def self.create(conn, mode) sql = %( insert into postspec.runs (mode) values ('#{mode}') returning id, created_at ) id, created_at = @conn.tuple(sql) State.new(conn, id, mode, nil, nil, created_at) end def self.read_last(conn) sql = %( select id, mode, status, duration, created_at from postspec.runs order by desc id limit 1 ) tuple = @conn.tuples(sql).first tuple && State.new(conn, *tuple) end end class SeedEnvironment < Environment def initialize(postspec, id, mode, status, duration, created_at) end def create super conn.execute(render.readonly_triggers(:create, ids) end def update end def drop conn.execute(render.readonly_triggers(:drop, ids) end end class EmptyEnvironment < Environment def create super conn.execute(render.delete_tables(postspec.tables.map(&:uid)) end def update end def drop end end class KeepEnvironment < Environment end class Environment attr_reader :postspec attr_reader :state forward_to :postspec, :conn, :meta, :render def initialize(postspec, mode) @postspec = postspec @state = State.create(postspec.conn, mode) last_state end def create() if meta.schemas.key?("postspec") # meta knows about the postspec schema so we hide it meta.schemas["postspec"].hidden = true # @conn.execute render.delete_postspec_tables else # meta doesn't know about the postspec schema conn.execute(IO.read(SHARE_DIR + "/postspec_schema.sql")) end conn.execute(render.change_triggers(:create)) end def reset() conn.execute(render.reset_postspec_tables) end def drop() conn.execute(render.change_triggers(:drop)) conn.execute("drop schema postspec cascade") end end class SeedEnvironment < Environment def create super conn.execute(render.readonly_triggers(:create, ids) end def update end def drop conn.execute(render.readonly_triggers(:drop, ids) end end class EmptyEnvironment < Environment def create super conn.execute(render.delete_tables(postspec.tables.map(&:uid)) end def update end def drop end end class KeepEnvironment < Environment end end