Class: Hecks::Adapters::PostgresEra
- Inherits:
-
Object
- Object
- Hecks::Adapters::PostgresEra
- Includes:
- SqlQueryBuilder
- Defined in:
- lib/hecks/ports/persistence/plugins/era/postgres_era.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb,
lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb
Overview
The enforcement-grade persistence adapter — and the only one that
declares the LINEAGE capability: it may act on shape drift
(translate, fork, merge) where every other adapter can only refuse
toward it. Sibling to the plain Postgres adapter (postgres.rb),
which is the same database with none of this machinery — pick
PostgresEra only once a domain actually needs to survive a shape
change live. See docs/implemented/postgres-era-adapter-split-plan.md for why
the two are split and what each one carries.
Storage model (see postgres_era/lineage.rb for the DDL):
- One journal per DOMAIN, list-partitioned by era, one ordinal sequence spanning partitions. Appends go there; nothing updates or deletes a journal row (immutability by privilege — UPDATE and DELETE revoked; a deployment's app role connects as a non-owner).
- Per aggregate, the HEAD is derived: era 1 reads a plain view
(latest save per id); later eras read a view overlaying the
materialized, translated ancestor tail with live current-era
rows.
projectis therefore a no-op — old entries are never rewritten, and the head is never a table anything writes. - State is ONE jsonb column. jsonb normalizes key order (and drops
duplicate keys), so anything comparing stored state — the corpus
history gate above all — must compare CANONICALIZED state, never
raw bytes;
bin/canonicalisedeep-sorts keys, which is exactly why the gate survives this normalization. - Query pushdown is the shared SqlQueryBuilder: every declared operator compiles fully into SQL, or the query refuses loudly.
Defined Under Namespace
Modules: LineageManager Classes: Lineage
Constant Summary
Constants included from SqlQueryBuilder
Instance Attribute Summary collapse
-
#aggregate ⇒ Object
readonly
Returns the value of attribute aggregate.
Class Method Summary collapse
- .connect_for(name, settings) ⇒ Object
- .era_check!(registry:, bluebook:, current_text:, settings:, directory: nil) ⇒ Object
-
.lineage_capable? ⇒ Boolean
The capability idiom: only PostgresEra answers true, and only PostgresEra carries an era_check! for the boot gate to delegate to.
-
.setting(settings, key, default: nil) ⇒ Object
BOTH SPELLINGS OF A SETTING ARE HONORED — a world's settings hash may arrive symbol-keyed (built straight in Ruby) or string-keyed (round-tripped through JSON), and a domain that names one is not obligated to also skip the other.
-
.tenant_capable? ⇒ Boolean
TENANT-CAPABLE — see Runtime::TenantCheck's own header for the full reasoning.
Instance Method Summary collapse
-
#all(order_by: nil, direction: :asc) ⇒ Object
order_by IS A RUNTIME VALUE, not framework-authored bluebook source like every other caller of order_expression — a query param off an HTTP request, in the console's case.
-
#append(entry) ⇒ Object
HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the ordinal is assigned by the column's own
nextval()default, inside this same statement, so the lock has to already be held before that default evaluates. -
#atomic_put(entry, insert_only: false) ⇒ Object
Outcome detection, journal append and every derived projection share the SAME transaction and domain write lock.
- #count ⇒ Object
- #delete(id) ⇒ Object
- #delete_saga(process_manager:, correlation:) ⇒ Object
- #each_saga ⇒ Object
- #entries ⇒ Object
- #events ⇒ Object
- #find(id) ⇒ Object
-
#initialize(aggregate:, settings: {}, root: nil) ⇒ PostgresEra
constructor
A new instance of PostgresEra.
- #persistence_capabilities ⇒ Object
-
#project(entry) ⇒ Object
The head is DERIVED — projecting is reading, so there is nothing to write here.
-
#query(declared, args = {}, context: {}) ⇒ Object
THE TWO-PHASE SHORTCUT (Track C, docs/implemented/postgres-era-adapter- split-plan.md §3).
- #record_event(event) ⇒ Object
-
#reset! ⇒ Object
The journal carries FORCE ROW LEVEL SECURITY with exactly two policies — hecks_current_era's INSERT and hecks_read_all's SELECT (advance_era! above) — and no DELETE policy at all, for anyone.
- #save(instance) ⇒ Object
-
#save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: []) ⇒ Object
── the OPTIONAL saga-persistence capability (Ports::Persistence's own three-method shape, §2) — one row per (domain, process_manager, correlation),
domainkept as an explicit column even under schema isolation so two domains sharing one schema (neither declares its ownschema) still isolate correctly, matchinghecks_eras' own precedent (postgres/lineage/provisioning.rb). - #table ⇒ Object
Constructor Details
#initialize(aggregate:, settings: {}, root: nil) ⇒ PostgresEra
Returns a new instance of PostgresEra.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 147 def initialize(aggregate:, settings: {}, root: nil) @aggregate = aggregate @db = self.class.connect_for(aggregate.name, settings) # The domain names the journal (one journal per lineage). The # factory injects it; a directly-instantiated adapter (specs, # consoles) journals under the aggregate's own name. @domain = self.class.setting(settings, :domain, default: aggregate.storage_name).to_s @lineage = Lineage.new(@db, @domain) @lineage.ensure_base! # The era gate resolves which era this boot IS (an old checkout # boots a held-but-superseded era and keeps writing its own # partition); a directly-instantiated adapter defaults to the # newest. # # NOT `self.class.setting(...)` here — RepositoryFactory#build # always merges `era: registry.resolved_eras[domain]` into # settings, so the key is genuinely PRESENT (not absent) for # any domain the era boot gate hasn't resolved yet (or that # doesn't have one at all), just holding `nil`. `setting`'s own # presence-over-truthiness discipline (correct for a field like # `:role`, where a stored `false` is a real, distinct answer # from "unset") does the wrong thing for `:era` specifically: # `nil` can never be a meaningful era override — only a real # ordinal or "not resolved, self-resolve" ever apply — so # coalescing here, not `setting`'s presence check, is what # actually honors this comment's own "defaults to the newest" # promise. @era = settings.key?(:era) ? settings[:era] : settings["era"] @era ||= @lineage.current_era # Unconditional and idempotent, regardless of era — belt-and- # suspenders self-healing (compile_head! already ensures this for # a freshly-minted era's own name; ensure_first_head! for era 1's) # against any boot-ordering surprise, at the cost of one # CREATE TABLE IF NOT EXISTS nobody pays for twice. @lineage.ensure_head_snapshot!(table, @era) @lineage.ensure_first_head!(table) if @era == 1 # THE READ-CACHE SIDE OF THE ERA WORKAROUND (Track C, # docs/implemented/postgres-era-adapter-split-plan.md §3) — one row-cache # table per `where`-field this aggregate's own declared queries # (and its entities' own) actually use, derived automatically # (principle 3 — no bluebook keyword), self-healing and # idempotent like everything else booted here. `@field_caches` # maps field -> cache-table name; `query` below consults it to # decide whether a declared query can skip the DISTINCT ON # reduction entirely. @field_caches = ensure_field_caches! create_event_table! create_saga_table! end |
Instance Attribute Details
#aggregate ⇒ Object (readonly)
Returns the value of attribute aggregate.
44 45 46 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 44 def aggregate @aggregate end |
Class Method Details
.connect_for(name, settings) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 88 def self.connect_for(name, settings) # LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize: # a domain that never wires PostgresEra should never need the gem. require "pg" declared = setting(settings, :database) if declared.to_s.empty? raise Runtime::WiringError, "#{name} binds PostgresEra, which needs a database connection, " \ "but its world declares no \"database\"." end connection = if declared.start_with?("postgres://", "postgresql://") PG.connect(declared) else PG.connect(dbname: declared) end # SHARED-INSTANCE ISOLATION. A domain that declares `schema` is # sharing its Postgres instance with other domains (the # storehouse) — every unqualified table/view/function reference # this adapter and its lineage classes ever construct resolves # through search_path, so this one SET is what makes ALTER # TABLE ... SET SCHEMA migrations transparent to the rest of the # adapter. A domain with no `schema` setting keeps Postgres's # own default search_path (public), same as before this existed. schema = setting(settings, :schema) if schema.to_s != "" # THE SCHEMA ITSELF, IDEMPOTENTLY — a domain naming a `schema:` # nobody has created yet used to fail on its FIRST table- # creation attempt with Postgres's own "no schema has been # selected to create in", found live provisioning tenant_ # isolation_spec.rb's own multi-schema fixture by hand before # this existed. `CREATE SCHEMA IF NOT EXISTS` is exactly the # same self-healing idempotency this adapter's own table/era # provisioning already holds itself to (see the comment right # below on `client_min_messages`) — a schema that already # exists is the ORDINARY case for every boot after the first, # not news. connection.exec("CREATE SCHEMA IF NOT EXISTS #{connection.quote_ident(schema)}") connection.exec("SET search_path TO #{connection.quote_ident(schema)}") end # QUIET ON PURPOSE. Provisioning re-runs its own idempotent # `CREATE ... IF NOT EXISTS` checks on every boot — a schema that # already exists is the ORDINARY case, not news, and Postgres # surfaces every one as a NOTICE by default. `bin/set-password` # boots a real registry just to mint an Identity, and nobody # setting a password needs to see a page of "relation ... # already exists, skipping" to do it. WARNING and above (real # problems) still surface. connection.exec("SET client_min_messages = warning") connection rescue PG::Error => error raise Runtime::WiringError, "cannot bind PostgresEra at #{declared} for #{name}: #{error..strip}" end |
.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil) ⇒ Object
63 64 65 66 67 68 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 63 def self.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil) LineageManager.check!( registry: registry, bluebook: bluebook, current_text: current_text, settings: settings, directory: directory ) end |
.lineage_capable? ⇒ Boolean
The capability idiom: only PostgresEra answers true, and only PostgresEra carries an era_check! for the boot gate to delegate to.
50 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 50 def self.lineage_capable? = true |
.setting(settings, key, default: nil) ⇒ Object
BOTH SPELLINGS OF A SETTING ARE HONORED — a world's settings hash
may arrive symbol-keyed (built straight in Ruby) or string-keyed
(round-tripped through JSON), and a domain that names one is not
obligated to also skip the other. key? decides which spelling
actually exists, never || — || cannot tell a genuinely stored
false apart from an absent key, and would silently prefer the
OTHER spelling (or default) instead of returning the real, held
answer. See Hecks::QuerySpecification::FieldPath#read for the same
discipline applied to stored state instead of settings.
79 80 81 82 83 84 85 86 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 79 def self.setting(settings, key, default: nil) return settings[key] if settings.key?(key) str_key = key.to_s return settings[str_key] if settings.key?(str_key) default end |
.tenant_capable? ⇒ Boolean
TENANT-CAPABLE — see Runtime::TenantCheck's own header for the
full reasoning. connect_for's own schema: setting (the
Storehouse shared-instance mechanism, already built, already
proven for eras) is what keeps two tenant boots' tables apart:
each boot's own SET search_path means every unqualified
reference this adapter and its lineage classes construct
resolves into that boot's own schema, never another tenant's —
proven for real, not assumed, by tenant_isolation_spec.rb, the
same discipline lineage_capable? already holds itself to.
61 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 61 def self.tenant_capable? = true |
Instance Method Details
#all(order_by: nil, direction: :asc) ⇒ Object
order_by IS A RUNTIME VALUE, not framework-authored bluebook source like every other caller of order_expression — a query param off an HTTP request, in the console's case. Whitelisted against the aggregate's own real attributes (plus its lifecycle field) before it ever reaches order_expression, unlike a declared query's order_by, which the language itself already only lets name a real attribute at parse time. Without this, an unknown field wouldn't error — query_expression degrades a nil attribute to a harmless no-op path — it would just silently sort by nothing.
215 216 217 218 219 220 221 222 223 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 215 def all(order_by: nil, direction: :asc) return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by name = order_by.to_s.split(".").first raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name) spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction) @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) } end |
#append(entry) ⇒ Object
HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the
ordinal is assigned by the column's own nextval() default, inside
this same statement, so the lock has to already be held before that
default evaluates. A DIFFERENT key from mint_era!/merge_tail!'s
hecks_eras:domain : this serializes plain writes against EACH
OTHER, never against a mint. See postgres/lineage.rb's own comment
for why only that half of the race is closed.
The journal insert and the snapshot upsert/delete happen in the
SAME transaction — real ACID atomicity, not the append-then-
project two-step a file-based adapter needs a crash-recovery
replay for (see Heki). If this transaction commits, the snapshot
is already exactly as current as the journal; if it doesn't,
neither happened. project stays uninvolved on purpose — it
still runs, cheaply, during AppendOnly#recover!'s full replay on
every boot (see project below), and a second write there would
make that replay pay real DB cost for a snapshot that's already
correct.
288 289 290 291 292 293 294 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 288 def append(entry) @db.transaction do lock_writes! append_and_project!(entry) end entry end |
#atomic_put(entry, insert_only: false) ⇒ Object
Outcome detection, journal append and every derived projection share
the SAME transaction and domain write lock. The lineage-aware head
determines whether this id is already visible; no repository find
occurs before entering this adapter-native operation.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 300 def atomic_put(entry, insert_only: false) status = nil @db.transaction do lock_writes! exists = !@db.exec_params( "SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1", [entry.id.to_s] ).ntuples.zero? if insert_only && exists status = :conflicted next end status = exists ? :replaced : :inserted append_and_project!(entry) end status end |
#count ⇒ Object
225 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 225 def count = @db.exec(%(SELECT COUNT(*) FROM #{quoted_head}))[0]["count"].to_i |
#delete(id) ⇒ Object
379 380 381 382 383 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 379 def delete(id) entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil) append(entry) true end |
#delete_saga(process_manager:, correlation:) ⇒ Object
425 426 427 428 429 430 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 425 def delete_saga(process_manager:, correlation:) @db.exec_params( "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3", [@domain, process_manager.to_s, correlation.to_s] ) end |
#each_saga ⇒ Object
432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 432 def each_saga return enum_for(:each_saga) unless block_given? @db.exec_params( "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1", [@domain] ).each do |row| yield row["process_manager"], row["correlation"], row["state"], JSON.parse(row["memory"], symbolize_names: true), JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true) end end |
#entries ⇒ Object
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 329 def entries @db.exec_params( "SELECT aggregate_id, operation, state, mirrors FROM #{@lineage.quoted_journal} " \ "WHERE aggregate = $1 ORDER BY ordinal", [table] ).map do |row| state = row["state"] && JSON.parse(row["state"]) Ports::Persistence::Entry.new( operation: row["operation"] || "save", id: row["aggregate_id"], state: state&.transform_keys(&:to_sym), mirrors: row["mirrors"] && JSON.parse(row["mirrors"]) ) end end |
#events ⇒ Object
392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 392 def events @db.exec("SELECT * FROM events ORDER BY id").map do |row| Runtime::Event.new( name: row["name"], aggregate: row["aggregate"], id: row["aggregate_id"], payload: JSON.parse(row["payload"], symbolize_names: true), occurred_at: row["occurred_at"] ) end end |
#find(id) ⇒ Object
199 200 201 202 203 204 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 199 def find(id) result = @db.exec_params(%(SELECT id, state FROM #{quoted_head} WHERE id = $1), [id.to_s]) return nil if result.ntuples.zero? instance(result[0]) end |
#persistence_capabilities ⇒ Object
46 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 46 def persistence_capabilities = [:atomic_put] |
#project(entry) ⇒ Object
The head is DERIVED — projecting is reading, so there is nothing
to write here. append above already keeps the snapshot the head
view reads from current, transactionally. The instance is still
built (and validated) so a save returns what every other adapter
returns.
323 324 325 326 327 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 323 def project(entry) return if entry.delete? Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state) end |
#query(declared, args = {}, context: {}) ⇒ Object
THE TWO-PHASE SHORTCUT (Track C, docs/implemented/postgres-era-adapter-
split-plan.md §3). SqlQueryBuilder#query (super, unmodified per
principle 2) always runs correctly here — it filters against
head_view, which is already the fully-reduced current state —
but for a domain that has minted a second era, that reduction
itself is the expensive part, and no index on the jsonb path
changes that (see field_cache.rb's own header for the SQL-
semantics reason why). When every where clause this query
declares either targets a cached field or is eligible to (an
ordinary comparator, not a null-vs-value special case — see
cache_eligible?), skip the reduction: look candidate ids up in
the cache table(s) first (cheap, indexed, no reduction involved),
then read ONLY those ids' current state from head_view — safe
THROUGH the reduction because id is its own partition key. Any
clause that ISN'T cache-eligible (an uncached field, or a null
comparison) is simply re-checked against head_view in the
second phase, exactly as super would have checked it anyway —
this can only ever NARROW what phase two has to look at, never
change what a clause means.
FALLS BACK TO super WHENEVER NO CLAUSE CAN BE ACCELERATED — a
query with no where at all (order_by-only — no cache table
exists for these, see field_cache.rb), a query whose only clauses
target fields with no cache table, or a domain that has never
minted a second era at all (@field_caches is never empty just
because era 1 has no reduction to skip — the cache tables still
exist and still accelerate era 1 the same way, but the fallback
path is already just as cheap there since head_view IS the
snapshot table verbatim for era 1; skipping straight to super
in that case would be a valid FUTURE optimization, not attempted
here to keep this one code path correct for every era uniformly).
258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 258 def query(declared, args = {}, context: {}) return super if @field_caches.empty? || declared.wheres.empty? evaluated = declared.wheres.map { |clause| [clause, query_value(clause.value, args)] } cached, uncached = evaluated.partition { |clause, value| cache_eligible?(clause, value) } return super if cached.empty? ids = cache_phase(cached) return [] if ids.empty? head_phase(declared, uncached, ids, args) end |
#record_event(event) ⇒ Object
385 386 387 388 389 390 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 385 def record_event(event) @db.exec_params( "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)", [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at] ) end |
#reset! ⇒ Object
The journal carries FORCE ROW LEVEL SECURITY with exactly two
policies — hecks_current_era's INSERT and hecks_read_all's
SELECT (advance_era! above) — and no DELETE policy at all, for
anyone. FORCE means even the table's own owner is fenced by
that (only an actual Postgres superuser or a role granted
BYPASSRLS sits above it — see lineage.rb's own header), so a
plain DELETE ... WHERE aggregate = $1 from an ordinary
connection silently matches zero rows: no privilege error, no
exception, just a no-op that looks like success. Counting
before and comparing to what the DELETE itself reports is what
tells "nothing to delete" apart from "RLS silently ate the
delete" — the same row count, from the same statement, either
way, with no separate query racing the DELETE for an answer.
358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 358 def reset! before = @db.exec_params( "SELECT count(*) FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table] )[0]["count"].to_i result = @db.exec_params("DELETE FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table]) if before.positive? && result.cmd_tuples.zero? raise Runtime::WiringError, "reset! deleted 0 of #{before} row(s) for #{table} in #{@lineage.quoted_journal} — " \ "FORCE ROW LEVEL SECURITY admits no DELETE policy on the journal, so this connection's " \ "DELETE silently matched nothing. reset! only works connected as an actual Postgres " \ "superuser or a role granted BYPASSRLS, not as the provisioner or an app role." end self end |
#save(instance) ⇒ Object
373 374 375 376 377 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 373 def save(instance) entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup) append(entry) project(entry) end |
#save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: []) ⇒ Object
── the OPTIONAL saga-persistence capability (Ports::Persistence's
own three-method shape, §2) — one row per (domain, process_manager,
correlation), domain kept as an explicit column even under
schema isolation so two domains sharing one schema (neither
declares its own schema) still isolate correctly, matching
hecks_eras' own precedent (postgres/lineage/provisioning.rb).
No advisory lock of its own: every call here already runs inside
SagaInterpreter's own mutex (§7) serializing IN-PROCESS writers,
and gets the SAME cross-process safety an aggregate's own writes
get from this adapter — no better, no worse.
414 415 416 417 418 419 420 421 422 423 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 414 def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: []) @db.exec_params( "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \ "VALUES ($1, $2, $3, $4, $5, $6) " \ "ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \ "SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \ "completed_compensations = EXCLUDED.completed_compensations, updated_at = now()", [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)] ) end |
#table ⇒ Object
197 |
# File 'lib/hecks/ports/persistence/plugins/era/postgres_era.rb', line 197 def table = @aggregate.storage_name |