Skip to content

prevent infinite loops #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/sequel/extensions/pg_triggers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions spec/sequel_postgresql_triggers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down