hecks 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +7 -2
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +48 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
1
|
+
require_relative "../../era_tamper"
|
|
2
|
+
require_relative "../../../../../../runtime/registry"
|
|
3
|
+
require_relative "../../storage_shape"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../naming"
|
|
2
|
+
require_relative "../../translation/rule_compiler"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
@@ -67,15 +67,41 @@ module Hecks
|
|
|
67
67
|
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
|
|
68
68
|
next if table_exists?(name)
|
|
69
69
|
|
|
70
|
+
# `operation` + a NULLABLE `state` — H3, docs/audits/2026-08-
|
|
71
|
+
# 10-main-bug-audit.md: a delete used to just `DELETE FROM`
|
|
72
|
+
# this table, leaving NO row at all for an id carried in
|
|
73
|
+
# from an ancestor era. `compile_head!`'s union below then
|
|
74
|
+
# had nothing current-era to outrank the ancestor
|
|
75
|
+
# matview's own (still-live-looking) `save` row with, so a
|
|
76
|
+
# deleted ancestor-carried record kept winning `DISTINCT
|
|
77
|
+
# ON` forever. A delete now upserts a TOMBSTONE row here
|
|
78
|
+
# instead (`operation = 'delete'`, `state` NULL) — the
|
|
79
|
+
# exact same ordinal-guarded upsert every write already
|
|
80
|
+
# uses, so it participates in the union/`DISTINCT ON`
|
|
81
|
+
# exactly like a save does, and out-ranks the ancestor row
|
|
82
|
+
# by ordinal the same way a real re-save already did
|
|
83
|
+
# ("re-saves are masked correctly" — the audit's own
|
|
84
|
+
# phrasing for why this half of the read path was never
|
|
85
|
+
# broken).
|
|
70
86
|
@db.exec(<<~SQL)
|
|
71
87
|
CREATE TABLE #{quote(name)} (
|
|
72
|
-
id
|
|
73
|
-
ordinal
|
|
74
|
-
|
|
88
|
+
id text PRIMARY KEY,
|
|
89
|
+
ordinal bigint NOT NULL,
|
|
90
|
+
operation text NOT NULL DEFAULT 'save',
|
|
91
|
+
state jsonb
|
|
75
92
|
)
|
|
76
93
|
SQL
|
|
77
94
|
end
|
|
78
95
|
end
|
|
96
|
+
# SELF-HEALING for a table this ADR predates — same idiom as
|
|
97
|
+
# `postgres/schema_builder.rb`'s own `hecks_version` backfill:
|
|
98
|
+
# unconditional, runs on every boot, a no-op once the column
|
|
99
|
+
# is there. A table created before H3's fix has no
|
|
100
|
+
# `operation` column and a `state NOT NULL` constraint a
|
|
101
|
+
# tombstone's NULL state would violate; both are corrected
|
|
102
|
+
# here regardless of which branch above just ran.
|
|
103
|
+
@db.exec("ALTER TABLE #{quote(name)} ADD COLUMN IF NOT EXISTS operation text NOT NULL DEFAULT 'save'")
|
|
104
|
+
@db.exec("ALTER TABLE #{quote(name)} ALTER COLUMN state DROP NOT NULL")
|
|
79
105
|
backfill_head_snapshot!(name, storage_name, era)
|
|
80
106
|
end
|
|
81
107
|
|
|
@@ -105,10 +131,13 @@ module Hecks
|
|
|
105
131
|
end,
|
|
106
132
|
upsert: lambda do |rows|
|
|
107
133
|
rows.each do |row|
|
|
134
|
+
# Always `'save'` — `source_sql` above already filtered
|
|
135
|
+
# to `operation = 'save'`, so nothing this backfill ever
|
|
136
|
+
# sees is a delete tombstone.
|
|
108
137
|
@db.exec_params(
|
|
109
|
-
"INSERT INTO #{quote(name)} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
110
|
-
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal,
|
|
111
|
-
"WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
138
|
+
"INSERT INTO #{quote(name)} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
|
|
139
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
140
|
+
"state = EXCLUDED.state WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
112
141
|
[row["id"], row["ordinal"], row["state"]]
|
|
113
142
|
)
|
|
114
143
|
end
|
|
@@ -149,9 +178,15 @@ module Hecks
|
|
|
149
178
|
# already keeps the snapshot current as of every write.
|
|
150
179
|
def ensure_first_head!(storage_name)
|
|
151
180
|
ensure_head_snapshot!(storage_name, 1)
|
|
181
|
+
# `WHERE operation = 'save'` — era 1 has no ancestor tail to
|
|
182
|
+
# worry about, but the snapshot table can now hold delete
|
|
183
|
+
# tombstones too (H3, see `ensure_head_snapshot!`'s own
|
|
184
|
+
# comment), and a tombstone's `state` is NULL: without this
|
|
185
|
+
# filter a deleted id would still resolve here, just to a nil
|
|
186
|
+
# state, instead of correctly falling out of the head at all.
|
|
152
187
|
@db.exec(<<~SQL)
|
|
153
188
|
CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
|
|
154
|
-
SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))}
|
|
189
|
+
SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))} WHERE operation = 'save'
|
|
155
190
|
SQL
|
|
156
191
|
end
|
|
157
192
|
|
|
@@ -359,13 +394,27 @@ module Hecks
|
|
|
359
394
|
# under this era specifically.
|
|
360
395
|
ensure_head_snapshot!(storage_name, era)
|
|
361
396
|
@db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
|
|
397
|
+
# THE CURRENT-ERA SIDE READS ITS OWN `operation` COLUMN NOW —
|
|
398
|
+
# H3 (docs/audits/2026-08-10-main-bug-audit.md). This used to
|
|
399
|
+
# hardcode `'save' AS operation` here, on the reasoning that a
|
|
400
|
+
# delete simply removed its row from the snapshot table. But
|
|
401
|
+
# for an id carried in from an ancestor era, that left the
|
|
402
|
+
# ancestor matview's own `save` row as the ONLY row on either
|
|
403
|
+
# side of this union — which then won `DISTINCT ON` and
|
|
404
|
+
# `WHERE operation = 'save'` let it straight through, so a
|
|
405
|
+
# deleted ancestor-carried record kept resurrecting forever.
|
|
406
|
+
# The snapshot side now upserts a real tombstone row on
|
|
407
|
+
# delete (`ensure_head_snapshot!`'s own comment) instead of
|
|
408
|
+
# deleting the row, so reading its actual `operation` here
|
|
409
|
+
# lets that tombstone outrank the stale ancestor `save` row
|
|
410
|
+
# by ordinal, exactly the way a genuine re-save already did.
|
|
362
411
|
@db.exec(<<~SQL)
|
|
363
412
|
CREATE VIEW #{quote(head_view(storage_name))} AS
|
|
364
413
|
SELECT id, state FROM (
|
|
365
414
|
SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, operation, state FROM (
|
|
366
415
|
SELECT ordinal, aggregate_id, operation, state FROM #{quote(view)}
|
|
367
416
|
UNION ALL
|
|
368
|
-
SELECT ordinal, id AS aggregate_id,
|
|
417
|
+
SELECT ordinal, id AS aggregate_id, operation, state
|
|
369
418
|
FROM #{quote(head_snapshot(storage_name, era))}
|
|
370
419
|
) merged ORDER BY aggregate_id, ordinal DESC
|
|
371
420
|
) latest WHERE operation = 'save'
|
|
@@ -78,7 +78,35 @@ module Hecks
|
|
|
78
78
|
# to revoke them from the owner itself); a deployment's app
|
|
79
79
|
# role connects as a NON-owner and gets exactly INSERT, per
|
|
80
80
|
# era, at mint time.
|
|
81
|
-
|
|
81
|
+
#
|
|
82
|
+
# GUARDED, same reasoning as the RLS ALTER TABLE calls just
|
|
83
|
+
# below: REVOKE still writes pg_class.relacl (and takes the
|
|
84
|
+
# matching lock) even when the resulting privileges are
|
|
85
|
+
# unchanged, so an unconditional reissue on every ordinary
|
|
86
|
+
# reboot raced two concurrent boots into
|
|
87
|
+
# `PG::InternalError: tuple concurrently updated` — read
|
|
88
|
+
# first, touch the catalog only on the boot that actually
|
|
89
|
+
# needs to.
|
|
90
|
+
#
|
|
91
|
+
# `relacl IS NULL`, not `has_table_privilege('public', ...,
|
|
92
|
+
# 'UPDATE')` — found live, not assumed: a brand-new table's
|
|
93
|
+
# PUBLIC privilege is already "no UPDATE" by Postgres's own
|
|
94
|
+
# default (nothing has ever been explicitly granted to
|
|
95
|
+
# PUBLIC), so `has_table_privilege` answers false BOTH before
|
|
96
|
+
# the REVOKE has ever run AND after it has — indistinguishable
|
|
97
|
+
# by that check alone, which meant the very first boot's own
|
|
98
|
+
# REVOKE never actually ran, `relacl` stayed NULL forever, and
|
|
99
|
+
# "journal rows accept no UPDATE/DELETE from PUBLIC" was true
|
|
100
|
+
# only by accident of Postgres's default, not by the explicit
|
|
101
|
+
# privilege revocation this method exists to record.
|
|
102
|
+
# `relacl IS NULL` means "default ACL, nothing explicit yet" —
|
|
103
|
+
# exactly the one-time signal needed, and (like the RLS flags
|
|
104
|
+
# below) `pg_table_is_visible(oid)`, not a bare relname match,
|
|
105
|
+
# for the same shared-instance/storehouse reason.
|
|
106
|
+
relacl_null = @db.exec_params(
|
|
107
|
+
"SELECT relacl IS NULL FROM pg_class WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
|
|
108
|
+
).getvalue(0, 0)
|
|
109
|
+
@db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") if relacl_null == "t"
|
|
82
110
|
# RLS goes on AT PROVISIONING, never mid-life — enabling it
|
|
83
111
|
# later would deny every role that has no policy yet, on
|
|
84
112
|
# whatever the shape of the schema happened to be at that
|
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/registry"
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Adapters
|
|
@@ -74,6 +74,7 @@ module Hecks
|
|
|
74
74
|
compile_head!(aggregate, era, label, edges, full: true)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
77
78
|
winners.each do |id, side|
|
|
78
79
|
aggregates.each do |aggregate|
|
|
79
80
|
state =
|
|
@@ -101,10 +102,16 @@ module Hecks
|
|
|
101
102
|
"VALUES ($1, $2, $3, 'save', $4) RETURNING ordinal",
|
|
102
103
|
[era, aggregate.storage_name, id, state]
|
|
103
104
|
)[0]["ordinal"]
|
|
105
|
+
# Always `'save'` — a merge winner is read from either
|
|
106
|
+
# the matview's own `operation = 'save'` rows or the new
|
|
107
|
+
# world's live head (`new_states`, captured from
|
|
108
|
+
# `head_view`, which is save-only by construction), so
|
|
109
|
+
# nothing reaching this INSERT is ever a delete.
|
|
104
110
|
@db.exec_params(
|
|
105
|
-
"INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal,
|
|
106
|
-
"
|
|
107
|
-
"
|
|
111
|
+
"INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal, operation, state) " \
|
|
112
|
+
"VALUES ($1, $2, 'save', $3) " \
|
|
113
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
114
|
+
"state = EXCLUDED.state WHERE #{quote(head_snapshot(aggregate.storage_name, era))}.ordinal < EXCLUDED.ordinal",
|
|
108
115
|
[id, ordinal, state]
|
|
109
116
|
)
|
|
110
117
|
end
|
|
@@ -7,7 +7,27 @@ module Hecks
|
|
|
7
7
|
# equal to the port's reference entry-JSON transform by the
|
|
8
8
|
# cross-execution equivalence spec; the SQL here is a compilation
|
|
9
9
|
# target, not a second source of truth.
|
|
10
|
+
#
|
|
11
|
+
# LOCKED, unlike every other statement `ensure_base!` runs — those
|
|
12
|
+
# are all `CREATE ... IF NOT EXISTS`/`ADD COLUMN IF NOT EXISTS`,
|
|
13
|
+
# which Postgres itself resolves safely under concurrent boots.
|
|
14
|
+
# `CREATE OR REPLACE FUNCTION` is not: it always rewrites the
|
|
15
|
+
# `pg_proc` row, so two sessions racing to (re)install the SAME
|
|
16
|
+
# function — these six are shared/global, not per-domain, so any
|
|
17
|
+
# two domains' concurrent first-boots can collide here — hit a
|
|
18
|
+
# real `PG::InternalError: tuple concurrently updated`, not a
|
|
19
|
+
# graceful no-op. `nested_transaction` is the same
|
|
20
|
+
# already-open-transaction-safe wrapper `ensure_field_cache!`
|
|
21
|
+
# uses for its own advisory lock; a fixed, domain-independent key
|
|
22
|
+
# is correct since these functions have no domain of their own.
|
|
10
23
|
def install_transforms!
|
|
24
|
+
nested_transaction("hecks_tr_functions") do
|
|
25
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_tr_functions'))", [])
|
|
26
|
+
install_transform_functions!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_transform_functions!
|
|
11
31
|
@db.exec(<<~SQL)
|
|
12
32
|
CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
|
|
13
33
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -9,7 +9,7 @@ require_relative "lineage/resumable_backfill"
|
|
|
9
9
|
require_relative "lineage/head_compiler"
|
|
10
10
|
require_relative "lineage/field_cache"
|
|
11
11
|
require_relative "lineage/transform_installer"
|
|
12
|
-
require_relative "
|
|
12
|
+
require_relative "../../../../../naming"
|
|
13
13
|
|
|
14
14
|
module Hecks
|
|
15
15
|
module Adapters
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
1
|
+
require_relative "../../lineage"
|
|
2
|
+
require_relative "../../era_guard"
|
|
3
|
+
require_relative "../../../../../../runtime/identity"
|
|
4
|
+
require_relative "../../../../../../runtime/registry"
|
|
5
|
+
require_relative "../../translation/audit"
|
|
6
6
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Adapters
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative "../lineage"
|
|
2
|
-
require_relative "
|
|
2
|
+
require_relative "../../storage_shape"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
@@ -14,7 +14,10 @@ module Hecks
|
|
|
14
14
|
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
15
|
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
16
16
|
lineage.ensure_base!
|
|
17
|
-
|
|
17
|
+
# Both spellings honored, key? first — never `||`, which cannot
|
|
18
|
+
# tell a genuinely stored `false` apart from an absent key (see
|
|
19
|
+
# PostgresEra.setting's own comment for the full reasoning).
|
|
20
|
+
role = settings.key?(:role) ? settings[:role] : settings["role"]
|
|
18
21
|
|
|
19
22
|
held = lineage.eras
|
|
20
23
|
if held.empty?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require_relative "../lineage"
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
2
|
+
require_relative "../../../../../../runtime/registry"
|
|
3
|
+
require_relative "../../translation/audit"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/registry"
|
|
2
|
+
require_relative "../../storage_shape"
|
|
3
|
+
require_relative "../../translation/audit"
|
|
4
|
+
require_relative "../../translation/scaffold"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Adapters
|
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb
RENAMED
|
@@ -4,8 +4,8 @@ require_relative "lineage_manager/era_resolver"
|
|
|
4
4
|
require_relative "lineage_manager/minter"
|
|
5
5
|
require_relative "lineage_manager/merge_coordinator"
|
|
6
6
|
require_relative "lineage_manager/coverage_check"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
7
|
+
require_relative "../era_guard"
|
|
8
|
+
require_relative "../../../../../runtime/registry"
|
|
9
9
|
|
|
10
10
|
module Hecks
|
|
11
11
|
module Adapters
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
|
-
require_relative "sql_query_builder"
|
|
3
|
+
require_relative "../../../../adapters/driven/sql_query_builder"
|
|
4
4
|
require_relative "postgres_era/lineage"
|
|
5
5
|
require_relative "postgres_era/lineage_manager"
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "
|
|
6
|
+
require_relative "../../../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../../../query_specification/common/order_by"
|
|
8
|
+
require_relative "../../../../runtime/errors"
|
|
9
|
+
require_relative "../../../../runtime/event"
|
|
10
|
+
require_relative "../../../../runtime/instance"
|
|
11
|
+
require_relative "../../../../runtime/registry"
|
|
12
12
|
|
|
13
13
|
module Hecks
|
|
14
14
|
module Adapters
|
|
@@ -67,12 +67,30 @@ module Hecks
|
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# BOTH SPELLINGS OF A SETTING ARE HONORED — a world's settings hash
|
|
71
|
+
# may arrive symbol-keyed (built straight in Ruby) or string-keyed
|
|
72
|
+
# (round-tripped through JSON), and a domain that names one is not
|
|
73
|
+
# obligated to also skip the other. `key?` decides which spelling
|
|
74
|
+
# actually exists, never `||` — `||` cannot tell a genuinely stored
|
|
75
|
+
# `false` apart from an absent key, and would silently prefer the
|
|
76
|
+
# OTHER spelling (or `default`) instead of returning the real, held
|
|
77
|
+
# answer. See Hecks::QuerySpecification::FieldPath#read for the same
|
|
78
|
+
# discipline applied to stored state instead of settings.
|
|
79
|
+
def self.setting(settings, key, default: nil)
|
|
80
|
+
return settings[key] if settings.key?(key)
|
|
81
|
+
|
|
82
|
+
str_key = key.to_s
|
|
83
|
+
return settings[str_key] if settings.key?(str_key)
|
|
84
|
+
|
|
85
|
+
default
|
|
86
|
+
end
|
|
87
|
+
|
|
70
88
|
def self.connect_for(name, settings)
|
|
71
89
|
# LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize:
|
|
72
90
|
# a domain that never wires PostgresEra should never need the gem.
|
|
73
91
|
require "pg"
|
|
74
92
|
|
|
75
|
-
declared = settings
|
|
93
|
+
declared = setting(settings, :database)
|
|
76
94
|
if declared.to_s.empty?
|
|
77
95
|
raise Runtime::WiringError,
|
|
78
96
|
"#{name} binds PostgresEra, which needs a database connection, " \
|
|
@@ -94,7 +112,7 @@ module Hecks
|
|
|
94
112
|
# TABLE ... SET SCHEMA migrations transparent to the rest of the
|
|
95
113
|
# adapter. A domain with no `schema` setting keeps Postgres's
|
|
96
114
|
# own default search_path (public), same as before this existed.
|
|
97
|
-
schema = settings
|
|
115
|
+
schema = setting(settings, :schema)
|
|
98
116
|
if schema.to_s != ""
|
|
99
117
|
# THE SCHEMA ITSELF, IDEMPOTENTLY — a domain naming a `schema:`
|
|
100
118
|
# nobody has created yet used to fail on its FIRST table-
|
|
@@ -132,14 +150,29 @@ module Hecks
|
|
|
132
150
|
# The domain names the journal (one journal per lineage). The
|
|
133
151
|
# factory injects it; a directly-instantiated adapter (specs,
|
|
134
152
|
# consoles) journals under the aggregate's own name.
|
|
135
|
-
@domain = (settings
|
|
153
|
+
@domain = self.class.setting(settings, :domain, default: aggregate.storage_name).to_s
|
|
136
154
|
@lineage = Lineage.new(@db, @domain)
|
|
137
155
|
@lineage.ensure_base!
|
|
138
156
|
# The era gate resolves which era this boot IS (an old checkout
|
|
139
157
|
# boots a held-but-superseded era and keeps writing its own
|
|
140
158
|
# partition); a directly-instantiated adapter defaults to the
|
|
141
159
|
# newest.
|
|
142
|
-
|
|
160
|
+
#
|
|
161
|
+
# NOT `self.class.setting(...)` here — RepositoryFactory#build
|
|
162
|
+
# always merges `era: registry.resolved_eras[domain]` into
|
|
163
|
+
# settings, so the key is genuinely PRESENT (not absent) for
|
|
164
|
+
# any domain the era boot gate hasn't resolved yet (or that
|
|
165
|
+
# doesn't have one at all), just holding `nil`. `setting`'s own
|
|
166
|
+
# presence-over-truthiness discipline (correct for a field like
|
|
167
|
+
# `:role`, where a stored `false` is a real, distinct answer
|
|
168
|
+
# from "unset") does the wrong thing for `:era` specifically:
|
|
169
|
+
# `nil` can never be a meaningful era override — only a real
|
|
170
|
+
# ordinal or "not resolved, self-resolve" ever apply — so
|
|
171
|
+
# coalescing here, not `setting`'s presence check, is what
|
|
172
|
+
# actually honors this comment's own "defaults to the newest"
|
|
173
|
+
# promise.
|
|
174
|
+
@era = settings.key?(:era) ? settings[:era] : settings["era"]
|
|
175
|
+
@era ||= @lineage.current_era
|
|
143
176
|
# Unconditional and idempotent, regardless of era — belt-and-
|
|
144
177
|
# suspenders self-healing (compile_head! already ensures this for
|
|
145
178
|
# a freshly-minted era's own name; ensure_first_head! for era 1's)
|
|
@@ -309,8 +342,31 @@ module Hecks
|
|
|
309
342
|
end
|
|
310
343
|
end
|
|
311
344
|
|
|
345
|
+
# The journal carries FORCE ROW LEVEL SECURITY with exactly two
|
|
346
|
+
# policies — hecks_current_era's INSERT and hecks_read_all's
|
|
347
|
+
# SELECT (advance_era! above) — and no DELETE policy at all, for
|
|
348
|
+
# anyone. FORCE means even the table's own owner is fenced by
|
|
349
|
+
# that (only an actual Postgres superuser or a role granted
|
|
350
|
+
# BYPASSRLS sits above it — see lineage.rb's own header), so a
|
|
351
|
+
# plain `DELETE ... WHERE aggregate = $1` from an ordinary
|
|
352
|
+
# connection silently matches zero rows: no privilege error, no
|
|
353
|
+
# exception, just a no-op that looks like success. Counting
|
|
354
|
+
# before and comparing to what the DELETE itself reports is what
|
|
355
|
+
# tells "nothing to delete" apart from "RLS silently ate the
|
|
356
|
+
# delete" — the same row count, from the same statement, either
|
|
357
|
+
# way, with no separate query racing the DELETE for an answer.
|
|
312
358
|
def reset!
|
|
313
|
-
@db.exec_params(
|
|
359
|
+
before = @db.exec_params(
|
|
360
|
+
"SELECT count(*) FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table]
|
|
361
|
+
)[0]["count"].to_i
|
|
362
|
+
result = @db.exec_params("DELETE FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table])
|
|
363
|
+
if before.positive? && result.cmd_tuples.zero?
|
|
364
|
+
raise Runtime::WiringError,
|
|
365
|
+
"reset! deleted 0 of #{before} row(s) for #{table} in #{@lineage.quoted_journal} — " \
|
|
366
|
+
"FORCE ROW LEVEL SECURITY admits no DELETE policy on the journal, so this connection's " \
|
|
367
|
+
"DELETE silently matched nothing. reset! only works connected as an actual Postgres " \
|
|
368
|
+
"superuser or a role granted BYPASSRLS, not as the provisioner or an app role."
|
|
369
|
+
end
|
|
314
370
|
self
|
|
315
371
|
end
|
|
316
372
|
|
|
@@ -355,13 +411,14 @@ module Hecks
|
|
|
355
411
|
# `SagaInterpreter`'s own mutex (§7) serializing IN-PROCESS writers,
|
|
356
412
|
# and gets the SAME cross-process safety an aggregate's own writes
|
|
357
413
|
# get from this adapter — no better, no worse.
|
|
358
|
-
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
414
|
+
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
359
415
|
@db.exec_params(
|
|
360
|
-
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
|
|
361
|
-
"VALUES ($1, $2, $3, $4, $5) " \
|
|
416
|
+
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
|
|
417
|
+
"VALUES ($1, $2, $3, $4, $5, $6) " \
|
|
362
418
|
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
363
|
-
"SET state = EXCLUDED.state, memory = EXCLUDED.memory,
|
|
364
|
-
|
|
419
|
+
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
|
|
420
|
+
"completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
|
|
421
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
|
|
365
422
|
)
|
|
366
423
|
end
|
|
367
424
|
|
|
@@ -376,11 +433,12 @@ module Hecks
|
|
|
376
433
|
return enum_for(:each_saga) unless block_given?
|
|
377
434
|
|
|
378
435
|
@db.exec_params(
|
|
379
|
-
"SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
|
|
436
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1",
|
|
380
437
|
[@domain]
|
|
381
438
|
).each do |row|
|
|
382
439
|
yield row["process_manager"], row["correlation"], row["state"],
|
|
383
|
-
JSON.parse(row["memory"], symbolize_names: true)
|
|
440
|
+
JSON.parse(row["memory"], symbolize_names: true),
|
|
441
|
+
JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
|
|
384
442
|
end
|
|
385
443
|
end
|
|
386
444
|
|
|
@@ -404,9 +462,9 @@ module Hecks
|
|
|
404
462
|
|
|
405
463
|
if entry.save?
|
|
406
464
|
@db.exec_params(
|
|
407
|
-
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
408
|
-
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal,
|
|
409
|
-
"WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
465
|
+
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
|
|
466
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
467
|
+
"state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
410
468
|
[entry.id, ordinal, state_json]
|
|
411
469
|
)
|
|
412
470
|
# SAME TRANSACTION, SAME ORDINAL — every field cache stays
|
|
@@ -419,7 +477,30 @@ module Hecks
|
|
|
419
477
|
@lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
|
|
420
478
|
end
|
|
421
479
|
else
|
|
422
|
-
|
|
480
|
+
# A TOMBSTONE ROW, NOT A BARE DELETE — H3 (docs/audits/2026-08-
|
|
481
|
+
# 10-main-bug-audit.md). `DELETE FROM head_snapshot` used to be
|
|
482
|
+
# the whole story here, which is correct in isolation but wrong
|
|
483
|
+
# once an ancestor era is in the picture: for a record carried
|
|
484
|
+
# into this era from an ancestor, removing this era's row left
|
|
485
|
+
# NOTHING on the current-era side of `compile_head!`'s union to
|
|
486
|
+
# outrank the ancestor matview's own (still-present, still
|
|
487
|
+
# `save`) row, so `DISTINCT ON` picked the ancestor's row and
|
|
488
|
+
# the "deleted" record kept reading back forever. Upserting a
|
|
489
|
+
# tombstone (`operation = 'delete'`, `state` NULL) instead
|
|
490
|
+
# means this era always has ITS OWN newest-ordinal row for the
|
|
491
|
+
# id, exactly like a real re-save already did ("re-saves are
|
|
492
|
+
# masked correctly" — the audit's own phrasing for why that
|
|
493
|
+
# half of this was never broken) — it just carries `operation
|
|
494
|
+
# = 'delete'` instead of `'save'`, so `head_view`'s own `WHERE
|
|
495
|
+
# operation = 'save'` still correctly hides it. Ordinal-guarded
|
|
496
|
+
# the same as every other upsert here, so an out-of-order
|
|
497
|
+
# replay can never let a stale delete clobber a newer save.
|
|
498
|
+
@db.exec_params(
|
|
499
|
+
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, operation, state) VALUES ($1, $2, 'delete', NULL) " \
|
|
500
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
501
|
+
"state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
502
|
+
[entry.id, ordinal]
|
|
503
|
+
)
|
|
423
504
|
@field_caches.each_value { |cache_table| @lineage.delete_field_cache_row!(cache_table, entry.id) }
|
|
424
505
|
end
|
|
425
506
|
|
|
@@ -656,15 +737,21 @@ module Hecks
|
|
|
656
737
|
def create_saga_table!
|
|
657
738
|
@db.exec(<<~SQL)
|
|
658
739
|
CREATE TABLE IF NOT EXISTS hecks_saga_instances (
|
|
659
|
-
domain
|
|
660
|
-
process_manager
|
|
661
|
-
correlation
|
|
662
|
-
state
|
|
663
|
-
memory
|
|
664
|
-
|
|
740
|
+
domain text NOT NULL,
|
|
741
|
+
process_manager text NOT NULL,
|
|
742
|
+
correlation text NOT NULL,
|
|
743
|
+
state text NOT NULL,
|
|
744
|
+
memory jsonb NOT NULL,
|
|
745
|
+
completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
746
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
665
747
|
PRIMARY KEY (domain, process_manager, correlation)
|
|
666
748
|
)
|
|
667
749
|
SQL
|
|
750
|
+
# `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
|
|
751
|
+
# this same domain already created before this column existed —
|
|
752
|
+
# the same idiom `rust/host/src/journal.rs`'s own
|
|
753
|
+
# `sagas_backfilled` column addition already uses.
|
|
754
|
+
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb")
|
|
668
755
|
end
|
|
669
756
|
end
|
|
670
757
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/errors"
|
|
2
|
+
require_relative "../../../../../../runtime/instance"
|
|
3
|
+
require_relative "../../../../../../runtime/value/invariant_violation"
|
|
4
|
+
require_relative "../../../../../../bluebook/model_check"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module Translation
|
|
@@ -21,8 +22,16 @@ module Hecks
|
|
|
21
22
|
next unless lifecycle
|
|
22
23
|
|
|
23
24
|
held = instance[lifecycle.field]
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# `Lifecycle#states` answers default+targets only — a state
|
|
26
|
+
# legitimately declared just as a `from:` (a terminal
|
|
27
|
+
# transition's source, never anyone's target) is real and
|
|
28
|
+
# reachable but invisible to it. `ModelCheck.full_states`
|
|
29
|
+
# is the full declared set (default, every target, AND
|
|
30
|
+
# every from) that `fuzzing/properties.rb`'s own replay
|
|
31
|
+
# check already uses for this identical question — see its
|
|
32
|
+
# comment on this same hole.
|
|
33
|
+
allowed = Bluebook::ModelCheck.full_states(lifecycle)
|
|
34
|
+
unless held.nil? || allowed.include?(held.to_s)
|
|
26
35
|
violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
|
|
27
36
|
"a state this era's lifecycle never reaches"
|
|
28
37
|
end
|