From be7898fdc91afe812413642bf5169e8de89aa598 Mon Sep 17 00:00:00 2001 From: bougyman Date: Thu, 13 Feb 2025 14:28:09 -0600 Subject: [PATCH 1/7] feat: Adds transactional outbox setup and trigger (#1) * feat: Adds transactional outbox setup and trigger * docs: Adds README section about outbox trigger * test: Adds spec for outbox trigger * test: Completes basic spec for outbox * test: Gets specs back to 100% line/branch coverage --- README.rdoc | 49 +++++++ lib/sequel/extensions/pg_triggers.rb | 64 +++++++++ spec/sequel_postgresql_triggers_spec.rb | 176 +++++++++++++++++++++--- 3 files changed, 268 insertions(+), 21 deletions(-) diff --git a/README.rdoc b/README.rdoc index 465b1b9..63dca87 100644 --- a/README.rdoc +++ b/README.rdoc @@ -265,6 +265,55 @@ function :: The name of the trigger function to call to log changes Note that it is probably a bad idea to use the same table argument to both +pgt_json_audit_log_setup+ and +pgt_json_audit_log+. +=== Transactional Outbox Events - pgt_outbox_setup and pgt_outbox_events + +These methods setup an outbox table and write events to it when +writes happen to the watched table. + +==== pgt_outbox_setup + +This creates an outbox table and a trigger function that will write +event data to the outbox table. This returns the name of the +trigger function created, which should be passed to ++pgt_outbox_events+. + +Arguments: +table :: The name of the table storing the audit logs. + +Options: +function_name :: The name of the trigger function +outbox_table :: The name for the outbox table. Defaults to table_outbox +event_prefix :: The prefix to use for event_type, defaults to table_ (table_updated, table_created, table_deleted) +boolean_completed_column :: If this is true, the :completed column will be boolean, otherwise it will be timestamptz +uuid_primary_key :: Use a uuid type for the primary key of the outbox table +uuid_function :: The pl/pgsql function name to use for generating a uuid pkey. defaults to :generate_uuid_v4 +function_opts :: Options to pass to +create_function+ when creating the trigger function. +Column Name Options: (column type in parenthesis) +created_column :: defaults to :created (timestamptz) +updated_column :: defaults to :updated (timestamptz) +attempts_column :: defaults to :attempts (Integer) +attempted_column :: defaults to :attempted (timestamptz) +completed_column :: defaults to :completed (Boolean or timestamptz, depending on :boolean_completed_column) +event_type_column :: defaults to :event_type (String) +last_error_column :: defaults to :last_error (String) +data_before_column :: defaults to :data_before (jsonb) +data_after_column :: defaults to :data_after (jsonb) +metadata_column :: defaults to :metadata (jsonb) + +==== pgt_outbox_events + +This adds a trigger to the table that will store events in the outbox table +when updates occur on the table (and match the filter). + +Arguments: +table :: The name of the table to audit +function :: The name of the trigger function to call to log changes (usually returned from pgt_outbox_setup) + +Options: +events :: The events to care about. Defaults to [:updated, :deleted, :created] (all writes) +trigger_name :: The name for the trigger +when :: A filter for the trigger, where clause if you will + == Caveats If you have defined counter or sum cache triggers using this library diff --git a/lib/sequel/extensions/pg_triggers.rb b/lib/sequel/extensions/pg_triggers.rb index dd46953..7cbda09 100644 --- a/lib/sequel/extensions/pg_triggers.rb +++ b/lib/sequel/extensions/pg_triggers.rb @@ -322,6 +322,70 @@ def pgt_foreign_key_array(opts={}) SQL end + def pgt_outbox_setup(table, opts={}) + function_name = opts.fetch(:function_name, "pgt_outbox_#{pgt_mangled_table_name(table)}") + outbox_table = opts.fetch(:outbox_table, "#{table}_outbox") + quoted_outbox = quote_schema_table(outbox_table) + event_prefix = opts.fetch(:event_prefix, table) + created_column = opts.fetch(:created_column, :created) + updated_column = opts.fetch(:updated_column, :updated) + event_type_column = opts.fetch(:event_type_column, :event_type) + data_after_column = opts.fetch(:data_after_column, :data_after) + data_before_column = opts.fetch(:data_before_column, :data_before) + boolean_completed_column = opts.fetch(:boolean_completed_column, false) + uuid_primary_key = opts.fetch(:uuid_primary_key, false) + run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"' if uuid_primary_key + create_table(outbox_table) do + if uuid_primary_key + uuid_function = opts.fetch(:uuid_function, :uuid_generate_v4) + uuid :id, default: Sequel.function(uuid_function), primary_key: true + else + primary_key :id + end + Integer opts.fetch(:attempts_column, :attempts), null: false, default: 0 + column created_column, :timestamptz + column updated_column, :timestamptz + column opts.fetch(:attempted_column, :attempted), :timestamptz + if boolean_completed_column + FalseClass opts.fetch(:completed_column, :completed), null: false, default: false + else + column opts.fetch(:completed_column, :completed), :timestamptz + end + String event_type_column, null: false + String opts.fetch(:last_error_column, :last_error) + jsonb data_before_column + jsonb data_after_column + jsonb opts.fetch(:metadata_column, :metadata) + end + pgt_created_at outbox_table, created_column + pgt_updated_at outbox_table, updated_column + create_function(function_name, (<<-SQL), {:language=>:plpgsql, :returns=>:trigger, :replace=>true}.merge(opts[:function_opts]||{})) + BEGIN + #{pgt_pg_trigger_depth_guard_clause(opts)} + IF (TG_OP = 'INSERT') THEN + INSERT INTO #{quoted_outbox} ("#{event_type_column}", "#{data_after_column}") VALUES + ('#{event_prefix}_created', to_jsonb(NEW)); + RETURN NEW; + ELSIF (TG_OP = 'UPDATE') THEN + INSERT INTO #{quoted_outbox} ("#{event_type_column}", "#{data_before_column}", "#{data_after_column}") VALUES + ('#{event_prefix}_updated', to_jsonb(OLD), to_jsonb(NEW)); + RETURN NEW; + ELSIF (TG_OP = 'DELETE') THEN + INSERT INTO #{quoted_outbox} ("#{event_type_column}", "#{data_before_column}") VALUES + ('#{event_prefix}_deleted', to_jsonb(OLD)); + RETURN OLD; + END IF; + END; + SQL + function_name + end + + def pgt_outbox_events(table, function, opts={}) + events = opts.fetch(:events, [:insert, :update, :delete]) + trigger_name = opts.fetch(:trigger_name, "pgt_outbox_#{pgt_mangled_table_name(table)}") + create_trigger(table, trigger_name, function, events: events, replace: true, each_row: true, after: true, when: opts[:when]) + end + private # Add or replace a function that returns trigger to handle the action, diff --git a/spec/sequel_postgresql_triggers_spec.rb b/spec/sequel_postgresql_triggers_spec.rb index bd05a50..370ab54 100644 --- a/spec/sequel_postgresql_triggers_spec.rb +++ b/spec/sequel_postgresql_triggers_spec.rb @@ -25,7 +25,7 @@ require 'sequel_postgresql_triggers' else puts "Running specs with extension" - DB.extension :pg_triggers + DB.extension :pg_triggers end DB.extension :pg_array @@ -59,31 +59,31 @@ DB[:entries].insert(:id=>2, :account_id=>1) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0] - + DB[:entries].insert(:id=>3, :account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0] - + DB[:entries].where(:id=>3).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 1] - + DB[:entries].where(:id=>2).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2] - + DB[:entries].where(:id=>2).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].where(:id=>2).update(:id=>4) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].where(:id=>4).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2] - + DB[:entries].where(:id=>4).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].filter(:id=>4).delete DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].delete DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0] end @@ -195,34 +195,34 @@ DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0] - + DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0] - + DB[:entries].where(:id=>3).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 500] - + DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2)) DB[:accounts].order(:id).select_map(:balance).must_equal [400, 1000] - + DB[:entries].where(:id=>2).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200] - + DB[:entries].where(:id=>2).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].where(:id=>2).update(:id=>4) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].where(:id=>4).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200] - + DB[:entries].where(:id=>4).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].filter(:id=>4).delete DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].delete DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0] end @@ -748,7 +748,6 @@ end end - describe "PostgreSQL JSON Audit Logging" do before do DB.extension :pg_json @@ -788,3 +787,138 @@ h.must_equal(:schema=>"public", :table=>"accounts", :action=>"DELETE", :prior=>{"a"=>3, "id"=>2}) end end if DB.server_version >= 90400 + +describe "Basic PostgreSQL Transactional Outbox" do + before do + DB.extension :pg_json + DB.create_table(:accounts){integer :id; String :s} + function_name = DB.pgt_outbox_setup(:accounts, :function_name=>:spgt_outbox_events) + DB.pgt_outbox_events(:accounts, function_name) + @logs = DB[:accounts_outbox].reverse(:created) + end + + after do + DB.drop_table(:accounts, :accounts_outbox) + DB.drop_function(:spgt_outbox_events) + end + + it "should store outbox events for writes on main table" do + @logs.first.must_be_nil + + ds = DB[:accounts] + ds.insert(id: 1, s: 'string') + ds.all.must_equal [{id: 1, s: 'string'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.must_equal(id: 1, attempts: 0, attempted: nil, completed: nil, event_type: "accounts_created", last_error: nil, data_before: nil, data_after: {"s" => "string", "id" => 1}, metadata: nil) + + ds.where(id: 1).update(s: 'string2') + ds.all.must_equal [{id: 1, s: 'string2'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.must_equal(id: 2, attempts: 0, attempted: nil, completed: nil, event_type: "accounts_updated", last_error: nil, data_before: {"s" => "string", "id" => 1}, data_after: {"s" => "string2", "id" => 1}, metadata: nil) + + ds.delete + ds.all.must_equal [] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.must_equal(id: 3, attempts: 0, attempted: nil, completed: nil, event_type: "accounts_deleted", last_error: nil, data_before: {"s" => "string2", "id" => 1}, data_after: nil, metadata: nil) + end +end if DB.server_version >= 90400 + +describe "PostgreSQL Transactional Outbox With UUID Pkey" do + before do + DB.extension :pg_json + DB.create_table(:accounts){integer :id; String :s} + function_name = DB.pgt_outbox_setup(:accounts, uuid_primary_key: true, function_name: :spgt_outbox_events) + DB.pgt_outbox_events(:accounts, function_name) + @logs = DB[:accounts_outbox].reverse(:created) + end + + after do + DB.drop_table(:accounts, :accounts_outbox) + DB.drop_function(:spgt_outbox_events) + end + + it "should store outbox events for writes on main table" do + @logs.first.must_be_nil + + ds = DB[:accounts] + ds.insert(id: 1, s: 'string') + ds.all.must_equal [{id: 1, s: 'string'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: nil, event_type: "accounts_created", last_error: nil, data_before: nil, data_after: {"s" => "string", "id" => 1}, metadata: nil) + + ds.where(id: 1).update(s: 'string2') + ds.all.must_equal [{id: 1, s: 'string2'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: nil, event_type: "accounts_updated", last_error: nil, data_before: {"s" => "string", "id" => 1}, data_after: {"s" => "string2", "id" => 1}, metadata: nil) + + ds.delete + ds.all.must_equal [] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: nil, event_type: "accounts_deleted", last_error: nil, data_before: {"s" => "string2", "id" => 1}, data_after: nil, metadata: nil) + end +end if DB.server_version >= 90400 + +describe "PostgreSQL Transactional Outbox With UUID Pkey" do + before do + DB.extension :pg_json + DB.create_table(:accounts){integer :id; String :s} + function_name = DB.pgt_outbox_setup(:accounts, uuid_primary_key: true, boolean_completed_column: true, function_name: :spgt_outbox_events) + DB.pgt_outbox_events(:accounts, function_name) + @logs = DB[:accounts_outbox].reverse(:created) + end + + after do + DB.drop_table(:accounts, :accounts_outbox) + DB.drop_function(:spgt_outbox_events) + end + + it "should store outbox events for writes on main table" do + @logs.first.must_be_nil + + ds = DB[:accounts] + ds.insert(id: 1, s: 'string') + ds.all.must_equal [{id: 1, s: 'string'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: false, event_type: "accounts_created", last_error: nil, data_before: nil, data_after: {"s" => "string", "id" => 1}, metadata: nil) + + ds.where(id: 1).update(s: 'string2') + ds.all.must_equal [{id: 1, s: 'string2'}] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: false, event_type: "accounts_updated", last_error: nil, data_before: {"s" => "string", "id" => 1}, data_after: {"s" => "string2", "id" => 1}, metadata: nil) + + ds.delete + ds.all.must_equal [] + h = @logs.first + h.delete(:created).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + h.delete(:updated).to_i.must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) + id = h.delete(:id) + id.must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) + h.must_equal(attempts: 0, attempted: nil, completed: false, event_type: "accounts_deleted", last_error: nil, data_before: {"s" => "string2", "id" => 1}, data_after: nil, metadata: nil) + end +end if DB.server_version >= 90400 From d2418ba4e1a5c256e440939498fa864e8d733841 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 20:33:23 +0000 Subject: [PATCH 2/7] fix: Corrects description of spec with boolean completed column --- spec/sequel_postgresql_triggers_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/sequel_postgresql_triggers_spec.rb b/spec/sequel_postgresql_triggers_spec.rb index 370ab54..f69960f 100644 --- a/spec/sequel_postgresql_triggers_spec.rb +++ b/spec/sequel_postgresql_triggers_spec.rb @@ -876,7 +876,7 @@ end end if DB.server_version >= 90400 -describe "PostgreSQL Transactional Outbox With UUID Pkey" do +describe "PostgreSQL Transactional Outbox With Boolean :completed field" do before do DB.extension :pg_json DB.create_table(:accounts){integer :id; String :s} From 6255285dd7811e12a8a6437760c0a63a4efeee9a Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 21:16:37 +0000 Subject: [PATCH 3/7] fix(time): Switches to Time for timestamp column types --- lib/sequel/extensions/pg_triggers.rb | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/sequel/extensions/pg_triggers.rb b/lib/sequel/extensions/pg_triggers.rb index 7cbda09..4d78770 100644 --- a/lib/sequel/extensions/pg_triggers.rb +++ b/lib/sequel/extensions/pg_triggers.rb @@ -343,23 +343,26 @@ def pgt_outbox_setup(table, opts={}) primary_key :id end Integer opts.fetch(:attempts_column, :attempts), null: false, default: 0 - column created_column, :timestamptz - column updated_column, :timestamptz - column opts.fetch(:attempted_column, :attempted), :timestamptz + Time created_column + Time updated_column + Time opts.fetch(:attempted_column, :attempted) if boolean_completed_column FalseClass opts.fetch(:completed_column, :completed), null: false, default: false else - column opts.fetch(:completed_column, :completed), :timestamptz + Time opts.fetch(:completed_column, :completed) end String event_type_column, null: false String opts.fetch(:last_error_column, :last_error) - jsonb data_before_column - jsonb data_after_column - jsonb opts.fetch(:metadata_column, :metadata) + jsonb data_before_column + jsonb data_after_column + jsonb opts.fetch(:metadata_column, :metadata) + index Sequel.asc(created_column) + index Sequel.desc(attempted_column) end pgt_created_at outbox_table, created_column pgt_updated_at outbox_table, updated_column - create_function(function_name, (<<-SQL), {:language=>:plpgsql, :returns=>:trigger, :replace=>true}.merge(opts[:function_opts]||{})) + function_opts = { language: :plpgsql, returns: :trigger, replace: true }.merge(opts.fetch(:function_opts, {})) + create_function(function_name, <<-SQL, function_opts) BEGIN #{pgt_pg_trigger_depth_guard_clause(opts)} IF (TG_OP = 'INSERT') THEN From cfe9f23902993a5cad2183f69a87ba255b785c20 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 21:51:15 +0000 Subject: [PATCH 4/7] fix: Adds missing attempted_column assignment, undoes whitespace changes --- lib/sequel/extensions/pg_triggers.rb | 3 +- spec/sequel_postgresql_triggers_spec.rb | 41 +++++++++++++------------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/sequel/extensions/pg_triggers.rb b/lib/sequel/extensions/pg_triggers.rb index 4d78770..451795d 100644 --- a/lib/sequel/extensions/pg_triggers.rb +++ b/lib/sequel/extensions/pg_triggers.rb @@ -332,6 +332,7 @@ def pgt_outbox_setup(table, opts={}) event_type_column = opts.fetch(:event_type_column, :event_type) data_after_column = opts.fetch(:data_after_column, :data_after) data_before_column = opts.fetch(:data_before_column, :data_before) + attempted_column = opts.fetch(:attempted_column, :attempted) boolean_completed_column = opts.fetch(:boolean_completed_column, false) uuid_primary_key = opts.fetch(:uuid_primary_key, false) run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"' if uuid_primary_key @@ -345,7 +346,7 @@ def pgt_outbox_setup(table, opts={}) Integer opts.fetch(:attempts_column, :attempts), null: false, default: 0 Time created_column Time updated_column - Time opts.fetch(:attempted_column, :attempted) + Time attempted_column if boolean_completed_column FalseClass opts.fetch(:completed_column, :completed), null: false, default: false else diff --git a/spec/sequel_postgresql_triggers_spec.rb b/spec/sequel_postgresql_triggers_spec.rb index f69960f..f71cdd7 100644 --- a/spec/sequel_postgresql_triggers_spec.rb +++ b/spec/sequel_postgresql_triggers_spec.rb @@ -25,7 +25,7 @@ require 'sequel_postgresql_triggers' else puts "Running specs with extension" - DB.extension :pg_triggers + DB.extension :pg_triggers end DB.extension :pg_array @@ -59,31 +59,31 @@ DB[:entries].insert(:id=>2, :account_id=>1) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0] - + DB[:entries].insert(:id=>3, :account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0] - + DB[:entries].where(:id=>3).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 1] - + DB[:entries].where(:id=>2).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2] - + DB[:entries].where(:id=>2).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].where(:id=>2).update(:id=>4) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].where(:id=>4).update(:account_id=>2) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2] - + DB[:entries].where(:id=>4).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].filter(:id=>4).delete DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1] - + DB[:entries].delete DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0] end @@ -195,34 +195,34 @@ DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0] - + DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0] - + DB[:entries].where(:id=>3).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [300, 500] - + DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2)) DB[:accounts].order(:id).select_map(:balance).must_equal [400, 1000] - + DB[:entries].where(:id=>2).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200] - + DB[:entries].where(:id=>2).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].where(:id=>2).update(:id=>4) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].where(:id=>4).update(:account_id=>2) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200] - + DB[:entries].where(:id=>4).update(:account_id=>nil) DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].filter(:id=>4).delete DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000] - + DB[:entries].delete DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0] end @@ -748,6 +748,7 @@ end end + describe "PostgreSQL JSON Audit Logging" do before do DB.extension :pg_json From ab84b4e3640642182d850d62a7aa3932cf694c26 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 22:00:21 +0000 Subject: [PATCH 5/7] fix(clean): Parse all column name opts before the create table block --- lib/sequel/extensions/pg_triggers.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/sequel/extensions/pg_triggers.rb b/lib/sequel/extensions/pg_triggers.rb index 451795d..a636236 100644 --- a/lib/sequel/extensions/pg_triggers.rb +++ b/lib/sequel/extensions/pg_triggers.rb @@ -329,10 +329,14 @@ def pgt_outbox_setup(table, opts={}) event_prefix = opts.fetch(:event_prefix, table) created_column = opts.fetch(:created_column, :created) updated_column = opts.fetch(:updated_column, :updated) + completed_column = opts.fetch(:completed_column, :completed) + attempts_column = opts.fetch(:attempts_column, :attempts) + attempted_column = opts.fetch(:attempted_column, :attempted) event_type_column = opts.fetch(:event_type_column, :event_type) + last_error_column = opts.fetch(:last_error_column, :last_error) data_after_column = opts.fetch(:data_after_column, :data_after) data_before_column = opts.fetch(:data_before_column, :data_before) - attempted_column = opts.fetch(:attempted_column, :attempted) + metadata_column = opts.fetch(:metadata_column, :metadata) boolean_completed_column = opts.fetch(:boolean_completed_column, false) uuid_primary_key = opts.fetch(:uuid_primary_key, false) run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"' if uuid_primary_key @@ -343,20 +347,20 @@ def pgt_outbox_setup(table, opts={}) else primary_key :id end - Integer opts.fetch(:attempts_column, :attempts), null: false, default: 0 + Integer attempts_column, null: false, default: 0 Time created_column Time updated_column Time attempted_column if boolean_completed_column - FalseClass opts.fetch(:completed_column, :completed), null: false, default: false + FalseClass completed_column, null: false, default: false else - Time opts.fetch(:completed_column, :completed) + Time completed_column end String event_type_column, null: false - String opts.fetch(:last_error_column, :last_error) + String last_error_column jsonb data_before_column jsonb data_after_column - jsonb opts.fetch(:metadata_column, :metadata) + jsonb metadata_column index Sequel.asc(created_column) index Sequel.desc(attempted_column) end From 52f5f9b53290642255734512356425c65220083d Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 18:05:27 -0600 Subject: [PATCH 6/7] docs: Updates README with the Time type for timestamp columns --- README.rdoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rdoc b/README.rdoc index 63dca87..3004e50 100644 --- a/README.rdoc +++ b/README.rdoc @@ -284,16 +284,16 @@ Options: function_name :: The name of the trigger function outbox_table :: The name for the outbox table. Defaults to table_outbox event_prefix :: The prefix to use for event_type, defaults to table_ (table_updated, table_created, table_deleted) -boolean_completed_column :: If this is true, the :completed column will be boolean, otherwise it will be timestamptz +boolean_completed_column :: If this is true, the :completed column will be boolean, otherwise it will be Time uuid_primary_key :: Use a uuid type for the primary key of the outbox table uuid_function :: The pl/pgsql function name to use for generating a uuid pkey. defaults to :generate_uuid_v4 function_opts :: Options to pass to +create_function+ when creating the trigger function. Column Name Options: (column type in parenthesis) -created_column :: defaults to :created (timestamptz) -updated_column :: defaults to :updated (timestamptz) +created_column :: defaults to :created (Time) +updated_column :: defaults to :updated (Time) attempts_column :: defaults to :attempts (Integer) -attempted_column :: defaults to :attempted (timestamptz) -completed_column :: defaults to :completed (Boolean or timestamptz, depending on :boolean_completed_column) +attempted_column :: defaults to :attempted (Time) +completed_column :: defaults to :completed (Boolean or Time, depending on :boolean_completed_column) event_type_column :: defaults to :event_type (String) last_error_column :: defaults to :last_error (String) data_before_column :: defaults to :data_before (jsonb) From 97ab2b7a390b81575e60b7c7b9ac003cc9801ee5 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Feb 2025 19:50:10 -0600 Subject: [PATCH 7/7] docs: Corrects name of default uuid generation function --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 3004e50..c485108 100644 --- a/README.rdoc +++ b/README.rdoc @@ -286,7 +286,7 @@ outbox_table :: The name for the outbox table. Defaults to table_outbox event_prefix :: The prefix to use for event_type, defaults to table_ (table_updated, table_created, table_deleted) boolean_completed_column :: If this is true, the :completed column will be boolean, otherwise it will be Time uuid_primary_key :: Use a uuid type for the primary key of the outbox table -uuid_function :: The pl/pgsql function name to use for generating a uuid pkey. defaults to :generate_uuid_v4 +uuid_function :: The pl/pgsql function name to use for generating a uuid pkey. defaults to :uuid_generate_v4 function_opts :: Options to pass to +create_function+ when creating the trigger function. Column Name Options: (column type in parenthesis) created_column :: defaults to :created (Time)