module Postspec class Render attr_reader :postspec forward_to :postspec, :conn READONLY_PRIORITY = "90" # Is a string because it needs two digits not matter the value REGISTER_PRIORITY = "91" READONLY_BUD_TRIGGER_NAME = "p#{READONLY_PRIORITY}_postspec_readonly_bud_trg" READONLY_BT_TRIGGER_NAME = "p#{READONLY_PRIORITY}_postspec_readonly_bt_trg" REGISTER_TRIGGER_FORMAT = "p#{REGISTER_PRIORITY}_postspec_register_%s_b%s_trg" def initialize(postspec) constrain postspec, Postspec @postspec = postspec end def postspec_schema(state) raise NotYet end # Create triggers to register changes in the database. It uses the postgres # function register_EVENT to write the data to the postspec schema # # It is supposed to be called only once and the triggers can't be disabled # def register_triggers(state) constrain state, lambda { |state| [:create, :drop].include?(state) } postspec.tables.map { |table| %w(insert update delete).map { |event| name = REGISTER_TRIGGER_FORMAT % [event, event[0]] exist = postspec.meta.exist?("#{table.uid}.#{name}()") if state == :create && !exist ref = (event == "insert" ? "new" : "old") <<~EOS create trigger #{name} before #{event} on #{table.uid} for each row execute function postspec.register_#{event}() EOS elsif state == :drop && exist "drop trigger if exists #{name} on #{table.uid}" else nil end }.compact }.flatten end # Create/drop readonly triggers. The triggers can be disabled because the # tests may occasionally modify seed data # def readonly_triggers(state, uids = nil) case state when :create; create_readonly_triggers(uids) when :drop; drop_readonly_triggers else raise ArgumentError, state.inspect end end def drop_readonly_triggers postspec.tables.map { |uid| [READONLY_BUD_TRIGGER_NAME, READONLY_BT_TRIGGER_NAME].map { |trigger| trigger_uid = "#{uid}.#{trigger}()" postspec.meta.exist?(trigger_uid) ? "drop trigger #{trigger} on #{uid}" : nil }.compact }.flatten end # Create readonly seed triggers. Readonly triggers are used to raise an # error when seed data are updated, deleted, or truncated. They all call # the common postspec.readonly_failure() function that raises a Postgres # exception def create_readonly_triggers(uids) constrain uids, { String => [Integer, NilClass] } result = [] uids.map { |uid, id| bud_sql = <<~EOS1 create trigger #{READONLY_BUD_TRIGGER_NAME} before update or delete on #{uid} for each row when (OLD.id <= #{id}) execute function postspec.readonly_failure('#{uid}') EOS1 bt_sql = <<~EOS2 create trigger #{READONLY_BT_TRIGGER_NAME} before truncate on #{uid} execute function postspec.truncate_failure('#{uid}') EOS2 [ bud_sql.chomp, bt_sql.chomp, "insert into postspec.seeds (table_uid, record_id) values ('#{uid}', #{id})" ] }.flatten end def execution_unit(tables, sql) return [] if sql.empty? materialized_views = tables.select { |uid| uid !~ /^postspec\./ }.map { |uid| postspec.type.dot(uid).depending_materialized_views }.flatten.map(&:uid).uniq sql = tables.map { |uid| "alter table #{uid} disable trigger all" } + sql + tables.map { |uid| "alter table #{uid} enable trigger all" } + materialized_views.map { |uid| "refresh materialized view #{uid}" } end def truncate_tables(uids) ["truncate #{uids.join(', ')} cascade"] end def delete_tables(arg) constrain arg, Array, Hash uids = arg.is_a?(Array) ? arg.map { |uid| [uid, 0] }.to_h : arg sql = uids.map { |uid, id| "delete from #{uid}" + (id > 0 ? " where id > #{id}" : "") } + uids.select { |uid| uid =~ /^postspec\./ ? true : !postspec.type.dot(uid).sub_table? }.map { |uid, id| "alter table #{uid} alter column id restart" + (id > 0 ? " with #{id+1}" : "") } end def reset_postspec_tables() delete_tables %w(postspec.runs postspec.seeds postspec.inserts postspec.updates postspec.deletes) end # FIXME: doesn't seem to be any improvement performance-wise def delete_tables_new(arg) constrain arg, Array, Hash if arg.is_a?(Array) delete_all = arg delete_only = {} else delete_all = [] delete_only = {} arg.each { |uid, id| if id == 0 delete_all << uid else delete_only[uid] = id end } end table_alias_index = 0 if delete_all.empty? delete_all_sql = [] else delete_all_sql = [ "with " + delete_all.map { |uid| "t#{table_alias_index += 1} as (delete from #{uid} returning 1 as id)" }.join(", ") + " select " + (1...table_alias_index).map { |i| "t#{i}.id" }.join(", ") + " from " + (1...table_alias_index).map { |i| "t#{i}" }.join(", ") ] + delete_all.map { |uid| "alter table #{uid} alter column id restart" } end delete_only_sql = delete_only.map { |uid, id| "delete from #{uid}" + (id > 0 ? " > #{id}" : "") } + delete_only.map { |uid, id| "alter table #{uid} alter column id restart with #{id+1}" } sql = delete_all_sql + delete_only_sql # uids.map { |uid, id| "delete from #{uid}" + (id > 0 ? " > #{id}" : "") } + # uids.map { |uid, id| "alter table #{uid} alter column id restart" } end end end