diff --git a/lib/sequel/extensions/pg_triggers.rb b/lib/sequel/extensions/pg_triggers.rb index 736dca7..5a9ecbf 100644 --- a/lib/sequel/extensions/pg_triggers.rb +++ b/lib/sequel/extensions/pg_triggers.rb @@ -16,6 +16,7 @@ def pgt_counter_cache(main_table, main_table_id_column, counter_column, counted_ pgt_trigger(counted_table, trigger_name, function_name, [:insert, :update, :delete], <<-SQL, :after=>true) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (TG_OP = 'UPDATE' AND (NEW.#{id_column} = OLD.#{id_column} OR (OLD.#{id_column} IS NULL AND NEW.#{id_column} IS NULL))) THEN RETURN NEW; ELSE @@ -41,6 +42,7 @@ def pgt_created_at(table, column, opts={}) col = quote_identifier(column) pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (TG_OP = 'UPDATE') THEN NEW.#{col} := OLD.#{col}; ELSIF (TG_OP = 'INSERT') THEN @@ -60,6 +62,7 @@ def pgt_force_defaults(table, defaults, opts={}) end pgt_trigger(table, trigger_name, function_name, [:insert], <<-SQL) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} #{lines.join("\n")} RETURN NEW; END; @@ -95,6 +98,7 @@ def pgt_json_audit_log_setup(table, opts={}) end create_function(function_name, (<<-SQL), {:language=>:plpgsql, :returns=>:trigger, :replace=>true}.merge(opts[:function_opts]||{})) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} INSERT INTO #{quote_schema_table(table)} (txid, at, "user", "schema", "table", action, prior) VALUES (txid_current(), CURRENT_TIMESTAMP, CURRENT_USER, TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_OP, to_jsonb(OLD)); IF (TG_OP = 'DELETE') THEN @@ -124,6 +128,7 @@ def pgt_sum_cache(main_table, main_table_id_column, sum_column, summed_table, su pgt_trigger(summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL, :after=>true) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (TG_OP = 'UPDATE' AND NEW.#{id_column} = OLD.#{id_column}) THEN UPDATE #{table} SET #{sum_column} = #{sum_column} + #{new_table_summed_column} - #{old_table_summed_column} WHERE #{main_column} = NEW.#{id_column}; ELSE @@ -178,6 +183,7 @@ def pgt_sum_through_many_cache(opts={}) pgt_trigger(orig_summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL, :after=>true) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (TG_OP = 'UPDATE' AND NEW.#{summed_table_id_column} = OLD.#{summed_table_id_column}) THEN UPDATE #{main_table} SET #{sum_column} = #{sum_column} + #{new_table_summed_column} - #{old_table_summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = NEW.#{summed_table_id_column}); ELSE @@ -197,6 +203,7 @@ def pgt_sum_through_many_cache(opts={}) pgt_trigger(orig_join_table, join_trigger_name, join_function_name, [:insert, :delete, :update], <<-SQL, :after=>true) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (NOT (TG_OP = 'UPDATE' AND NEW.#{main_table_fk_column} = OLD.#{main_table_fk_column} AND NEW.#{summed_table_fk_column} = OLD.#{summed_table_fk_column})) THEN IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN UPDATE #{main_table} SET #{sum_column} = #{sum_column} + (SELECT #{general_summed_column} FROM #{summed_table} WHERE #{summed_table_id_column} = NEW.#{summed_table_fk_column}) WHERE #{main_table_id_column} = NEW.#{main_table_fk_column}; @@ -225,6 +232,7 @@ def pgt_touch(main_table, touch_table, column, expr, opts={}) sql = <<-SQL BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} IF (TG_OP = 'UPDATE' AND (#{same_id})) THEN #{update['NEW']} ELSE @@ -250,6 +258,7 @@ def pgt_updated_at(table, column, opts={}) function_name = opts[:function_name] || "pgt_ua_#{pgt_mangled_table_name(table)}__#{column}" pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL) BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} NEW.#{quote_identifier(column)} := CURRENT_TIMESTAMP; RETURN NEW; END; @@ -273,6 +282,7 @@ def pgt_foreign_key_array(opts={}) temp_count1 int; temp_count2 int; BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts[:prevent_depth])} arr := NEW.#{col}; temp_count1 := array_ndims(arr); IF arr IS NULL OR temp_count1 IS NULL THEN @@ -329,6 +339,16 @@ def pgt_trigger(table, trigger_name, function_name, events, definition, opts={}) def pgt_mangled_table_name(table) quote_schema_table(table).gsub('"', '').gsub(/[^A-Za-z0-9]/, '_').gsub(/_+/, '_') end + + def pgt_pg_trigger_depth_guard_clause(prevent_depth) + return unless prevent_depth + prevent_depth = 1 if true == prevent_depth + <<-SQL + IF pg_trigger_depth() > #{prevent_depth} THEN + RETURN NEW; + END IF; + SQL + end end module PGTMethods diff --git a/spec/sequel_postgresql_triggers_spec.rb b/spec/sequel_postgresql_triggers_spec.rb index f4b9f63..6fd1672 100644 --- a/spec/sequel_postgresql_triggers_spec.rb +++ b/spec/sequel_postgresql_triggers_spec.rb @@ -481,6 +481,74 @@ end end +describe "PostgreSQL Sum Through Many Cache Trigger with circular relation" do + before do + DB.create_table(:people) do + primary_key :id; + integer :amount, :null=>false, :default=>0; + integer :nonzero_entries_count, :default=>0, :null=>false + end + DB.create_table(:followings){integer :followee_id, :null=>false; integer :follower_id, :null=>false;} + DB.pgt_sum_through_many_cache( + :main_table=>:people, + :sum_column=>:nonzero_entries_count, + :summed_table=>:people, + :summed_column=>Sequel.case({0=>0}, 1, :amount), + :join_table=>:followings, + :main_table_fk_column=>:followee_id, + :summed_table_fk_column=>:follower_id, + :function_name=>:spgt_stm_cache, + :join_function_name=>:spgt_stm_cache_join, + :prevent_depth=>true + ) + DB[:people].insert(:id=>1) + DB[:people].insert(:id=>2) + end + + after do + DB.drop_table(:followings, :people) + DB.drop_function(:spgt_stm_cache) + DB.drop_function(:spgt_stm_cache_join) + end + + it "should count mutual reference" do + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0] + DB[:followings].insert(:followee_id=>1, :follower_id=>2) + DB[:followings].insert(:followee_id=>2, :follower_id=>1) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0] + DB[:people].update(:amount=>1) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1] + end + + it "should modify sum cache when adding, updating, or removing join records and records" do + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0] + + DB[:people].update(:amount=>1) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0] + + DB[:followings].insert(:followee_id=>1, :follower_id=>2) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0] + + DB[:followings].where(:followee_id=>1, :follower_id=>2).update(:followee_id=>2, :follower_id=>1) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 1] + + DB[:followings].where(:followee_id=>2, :follower_id=>1).delete + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0] + + DB[:people].insert(:id=>3) + DB[:followings].insert(:followee_id=>1, :follower_id=>2) + DB[:followings].insert(:followee_id=>1, :follower_id=>3) + DB[:followings].insert(:followee_id=>2, :follower_id=>3) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0, 0] + + DB[:people].where(:id=>3).update(:amount=>1000) + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1, 0] + + DB[:people].where(:id=>3).delete + DB[:people].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0] + end +end + describe "PostgreSQL Updated At Trigger" do before do DB.create_table(:accounts){integer :id; timestamp :changed_on}