Class: Hecks::Adapters::SqliteProjection
- Defined in:
- lib/hecks/adapters/driven/sqlite/projection.rb
Overview
Rebuilds a read store from the authoritative journal, entry by entry. A value object is written as its JSON object and a reference as the bare id it holds — the same shapes the command path writes, so there is no second representation to accept here any more.
Constant Summary
Constants inherited from Sqlite
Hecks::Adapters::Sqlite::SQL_TYPES
Constants included from SqlQueryBuilder
Hecks::Adapters::SqlQueryBuilder::COMPARATORS
Instance Attribute Summary
Attributes inherited from Sqlite
Instance Method Summary collapse
- #project(entry) ⇒ Object
-
#query_read_model(_domain, model, args, bluebook = nil) ⇒ Object
Execute a declared read model against the projected aggregate-head tables.
Methods inherited from Sqlite
#all, #append, #atomic_put, #count, #delete, #delete_saga, #each_saga, #entries, #events, #find, #initialize, #persistence_capabilities, #record_event, #reset!, #save, #save_saga, #table
Methods included from SqlQueryBuilder
Constructor Details
This class inherits a constructor from Hecks::Adapters::Sqlite
Instance Method Details
#project(entry) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/hecks/adapters/driven/sqlite/projection.rb', line 20 def project(entry) return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete? columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) } values = [entry.id.to_s] + persisted_fields.map { |field| encode_field(field, entry.state[field[:name]]) } @db.execute( "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{Array.new(columns.size, '?').join(', ')})", values ) entry end |
#query_read_model(_domain, model, args, bluebook = nil) ⇒ Object
Execute a declared read model against the projected aggregate-head tables. The report shape is assembled from SQL-selected rows, rather than scanning repositories and matching references in Ruby.
M19 (docs/audits/2026-08-10-main-bug-audit.md,
docs/audits/2026-08-11-bug-triage.md) — this used to diverge from
Runtime::ReadModelInterpreter#project (the in-process path) on
two counts, both fixed here to agree with it:
MISSING ROOT: the in-process path's own fetch refuses with
NotFound when the reference argument names no record —
query_read_model used to answer a silent {root: nil, ...}
instead, the one path a caller could dispatch a read model
against a record that never existed and get back something that
LOOKS like an empty report rather than the refusal every other
path gives.
CHAINED-INCLUDE JOIN SCOPE: a non-root head was always matched
against the ROOT's own id, regardless of what it actually
references — correct for a head that references the root
directly, silently EMPTY for one that references another
included head instead (Leaf -> Mid -> Root, Leaf itself
has no attribute referencing Root at all, so references
was always []). The in-process path's own root-first fix
(ReadModelInterpreter#project's "ROOT FIRST, ALWAYS" comment)
already matches a head against ANY already-projected source, not
only the root — select_related now does the same: each head is
matched against every source resolved so far (root first, then
declared order — the same one-level-of-declaration-order
dependency the in-process path itself still has, documented
there as L2, not a gap introduced here).
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/hecks/adapters/driven/sqlite/projection.rb', line 63 def query_read_model(_domain, model, args, bluebook = nil) raise ArgumentError, "projection query needs its domain bluebook" unless bluebook # The REFERENCE's own shape, read the same way ReadModelInterpreter # reads it — not the identity unwrap, which is gone : an identity is # declared as a path and followed. reference_id = args.fetch(model.reference_name).to_s eligible = model.filtered_head_name # ROOT FIRST, ALWAYS — see this method's own header. Mirrors # `ReadModelInterpreter#project`'s identical partition, for the # identical reason: a later head's own join has to be able to # match against a root (or another head) already resolved. root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target } projected = [] reports = {} (root_heads + other_heads).each do |head| aggregate = bluebook.aggregate(head[:aggregate]) rows = if head[:aggregate] == model.reference_target [select_projected(aggregate, reference_id) || raise(Runtime::NotFound, Runtime::RefusalWording.render("NotFound", "read_model_reference_missing", aggregate: head[:aggregate], offered: Hecks::Rendering.describe(reference_id)))] else (aggregate, projected) end rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible projected << { aggregate: head[:aggregate], rows: rows } reports[head[:as]] = if head[:many] rows.map { |row| Runtime::Value.materialize(row.to_h) } else rows.first && Runtime::Value.materialize(rows.first.to_h) end end [reports] end |