hecks 1.0.2 → 1.2.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/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
- data/lib/hecks/adapters/driven/postgres.rb +45 -31
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +15 -4
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +51 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/glossary.rb +374 -0
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +78 -42
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +34 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +45 -9
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +271 -49
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +16 -2
|
@@ -2,6 +2,12 @@ module Hecks
|
|
|
2
2
|
module Adapters
|
|
3
3
|
class PostgresEra
|
|
4
4
|
class Lineage
|
|
5
|
+
# Owner-only schema bootstrap: creates the lineage tables
|
|
6
|
+
# (`hecks_eras`, `hecks_era_texts`, `hecks_approvals`, the
|
|
7
|
+
# partitioned journal), sets up its RLS fence, bridges a renamed
|
|
8
|
+
# domain's history (`rename_domain!`), and attaches each era's
|
|
9
|
+
# journal partition (`ensure_partition!`) — the build-then-ATTACH
|
|
10
|
+
# dance that keeps a mint from stopping every writer.
|
|
5
11
|
module Provisioning
|
|
6
12
|
# Provisioning is the OWNER's job, and a deployment's app role is
|
|
7
13
|
# deliberately not the owner — it may append and read, and it
|
|
@@ -13,6 +19,15 @@ module Hecks
|
|
|
13
19
|
# grant_era! exists to constrain could never finish booting
|
|
14
20
|
# ("must be owner of table hecks_eras"). A fence nothing can
|
|
15
21
|
# reach is not a fence.
|
|
22
|
+
# One bootstrap sequence of DDL, order-dependent throughout —
|
|
23
|
+
# every guarded ALTER (RLS, REVOKE) is guarded and placed
|
|
24
|
+
# exactly where it is for measured, documented reasons (see the
|
|
25
|
+
# inline comments on each: catalog-lock avoidance, RLS timing,
|
|
26
|
+
# FORCE semantics). Splitting this into smaller methods would
|
|
27
|
+
# only hide that single ordered sequence behind several call
|
|
28
|
+
# sites, with nothing gained — each statement already reads as
|
|
29
|
+
# one step, and the length here is DDL, not branching logic.
|
|
30
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
16
31
|
def ensure_base!
|
|
17
32
|
rename_domain! if @formerly_known_as
|
|
18
33
|
return unless provisioner?
|
|
@@ -183,6 +198,16 @@ module Hecks
|
|
|
183
198
|
# Anything else (no rows under either name) falls through
|
|
184
199
|
# harmlessly — `formerly_known_as` pointing at a name with no
|
|
185
200
|
# held history is not an error, just inert.
|
|
201
|
+
# One transactional migration, whose own comment above spells
|
|
202
|
+
# out a three-way precheck and a FIXED lock-acquisition order
|
|
203
|
+
# (old before new, eras before ordinal) chosen specifically to
|
|
204
|
+
# avoid a deadlock against a concurrent rename/mint/write.
|
|
205
|
+
# Splitting this would separate the precheck from the locking
|
|
206
|
+
# from the renames from the commit/rollback handling — each a
|
|
207
|
+
# piece of ONE transaction that must stay in this exact order
|
|
208
|
+
# or the deadlock-avoidance guarantee stops holding.
|
|
209
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
210
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
186
211
|
def rename_domain!
|
|
187
212
|
return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"]
|
|
188
213
|
return if @db.exec_params(
|
|
@@ -236,17 +261,30 @@ module Hecks
|
|
|
236
261
|
# ensure_base! at all — only lazily, on first reattest! — so a
|
|
237
262
|
# domain that never needed one must not be forced through an
|
|
238
263
|
# UPDATE against a table that doesn't exist.
|
|
239
|
-
|
|
264
|
+
if @db.exec_params(
|
|
265
|
+
"SELECT to_regclass($1)", ["hecks_attestations"]
|
|
266
|
+
)[0]["to_regclass"]
|
|
267
|
+
@db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2",
|
|
268
|
+
[@domain, @formerly_known_as])
|
|
269
|
+
end
|
|
240
270
|
|
|
241
271
|
@db.exec("COMMIT")
|
|
242
272
|
rescue PG::LockNotAvailable
|
|
243
|
-
|
|
273
|
+
begin
|
|
274
|
+
@db.exec("ROLLBACK")
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
244
278
|
raise Runtime::WiringError,
|
|
245
279
|
"cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \
|
|
246
280
|
"one of the domain locks — waited 10s; try again shortly"
|
|
247
|
-
rescue PG::Error =>
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
rescue PG::Error => e
|
|
282
|
+
begin
|
|
283
|
+
@db.exec("ROLLBACK")
|
|
284
|
+
rescue StandardError
|
|
285
|
+
nil
|
|
286
|
+
end
|
|
287
|
+
raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
|
|
250
288
|
end
|
|
251
289
|
|
|
252
290
|
# Nothing provisioned yet — build it. Provisioned and owned —
|
|
@@ -4,6 +4,10 @@ module Hecks
|
|
|
4
4
|
module Adapters
|
|
5
5
|
class PostgresEra
|
|
6
6
|
class Lineage
|
|
7
|
+
# The one deliberate, human-driven command that closes a fork:
|
|
8
|
+
# `merge_tail!` re-enters an old era's post-cut writes into the
|
|
9
|
+
# current era under named winners, and `diverged_count` observes how
|
|
10
|
+
# far the two worlds have drifted at any time before that.
|
|
7
11
|
module TailMerge
|
|
8
12
|
# ── fork observability ─────────────────────────────────────────
|
|
9
13
|
|
|
@@ -29,6 +33,14 @@ module Hecks
|
|
|
29
33
|
# winner; resolution itself is append-only (the winner's state
|
|
30
34
|
# re-enters as the newest row and wins structurally — originals
|
|
31
35
|
# stay immutable).
|
|
36
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
37
|
+
# One Postgres transaction (BEGIN…COMMIT) with a manual ROLLBACK
|
|
38
|
+
# at every early refusal, and a snapshot-before-mutation
|
|
39
|
+
# invariant (`new_states` captured before the head rebuild lets
|
|
40
|
+
# the tail interleave). Splitting the steps into separate
|
|
41
|
+
# methods would force each to independently know how to roll
|
|
42
|
+
# back the same shared transaction, and would separate the
|
|
43
|
+
# snapshot from the mutation it must precede.
|
|
32
44
|
def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
|
|
33
45
|
@db.exec("BEGIN")
|
|
34
46
|
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
@@ -129,14 +141,23 @@ module Hecks
|
|
|
129
141
|
@db.exec("COMMIT")
|
|
130
142
|
true
|
|
131
143
|
rescue PG::LockNotAvailable
|
|
132
|
-
|
|
144
|
+
begin
|
|
145
|
+
@db.exec("ROLLBACK")
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
133
149
|
raise Runtime::WiringError,
|
|
134
150
|
"cannot merge the tail of #{@domain}: another mint or merge holds the domain lock — " \
|
|
135
151
|
"waited 10s; try again shortly"
|
|
136
|
-
rescue PG::Error =>
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
rescue PG::Error => e
|
|
153
|
+
begin
|
|
154
|
+
@db.exec("ROLLBACK")
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{e.message.strip}"
|
|
139
159
|
end
|
|
160
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
140
161
|
|
|
141
162
|
# Ids touched by BOTH worlds since the cut — the old world's
|
|
142
163
|
# post-cut tail INTERSECTed with the new world's own writes.
|
|
@@ -2,6 +2,12 @@ module Hecks
|
|
|
2
2
|
module Adapters
|
|
3
3
|
class PostgresEra
|
|
4
4
|
class Lineage
|
|
5
|
+
# Installs the shared, domain-independent `hecks_tr_*` Postgres
|
|
6
|
+
# functions (extract/insert/rename/move/convert/drop over jsonb)
|
|
7
|
+
# that a compiled translation rule set's SQL calls into — the SQL
|
|
8
|
+
# compilation target kept equal to the Ruby reference transform by
|
|
9
|
+
# the cross-execution equivalence spec, never a second source of
|
|
10
|
+
# truth.
|
|
5
11
|
module TransformInstaller
|
|
6
12
|
# The jsonb rule transforms — installed once, idempotently. Kept
|
|
7
13
|
# equal to the port's reference entry-JSON transform by the
|
|
@@ -27,7 +33,26 @@ module Hecks
|
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
36
|
+
# Six independent CREATE OR REPLACE FUNCTION statements — each
|
|
37
|
+
# self-contained SQL, no shared Ruby state, and (per this
|
|
38
|
+
# module's own header comment) safe in any install order since
|
|
39
|
+
# plpgsql bodies aren't resolved against each other until
|
|
40
|
+
# called, not at CREATE time. Split one-per-function below
|
|
41
|
+
# purely so each has its own name and (where relevant) its own
|
|
42
|
+
# comment to sit next to, not because the six have any
|
|
43
|
+
# sequencing dependency on one another.
|
|
30
44
|
def install_transform_functions!
|
|
45
|
+
install_hecks_tr_extract!
|
|
46
|
+
install_hecks_tr_insert!
|
|
47
|
+
install_hecks_tr_rename!
|
|
48
|
+
install_hecks_tr_move!
|
|
49
|
+
install_hecks_tr_convert!
|
|
50
|
+
install_hecks_tr_drop!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def install_hecks_tr_extract!
|
|
31
56
|
@db.exec(<<~SQL)
|
|
32
57
|
CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
|
|
33
58
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -57,15 +82,18 @@ module Hecks
|
|
|
57
82
|
END IF;
|
|
58
83
|
END $fn$
|
|
59
84
|
SQL
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ADVERSARIAL FINDING: a destination whose top segment already
|
|
88
|
+
# holds a value — most commonly a reference, a bare scalar id
|
|
89
|
+
# — used to be silently overwritten with an empty object the
|
|
90
|
+
# moment a dotted destination needed to nest under it. That is
|
|
91
|
+
# a drop that never declared itself, the one thing this
|
|
92
|
+
# language exists to make explicit (see hecks_tr_convert's own
|
|
93
|
+
# refusal below, the same shape) — refused here instead, with
|
|
94
|
+
# the Ruby reference transform (ports/persistence/lineage.rb's
|
|
95
|
+
# `insert`) raising the identical wording.
|
|
96
|
+
def install_hecks_tr_insert!
|
|
69
97
|
@db.exec(<<~SQL)
|
|
70
98
|
CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
|
|
71
99
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -83,6 +111,9 @@ module Hecks
|
|
|
83
111
|
RETURN jsonb_set(state, path, value);
|
|
84
112
|
END $fn$
|
|
85
113
|
SQL
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def install_hecks_tr_rename!
|
|
86
117
|
@db.exec(<<~SQL)
|
|
87
118
|
CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
|
|
88
119
|
LANGUAGE sql IMMUTABLE AS $fn$
|
|
@@ -91,6 +122,9 @@ module Hecks
|
|
|
91
122
|
ELSE state END
|
|
92
123
|
$fn$
|
|
93
124
|
SQL
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def install_hecks_tr_move!
|
|
94
128
|
@db.exec(<<~SQL)
|
|
95
129
|
CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
|
|
96
130
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -101,6 +135,9 @@ module Hecks
|
|
|
101
135
|
RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
|
|
102
136
|
END $fn$
|
|
103
137
|
SQL
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def install_hecks_tr_convert!
|
|
104
141
|
@db.exec(<<~SQL)
|
|
105
142
|
CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
|
|
106
143
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -117,6 +154,9 @@ module Hecks
|
|
|
117
154
|
from_label, extracted.value, extracted.value;
|
|
118
155
|
END $fn$
|
|
119
156
|
SQL
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def install_hecks_tr_drop!
|
|
120
160
|
@db.exec(<<~SQL)
|
|
121
161
|
CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
|
|
122
162
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -66,8 +66,8 @@ module Hecks
|
|
|
66
66
|
declared = edge.for_aggregate(aggregate.name)
|
|
67
67
|
after = begin
|
|
68
68
|
lineage.translated_latest(aggregate, ordinal, chain)
|
|
69
|
-
rescue PG::Error =>
|
|
70
|
-
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{
|
|
69
|
+
rescue PG::Error => e
|
|
70
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{e.message.strip}"
|
|
71
71
|
end
|
|
72
72
|
before = if chain.size > 1
|
|
73
73
|
lineage.translated_latest(aggregate, ordinal, chain[0..-2])
|
|
@@ -10,6 +10,15 @@ module Hecks
|
|
|
10
10
|
# held-but-superseded shape boots read-only-toward-the-fence; an
|
|
11
11
|
# unheld shape goes to the minter.
|
|
12
12
|
module EraResolver
|
|
13
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
14
|
+
# A single order-dependent dispatch over a closed set of four
|
|
15
|
+
# boot scenarios (first boot / quiet reboot / held-but-superseded
|
|
16
|
+
# / unheld-shape-goes-to-minter), each already explained by its
|
|
17
|
+
# own comment above, all sharing one `db` connection closed in
|
|
18
|
+
# `ensure`. Splitting the branches into separate methods would
|
|
19
|
+
# turn each `return` (which exits `check!`, closing `db`) into a
|
|
20
|
+
# sentinel value threaded back up, obscuring the mutual
|
|
21
|
+
# exclusivity that IS the method's whole point.
|
|
13
22
|
def check!(registry:, bluebook:, current_text:, settings:, directory: nil)
|
|
14
23
|
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
24
|
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
@@ -78,6 +87,7 @@ module Hecks
|
|
|
78
87
|
ensure
|
|
79
88
|
db&.close
|
|
80
89
|
end
|
|
90
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
end
|
|
@@ -20,10 +20,29 @@ module Hecks
|
|
|
20
20
|
label = hash[0, Runtime::StorageShape::LABEL_LENGTH]
|
|
21
21
|
ordinal = latest[:ordinal] + 1
|
|
22
22
|
|
|
23
|
+
edge = resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
|
|
24
|
+
ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
|
|
25
|
+
check_coverage!(registry, bluebook, shadow(latest[:held_text]), edge)
|
|
26
|
+
|
|
27
|
+
chain = edge_chain(registry, bluebook, lineage.eras, label)
|
|
28
|
+
audit!(bluebook, lineage, chain, ordinal, edge)
|
|
29
|
+
lineage.mint_era!(
|
|
30
|
+
ordinal: ordinal, hash: hash, label: label, held_text: current_text,
|
|
31
|
+
aggregates: bluebook.aggregates, edges: chain, role: role,
|
|
32
|
+
projection: Runtime::StorageShape.project(bluebook)
|
|
33
|
+
)
|
|
34
|
+
ordinal
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Find the one translation edge that leaves the held era, and
|
|
38
|
+
# validate it: exactly one edge must leave (eras fork
|
|
39
|
+
# mechanically — a second edge from the same source is a wiring
|
|
40
|
+
# mistake, not a merge to resolve automatically), and it must
|
|
41
|
+
# target the current shape's label (otherwise the edge is stale
|
|
42
|
+
# and boot must not silently mint past it).
|
|
43
|
+
def resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
|
|
23
44
|
edges = registry.translations.select { |t| t.domain == bluebook.name && t.from == latest[:label] }
|
|
24
|
-
if edges.empty?
|
|
25
|
-
refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory)
|
|
26
|
-
end
|
|
45
|
+
refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory) if edges.empty?
|
|
27
46
|
if edges.size > 1
|
|
28
47
|
raise Runtime::WiringError,
|
|
29
48
|
"cannot boot #{bluebook.name}: #{edges.size} translation edges leave era #{latest[:ordinal]} " \
|
|
@@ -36,43 +55,35 @@ module Hecks
|
|
|
36
55
|
"cannot boot #{bluebook.name}: the translation edge from #{latest[:label]} targets " \
|
|
37
56
|
"#{edge.to}, but the current shape is #{label} — the edge is stale; re-run bin/scaffold_translation"
|
|
38
57
|
end
|
|
58
|
+
edge
|
|
59
|
+
end
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
|
|
52
|
-
raise Runtime::WiringError,
|
|
53
|
-
"cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
|
|
54
|
-
"rule, and the audit's human-approved sample is its only verification — run " \
|
|
55
|
-
"bin/translation_audit with --approve, then boot again"
|
|
56
|
-
end
|
|
57
|
-
tip = lineage.last_ordinal
|
|
58
|
-
if approval[:reviewed_ordinal] != tip
|
|
59
|
-
raise Runtime::WiringError,
|
|
60
|
-
"cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
|
|
61
|
-
"review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
|
|
62
|
-
"human approved no longer cover the data; re-run bin/translation_audit with --approve"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
61
|
+
# A compute's (and, the same way, a rekey's) only verification
|
|
62
|
+
# is the audit's human-approved sample — mint stays
|
|
63
|
+
# non-interactive by requiring the approval to already exist,
|
|
64
|
+
# recorded IN THIS DATABASE by `bin/translation_audit …
|
|
65
|
+
# --approve` and bound to what was actually reviewed: this
|
|
66
|
+
# edge's parsed content, and the journal as it stood when the
|
|
67
|
+
# samples were read. A journal that has advanced past the
|
|
68
|
+
# review invalidates it — the approved samples no longer cover
|
|
69
|
+
# the data.
|
|
70
|
+
def ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
|
|
71
|
+
return unless edge.aggregates.any? { |declared| !declared.computes.empty? || !declared.rekeys.empty? }
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
approval = lineage.approval_for(from: edge.from, to: edge.to)
|
|
74
|
+
unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
|
|
75
|
+
raise Runtime::WiringError,
|
|
76
|
+
"cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
|
|
77
|
+
"rule, and the audit's human-approved sample is its only verification — run " \
|
|
78
|
+
"bin/translation_audit with --approve, then boot again"
|
|
79
|
+
end
|
|
80
|
+
tip = lineage.last_ordinal
|
|
81
|
+
return unless approval[:reviewed_ordinal] != tip
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
aggregates: bluebook.aggregates, edges: chain, role: role,
|
|
73
|
-
projection: Runtime::StorageShape.project(bluebook)
|
|
74
|
-
)
|
|
75
|
-
ordinal
|
|
83
|
+
raise Runtime::WiringError,
|
|
84
|
+
"cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
|
|
85
|
+
"review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
|
|
86
|
+
"human approved no longer cover the data; re-run bin/translation_audit with --approve"
|
|
76
87
|
end
|
|
77
88
|
|
|
78
89
|
# No edge yet: the boot refuses toward the authoring loop —
|
|
@@ -96,7 +107,7 @@ module Hecks
|
|
|
96
107
|
# Diff the held era against the current shape and write the edge
|
|
97
108
|
# file — confident rules inline, ambiguities as parse-refusing
|
|
98
109
|
# `unresolved` lines. Returns the file path.
|
|
99
|
-
def scaffold!(
|
|
110
|
+
def scaffold!(_registry, bluebook, lineage, latest, directory)
|
|
100
111
|
ensure_named!(lineage, latest)
|
|
101
112
|
latest = lineage.eras.last
|
|
102
113
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
require_relative "../../../../adapters/driven/sql_query_builder"
|
|
4
|
+
require_relative "../../../../adapters/driven/postgres/outbox"
|
|
5
|
+
require_relative "../../../../adapters/driven/postgres/reconnect"
|
|
4
6
|
require_relative "postgres_era/lineage"
|
|
5
7
|
require_relative "postgres_era/lineage_manager"
|
|
6
8
|
require_relative "../../../../ports/persistence/append_only"
|
|
@@ -40,10 +42,20 @@ module Hecks
|
|
|
40
42
|
# operator compiles fully into SQL, or the query refuses loudly.
|
|
41
43
|
class PostgresEra
|
|
42
44
|
include SqlQueryBuilder
|
|
45
|
+
include Adapters::PostgresOutbox
|
|
46
|
+
include Adapters::PostgresReconnect
|
|
43
47
|
|
|
44
48
|
attr_reader :aggregate
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
# `:cross_process_lock` tells `Interpreting#run_dispatch_order_with_isolation`
|
|
51
|
+
# (runtime/interpreting.rb) this repository can hold a REAL
|
|
52
|
+
# cross-process lock for the whole dispatch order itself, via
|
|
53
|
+
# `with_write_lock` below — so it should use that instead of the
|
|
54
|
+
# in-process `AggregateLock` `Mutex` every other non-CAS repository
|
|
55
|
+
# falls back to. See ADR 0036: that in-process Mutex is invisible
|
|
56
|
+
# to `rust/host` dispatching against the same PostgresEra-bound
|
|
57
|
+
# tables from a separate OS process.
|
|
58
|
+
def persistence_capabilities = %i[atomic_put cross_process_lock]
|
|
47
59
|
|
|
48
60
|
# The capability idiom: only PostgresEra answers true, and only
|
|
49
61
|
# PostgresEra carries an era_check! for the boot gate to delegate to.
|
|
@@ -139,18 +151,42 @@ module Hecks
|
|
|
139
151
|
# problems) still surface.
|
|
140
152
|
connection.exec("SET client_min_messages = warning")
|
|
141
153
|
connection
|
|
142
|
-
rescue PG::Error =>
|
|
154
|
+
rescue PG::Error => e
|
|
143
155
|
raise Runtime::WiringError,
|
|
144
|
-
"cannot bind PostgresEra at #{declared} for #{name}: #{
|
|
156
|
+
"cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
|
|
145
157
|
end
|
|
146
158
|
|
|
147
159
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
148
160
|
@aggregate = aggregate
|
|
161
|
+
@settings = settings
|
|
149
162
|
@db = self.class.connect_for(aggregate.name, settings)
|
|
150
163
|
# The domain names the journal (one journal per lineage). The
|
|
151
|
-
# factory injects it
|
|
152
|
-
#
|
|
153
|
-
|
|
164
|
+
# factory injects it as the owning bluebook's own declared name
|
|
165
|
+
# (`RepositoryFactory.build`'s own `domain` positional arg —
|
|
166
|
+
# `bluebook.name`, confirmed at `outbox.rb`'s own call site —
|
|
167
|
+
# never this default in real dispatch). This default exists only
|
|
168
|
+
# for a directly-instantiated adapter (specs, consoles) that
|
|
169
|
+
# skips the factory.
|
|
170
|
+
#
|
|
171
|
+
# When the aggregate DOES have an owning chapter (`hecks_owner`
|
|
172
|
+
# set — true for any aggregate sealed through a real bluebook,
|
|
173
|
+
# even if this adapter itself was built by hand), default to the
|
|
174
|
+
# chapter's own declared PascalCase name. That's the same string
|
|
175
|
+
# `rust/host`'s `pg_advisory_xact_lock` key derives from
|
|
176
|
+
# (`HECKS_DOMAIN`, deploy-time-set to `world.domain`) — the
|
|
177
|
+
# aggregate's own snake_case `storage_name` silently disagreed
|
|
178
|
+
# with it (`hashtext` is byte-sensitive: `"chess"` and `"Chess"`
|
|
179
|
+
# never collide), which is ADR 0036 Blocker 1.
|
|
180
|
+
#
|
|
181
|
+
# When there's no owning chapter at all (a bare
|
|
182
|
+
# `AggregateBuilder` fixture built outside any real bluebook —
|
|
183
|
+
# `hecks_owner` is only ever stamped by chapter construction, see
|
|
184
|
+
# `traits.rb`'s `hecks_owner = self`) there is no chapter name to
|
|
185
|
+
# match Rust against in the first place, so fall back to the
|
|
186
|
+
# aggregate's own name, same as before this ADR.
|
|
187
|
+
@domain = self.class.setting(
|
|
188
|
+
settings, :domain, default: aggregate.hecks_owner&.name || aggregate.storage_name
|
|
189
|
+
).to_s
|
|
154
190
|
@lineage = Lineage.new(@db, @domain)
|
|
155
191
|
@lineage.ensure_base!
|
|
156
192
|
# The era gate resolves which era this boot IS (an old checkout
|
|
@@ -192,6 +228,7 @@ module Hecks
|
|
|
192
228
|
@field_caches = ensure_field_caches!
|
|
193
229
|
create_event_table!
|
|
194
230
|
create_saga_table!
|
|
231
|
+
create_outbox_table!
|
|
195
232
|
end
|
|
196
233
|
|
|
197
234
|
def table = @aggregate.storage_name
|
|
@@ -216,7 +253,10 @@ module Hecks
|
|
|
216
253
|
return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
|
|
217
254
|
|
|
218
255
|
name = order_by.to_s.split(".").first
|
|
219
|
-
|
|
256
|
+
unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
257
|
+
raise Runtime::WiringError,
|
|
258
|
+
"#{@aggregate.name} has no attribute #{order_by.inspect} to order by"
|
|
259
|
+
end
|
|
220
260
|
|
|
221
261
|
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
222
262
|
@db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
|
|
@@ -268,6 +308,18 @@ module Hecks
|
|
|
268
308
|
head_phase(declared, uncached, ids, args)
|
|
269
309
|
end
|
|
270
310
|
|
|
311
|
+
# ADR 0036's actual fix — see `persistence_capabilities` above.
|
|
312
|
+
# Wraps the WHOLE dispatch order (hydrate through save), not just
|
|
313
|
+
# `append`'s own transaction below: `lock_writes!` has to be held
|
|
314
|
+
# before hydrate even starts, or two cross-process writers can
|
|
315
|
+
# both hydrate unlocked and race for the write, each blind to the
|
|
316
|
+
# other. `transaction` (via `include Adapters::PostgresOutbox`) is
|
|
317
|
+
# already re-entrant — `append`/`atomic_put`'s own inner
|
|
318
|
+
# `transaction do ... end`, deep inside the block below, joins
|
|
319
|
+
# this SAME transaction instead of opening/committing its own, so
|
|
320
|
+
# the advisory lock stays held until this whole block returns.
|
|
321
|
+
def with_write_lock(&block) = transaction { lock_writes!; block.call } # rubocop:disable Style/Semicolon
|
|
322
|
+
|
|
271
323
|
# HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the
|
|
272
324
|
# ordinal is assigned by the column's own `nextval()` default, inside
|
|
273
325
|
# this same statement, so the lock has to already be held before that
|
|
@@ -284,9 +336,12 @@ module Hecks
|
|
|
284
336
|
# still runs, cheaply, during AppendOnly#recover!'s full replay on
|
|
285
337
|
# every boot (see `project` below), and a second write there would
|
|
286
338
|
# make that replay pay real DB cost for a snapshot that's already
|
|
287
|
-
# correct.
|
|
339
|
+
# correct. When `with_write_lock` above already drove this whole
|
|
340
|
+
# dispatch, `lock_writes!` here is a harmless re-acquire of the
|
|
341
|
+
# same already-held (per-session-reentrant) advisory lock; a bare
|
|
342
|
+
# `repository.save` outside a full dispatch still takes it fresh.
|
|
288
343
|
def append(entry)
|
|
289
|
-
|
|
344
|
+
transaction do
|
|
290
345
|
lock_writes!
|
|
291
346
|
append_and_project!(entry)
|
|
292
347
|
end
|
|
@@ -299,7 +354,7 @@ module Hecks
|
|
|
299
354
|
# occurs before entering this adapter-native operation.
|
|
300
355
|
def atomic_put(entry, insert_only: false)
|
|
301
356
|
status = nil
|
|
302
|
-
|
|
357
|
+
transaction do
|
|
303
358
|
lock_writes!
|
|
304
359
|
exists = !@db.exec_params(
|
|
305
360
|
"SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1",
|
|
@@ -418,7 +473,8 @@ module Hecks
|
|
|
418
473
|
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
419
474
|
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
|
|
420
475
|
"completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
|
|
421
|
-
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory),
|
|
476
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory),
|
|
477
|
+
JSON.generate(completed_compensations)]
|
|
422
478
|
)
|
|
423
479
|
end
|
|
424
480
|
|
|
@@ -433,7 +489,8 @@ module Hecks
|
|
|
433
489
|
return enum_for(:each_saga) unless block_given?
|
|
434
490
|
|
|
435
491
|
@db.exec_params(
|
|
436
|
-
"SELECT process_manager, correlation, state, memory, completed_compensations
|
|
492
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations " \
|
|
493
|
+
"FROM hecks_saga_instances WHERE domain = $1",
|
|
437
494
|
[@domain]
|
|
438
495
|
).each do |row|
|
|
439
496
|
yield row["process_manager"], row["correlation"], row["state"],
|
|
@@ -624,11 +681,12 @@ module Hecks
|
|
|
624
681
|
end
|
|
625
682
|
|
|
626
683
|
def cached_where_fields
|
|
627
|
-
declared_queries.flat_map
|
|
628
|
-
q.wheres.map
|
|
684
|
+
fields = declared_queries.flat_map do |q|
|
|
685
|
+
q.wheres.map do |clause|
|
|
629
686
|
clause.field.to_s
|
|
630
|
-
|
|
631
|
-
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
fields.uniq.select { |field| cacheable_field?(field) }
|
|
632
690
|
end
|
|
633
691
|
|
|
634
692
|
def declared_queries
|
|
@@ -709,7 +767,7 @@ module Hecks
|
|
|
709
767
|
clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
|
|
710
768
|
end
|
|
711
769
|
|
|
712
|
-
sql =
|
|
770
|
+
sql = "SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
|
|
713
771
|
sql << if declared.order_by
|
|
714
772
|
" ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
|
|
715
773
|
else
|
|
@@ -751,7 +809,8 @@ module Hecks
|
|
|
751
809
|
# this same domain already created before this column existed —
|
|
752
810
|
# the same idiom `rust/host/src/journal.rs`'s own
|
|
753
811
|
# `sagas_backfilled` column addition already uses.
|
|
754
|
-
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb
|
|
812
|
+
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb " \
|
|
813
|
+
"NOT NULL DEFAULT '[]'::jsonb")
|
|
755
814
|
end
|
|
756
815
|
end
|
|
757
816
|
end
|
|
@@ -15,29 +15,27 @@ module Hecks
|
|
|
15
15
|
module LayerOne
|
|
16
16
|
def layer_one!(violations, aggregate, after)
|
|
17
17
|
after.each do |id, state|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
next unless lifecycle
|
|
18
|
+
symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
|
|
19
|
+
instance = Runtime::Instance.new(aggregate: aggregate, id: id, state: symbolized)
|
|
20
|
+
lifecycle = aggregate.lifecycle
|
|
21
|
+
next unless lifecycle
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
rescue Runtime::InvariantViolation, Runtime::TypeMismatch => error
|
|
39
|
-
violations << "#{aggregate.name}##{id}: #{error.message}"
|
|
23
|
+
held = instance[lifecycle.field]
|
|
24
|
+
# `Lifecycle#states` answers default+targets only — a state
|
|
25
|
+
# legitimately declared just as a `from:` (a terminal
|
|
26
|
+
# transition's source, never anyone's target) is real and
|
|
27
|
+
# reachable but invisible to it. `ModelCheck.full_states`
|
|
28
|
+
# is the full declared set (default, every target, AND
|
|
29
|
+
# every from) that `fuzzing/properties.rb`'s own replay
|
|
30
|
+
# check already uses for this identical question — see its
|
|
31
|
+
# comment on this same hole.
|
|
32
|
+
allowed = Bluebook::ModelCheck.full_states(lifecycle)
|
|
33
|
+
unless held.nil? || allowed.include?(held.to_s)
|
|
34
|
+
violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
|
|
35
|
+
"a state this era's lifecycle never reaches"
|
|
40
36
|
end
|
|
37
|
+
rescue Runtime::InvariantViolation, Runtime::TypeMismatch => e
|
|
38
|
+
violations << "#{aggregate.name}##{id}: #{e.message}"
|
|
41
39
|
end
|
|
42
40
|
end
|
|
43
41
|
end
|