Class: Whodunit::Chronicles::ChangeEvent

Inherits:
Object
  • Object
show all
Defined in:
lib/whodunit/chronicles/change_event.rb

Overview

Represents a database change event in a common format

This class normalizes database changes from different sources (PostgreSQL WAL, MariaDB binlog, etc.) into a consistent format for processing by audit systems.

Constant Summary collapse

ACTIONS =

Supported database actions

%w[INSERT UPDATE DELETE].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(table_name:, action:, primary_key:, old_data: nil, new_data: nil, timestamp: Time.now, schema_name: 'public', transaction_id: nil, sequence_number: nil, metadata: {}) ⇒ ChangeEvent

Initialize a new change event

Parameters:

  • table_name (String)

    The name of the table that changed

  • action (String)

    The type of change (INSERT, UPDATE, DELETE)

  • primary_key (Hash)

    The primary key values for the changed row

  • old_data (Hash, nil) (defaults to: nil)

    The row data before the change (nil for INSERT)

  • new_data (Hash, nil) (defaults to: nil)

    The row data after the change (nil for DELETE)

  • timestamp (Time) (defaults to: Time.now)

    When the change occurred

  • schema_name (String) (defaults to: 'public')

    The schema name (optional, defaults to ‘public’)

  • transaction_id (String, Integer) (defaults to: nil)

    Database transaction identifier

  • sequence_number (Integer) (defaults to: nil)

    Sequence number within the transaction

  • metadata (Hash) (defaults to: {})

    Additional adapter-specific metadata



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/whodunit/chronicles/change_event.rb', line 29

def initialize(
  table_name:,
  action:,
  primary_key:,
  old_data: nil,
  new_data: nil,
  timestamp: Time.now,
  schema_name: 'public',
  transaction_id: nil,
  sequence_number: nil,
  metadata: {}
)
  @table_name = table_name.to_s
  @schema_name = schema_name.to_s
  @action = validate_action(action.to_s.upcase)
  @primary_key = primary_key || {}
  @old_data = old_data
  @new_data = new_data
  @timestamp = timestamp
  @transaction_id = transaction_id
  @sequence_number = sequence_number
  @metadata =  || {}

  validate_data_consistency
end

Instance Attribute Details

#actionObject (readonly)

Returns the value of attribute action.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def action
  @action
end

#metadataObject (readonly)

Returns the value of attribute metadata.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def 
  @metadata
end

#new_dataObject (readonly)

Returns the value of attribute new_data.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def new_data
  @new_data
end

#old_dataObject (readonly)

Returns the value of attribute old_data.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def old_data
  @old_data
end

#primary_keyObject (readonly)

Returns the value of attribute primary_key.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def primary_key
  @primary_key
end

#schema_nameObject (readonly)

Returns the value of attribute schema_name.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def schema_name
  @schema_name
end

#sequence_numberObject (readonly)

Returns the value of attribute sequence_number.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def sequence_number
  @sequence_number
end

#table_nameObject (readonly)

Returns the value of attribute table_name.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def table_name
  @table_name
end

#timestampObject (readonly)

Returns the value of attribute timestamp.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def timestamp
  @timestamp
end

#transaction_idObject (readonly)

Returns the value of attribute transaction_id.



14
15
16
# File 'lib/whodunit/chronicles/change_event.rb', line 14

def transaction_id
  @transaction_id
end

Instance Method Details

#==(other) ⇒ Boolean

Compare events for equality

Parameters:

Returns:

  • (Boolean)


163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/whodunit/chronicles/change_event.rb', line 163

def ==(other)
  return false unless other.is_a?(ChangeEvent)

  table_name == other.table_name &&
    schema_name == other.schema_name &&
    action == other.action &&
    primary_key == other.primary_key &&
    old_data == other.old_data &&
    new_data == other.new_data &&
    timestamp == other.timestamp &&
    transaction_id == other.transaction_id &&
    sequence_number == other.sequence_number
end

#all_dataHash

Get all available data for this event

Returns:

  • (Hash)

    Combined old and new data



118
119
120
# File 'lib/whodunit/chronicles/change_event.rb', line 118

def all_data
  (old_data || {}).merge(new_data || {})
end

#changed_columnsArray<String>

Get the changed columns for UPDATE events

Returns:

  • (Array<String>)

    Array of column names that changed



86
87
88
89
90
# File 'lib/whodunit/chronicles/change_event.rb', line 86

def changed_columns
  return [] unless update? && old_data && new_data

  old_data.keys.reject { |key| old_data[key] == new_data[key] }
end

#changesHash

Get a hash of changes in [old_value, new_value] format

Returns:

  • (Hash)

    Hash of column_name => [old_value, new_value]



95
96
97
98
99
100
101
# File 'lib/whodunit/chronicles/change_event.rb', line 95

def changes
  return {} unless update? && old_data && new_data

  changed_columns.each_with_object({}) do |column, changes_hash|
    changes_hash[column] = [old_data[column], new_data[column]]
  end
end

#create?Boolean

Check if this is a create event

Returns:

  • (Boolean)


65
66
67
# File 'lib/whodunit/chronicles/change_event.rb', line 65

def create?
  action == 'INSERT'
end

#current_dataHash

Get the current data for this event

Returns:

  • (Hash)

    The new_data for INSERT/UPDATE, old_data for DELETE



106
107
108
109
110
111
112
113
# File 'lib/whodunit/chronicles/change_event.rb', line 106

def current_data
  case action
  when 'INSERT', 'UPDATE'
    new_data
  when 'DELETE'
    old_data
  end
end

#delete?Boolean

Check if this is a delete event

Returns:

  • (Boolean)


79
80
81
# File 'lib/whodunit/chronicles/change_event.rb', line 79

def delete?
  action == 'DELETE'
end

#inspectString

Detailed string representation

Returns:

  • (String)


155
156
157
# File 'lib/whodunit/chronicles/change_event.rb', line 155

def inspect
  "#<#{self.class.name} #{self}>"
end

#qualified_table_nameString

Get the qualified table name (schema.table)

Returns:

  • (String)


58
59
60
# File 'lib/whodunit/chronicles/change_event.rb', line 58

def qualified_table_name
  "#{schema_name}.#{table_name}"
end

#to_hHash

Convert to hash representation

Returns:

  • (Hash)


125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/whodunit/chronicles/change_event.rb', line 125

def to_h
  {
    table_name: table_name,
    schema_name: schema_name,
    qualified_table_name: qualified_table_name,
    action: action,
    primary_key: primary_key,
    old_data: old_data,
    new_data: new_data,
    current_data: current_data,
    changes: changes,
    changed_columns: changed_columns,
    timestamp: timestamp,
    transaction_id: transaction_id,
    sequence_number: sequence_number,
    metadata: ,
  }
end

#to_sString

String representation

Returns:

  • (String)


147
148
149
150
# File 'lib/whodunit/chronicles/change_event.rb', line 147

def to_s
  pk_str = primary_key.map { |k, v| "#{k}=#{v}" }.join(', ')
  "#{action} #{qualified_table_name}(#{pk_str}) at #{timestamp}"
end

#update?Boolean

Check if this is an update event

Returns:

  • (Boolean)


72
73
74
# File 'lib/whodunit/chronicles/change_event.rb', line 72

def update?
  action == 'UPDATE'
end

#validate_action(action) ⇒ Object (private)



179
180
181
182
183
184
185
# File 'lib/whodunit/chronicles/change_event.rb', line 179

def validate_action(action)
  unless ACTIONS.include?(action)
    raise ArgumentError, "Invalid action: #{action}. Must be one of: #{ACTIONS.join(', ')}"
  end

  action
end

#validate_data_consistencyObject (private)



187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/whodunit/chronicles/change_event.rb', line 187

def validate_data_consistency
  case action
  when 'INSERT'
    raise ArgumentError, 'INSERT events must have new_data' if new_data.nil? || new_data.empty?
    raise ArgumentError, 'INSERT events should not have old_data' unless old_data.nil?
  when 'UPDATE'
    raise ArgumentError, 'UPDATE events must have both old_data and new_data' if old_data.nil? || new_data.nil?
  when 'DELETE'
    raise ArgumentError, 'DELETE events must have old_data' if old_data.nil? || old_data.empty?
    raise ArgumentError, 'DELETE events should not have new_data' unless new_data.nil?
  end
end