Class: StandardLedger::Modes::Sql

Inherits:
Object
  • Object
show all
Defined in:
lib/standard_ledger/modes/sql.rb

Overview

‘:sql` mode: applies the projection by running a single recompute `UPDATE` against the target table, inside the entry’s ‘after_create` callback. The recompute SQL is supplied via the block-DSL’s ‘recompute “…”` clause; the `:target_id` placeholder is bound to the foreign-key value the entry holds for the target association.

Lower-overhead than ‘:inline` for projections expressible as `UPDATE target SET col = (SELECT aggregate(…) FROM entries WHERE …)` — there’s no Ruby-side handler invocation, no per-counter in-memory mutation, and no AR object load. Naturally rebuildable: ‘StandardLedger.rebuild!` runs the same statement against each target the log references, so the `after_create` and `rebuild!` paths share the same recompute SQL.

Like ‘:inline`, this fires from `after_create` (in the entry’s transaction), so failures roll the entry back alongside the projection. The notification payload’s ‘:target` field is `nil` for `:sql` mode — loading the target object would defeat the point of the mode (it’s meant to avoid Ruby-side reads). Subscribers get ‘entry`, the projection definition, and the timing.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.install!(entry_class) ⇒ void

This method returns an undefined value.

Install the ‘after_create` callback on `entry_class` exactly once. Subsequent calls (e.g. when a second `:sql` projection is added later in the class body) are no-ops — the same callback handles all `:sql` projections registered on the class.

Parameters:

  • entry_class (Class)

    the host entry class.

Raises:

  • (ArgumentError)

    when ‘entry_class` is not ActiveRecord-backed (no `after_create` hook available). `:sql` mode requires an AR-backed entry class — the recompute SQL runs through `entry.class.connection.exec_update`.



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/standard_ledger/modes/sql.rb', line 35

def self.install!(entry_class)
  return if entry_class.instance_variable_get(:@_standard_ledger_sql_installed)

  unless entry_class.respond_to?(:after_create)
    raise ArgumentError,
          "#{entry_class.name || entry_class.inspect} cannot use mode: :sql " \
          "because it does not respond to `after_create`. `:sql` mode requires " \
          "an ActiveRecord-backed entry class."
  end

  entry_class.after_create { StandardLedger::Modes::Sql.new.call(self) }
  entry_class.instance_variable_set(:@_standard_ledger_sql_installed, true)
end

Instance Method Details

#call(entry) ⇒ void

This method returns an undefined value.

Apply every ‘:sql` projection registered on the entry’s class. Called from the ‘after_create` callback installed by `.install!`.

For each definition: resolve the target’s foreign key from the entry, evaluate the optional ‘if:` guard, and run the recompute SQL with `:target_id` bound to the FK value.

A nil FK (target unset for this entry) is silently skipped — the entry simply doesn’t project onto a target this round. A guard returning false is also silently skipped.

Any exception raised by the SQL escapes — the entry’s transaction rolls back with the projection. The ‘<prefix>.projection.failed` notification fires before the re-raise so subscribers see the failure.

Parameters:

  • entry (ActiveRecord::Base)

    the just-created entry.



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/standard_ledger/modes/sql.rb', line 67

def call(entry)
  definitions = sql_definitions_for(entry.class)
  return if definitions.empty?

  definitions.each do |definition|
    next if definition.guard && !entry.instance_exec(&definition.guard)

    target_id = resolve_target_id(entry, definition)
    next if target_id.nil?

    run_recompute_sql(entry, definition, target_id)
  end
end