hecks 1.0.2 → 1.1.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 +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- 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 +17 -1
- 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 +2 -1
- 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/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 +19 -472
- 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 +11 -3
- 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 +40 -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 +23 -13
- 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/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/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -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/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- 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 +29 -2
- 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 +26 -3
- 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 +207 -48
- 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 +14 -2
data/lib/hecks/literal.rb
CHANGED
|
@@ -37,19 +37,20 @@ module Hecks
|
|
|
37
37
|
def to_s = "state(:#{name})"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# The `render`/`read` pair the header above describes: `render` turns a
|
|
41
|
+
# Ruby value into its self-describing wire spelling, `read` is its exact
|
|
42
|
+
# inverse.
|
|
40
43
|
module Literal
|
|
41
44
|
module_function
|
|
42
45
|
|
|
43
46
|
def render(value)
|
|
44
47
|
case value
|
|
45
|
-
when nil
|
|
46
|
-
when
|
|
47
|
-
when
|
|
48
|
-
when
|
|
49
|
-
when
|
|
50
|
-
when
|
|
51
|
-
when Hash then "{#{value.map { |key, held| "#{key}: #{render(held)}" }.join(', ')}}"
|
|
52
|
-
when Array then "[#{value.map { |held| render(held) }.join(', ')}]"
|
|
48
|
+
when nil then "nil"
|
|
49
|
+
when Symbol then ":#{value}"
|
|
50
|
+
when String then quote(value)
|
|
51
|
+
when StateRef, true, false, Integer, Float then value.to_s
|
|
52
|
+
when Hash then "{#{value.map { |key, held| "#{key}: #{render(held)}" }.join(', ')}}"
|
|
53
|
+
when Array then "[#{value.map { |held| render(held) }.join(', ')}]"
|
|
53
54
|
else
|
|
54
55
|
raise ArgumentError, "#{value.class} has no pinned literal spelling — teach Literal.render one " \
|
|
55
56
|
"rather than letting #to_s decide it"
|
|
@@ -59,6 +60,15 @@ module Hecks
|
|
|
59
60
|
# Read one back. Tolerant of a bare word on purpose: the language stores a
|
|
60
61
|
# closed set's members and a few hand-written fields as plain text that was
|
|
61
62
|
# never rendered, and those must stay the strings they are.
|
|
63
|
+
#
|
|
64
|
+
# The exact inverse of `render`'s `case`, above — one ordered guard per
|
|
65
|
+
# spelling `render` can produce (plus the bare-word tolerance this
|
|
66
|
+
# comment already explains). Each guard is a single self-contained
|
|
67
|
+
# `return`; splitting them into named predicates would just rename
|
|
68
|
+
# each line without changing what it does, and would separate this
|
|
69
|
+
# method from the `render` it is the deliberate mirror of.
|
|
70
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
71
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
62
72
|
def read(text)
|
|
63
73
|
raw = text.to_s.strip
|
|
64
74
|
return nil if raw.empty? || raw == "nil"
|
data/lib/hecks/naming.rb
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
module Hecks
|
|
2
|
+
# Every derived-name and identity-joining rule the language relies on —
|
|
3
|
+
# pluralisation/singularisation, snake/Pascal casing, dotted-path and verb
|
|
4
|
+
# splitting, and the command/event reference rewrites — collected here so
|
|
5
|
+
# two readers never invent two different spellings for the same
|
|
6
|
+
# derivation.
|
|
2
7
|
module Naming
|
|
3
8
|
# WHAT SEPARATES THE PARTS OF A DERIVED IDENTITY.
|
|
4
9
|
#
|
|
@@ -71,9 +76,7 @@ module Hecks
|
|
|
71
76
|
# is left actually ends in one of those five shapes is what
|
|
72
77
|
# keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
|
|
73
78
|
# from also losing a letter it never doubled.
|
|
74
|
-
if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
|
|
75
|
-
return word[0..-3]
|
|
76
|
-
end
|
|
79
|
+
return word[0..-3] if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
|
|
77
80
|
|
|
78
81
|
return word[0..-2] if word.length > 1 && word.end_with?("s")
|
|
79
82
|
|
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
# a domain's adapter here calls that directly, rather than this
|
|
20
20
|
# port re-deriving Governance-query logic another port already owns.
|
|
21
21
|
module AccessControl
|
|
22
|
-
NAME = "access_control"
|
|
22
|
+
NAME = "access_control".freeze
|
|
23
23
|
|
|
24
24
|
module_function
|
|
25
25
|
|
data/lib/hecks/ports/agent.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Hecks
|
|
|
39
39
|
# differently would mean the SAME malformed answer passing through
|
|
40
40
|
# one and refusing through the other.
|
|
41
41
|
module Agent
|
|
42
|
-
NAME = "agent"
|
|
42
|
+
NAME = "agent".freeze
|
|
43
43
|
|
|
44
44
|
# The answer came back and could not be used — no JSON in it, a
|
|
45
45
|
# category the language does not declare, a severity outside the
|
|
@@ -48,14 +48,16 @@ module Hecks
|
|
|
48
48
|
# `Ports::Authentication::ValidationError` already is, so no
|
|
49
49
|
# caller ever rescues a raw parser error leaking out of an
|
|
50
50
|
# adapter.
|
|
51
|
-
ValidationError
|
|
51
|
+
class ValidationError < StandardError
|
|
52
|
+
end
|
|
52
53
|
|
|
53
54
|
# Nothing answered at all — the binary is missing, the subprocess
|
|
54
55
|
# died, the call timed out. Separate from ValidationError on
|
|
55
56
|
# purpose: unavailable means retry or fall back to asking the
|
|
56
57
|
# human directly; malformed means the PROMPT is wrong, not the
|
|
57
58
|
# transport, and retrying identically will not fix it.
|
|
58
|
-
Unavailable
|
|
59
|
+
class Unavailable < StandardError
|
|
60
|
+
end
|
|
59
61
|
|
|
60
62
|
# ── what the four operations hand back ──────────────────────────
|
|
61
63
|
#
|
|
@@ -17,9 +17,10 @@ module Hecks
|
|
|
17
17
|
# purely to resolve which adapter answers it, same as every port
|
|
18
18
|
# here already does).
|
|
19
19
|
module Authentication
|
|
20
|
-
NAME = "authentication"
|
|
20
|
+
NAME = "authentication".freeze
|
|
21
21
|
|
|
22
|
-
ValidationError
|
|
22
|
+
class ValidationError < StandardError
|
|
23
|
+
end
|
|
23
24
|
|
|
24
25
|
module_function
|
|
25
26
|
|
data/lib/hecks/ports/clock.rb
CHANGED
|
@@ -2,8 +2,12 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
+
# The `extraction` port: resolves whichever adapter can recover a
|
|
6
|
+
# predicate's own source (`canonical`). Reads `Hecks.current_registry`
|
|
7
|
+
# directly rather than taking a registry argument, since extraction only
|
|
8
|
+
# ever happens while a bluebook is loading, never after boot.
|
|
5
9
|
module Extraction
|
|
6
|
-
NAME = "extraction"
|
|
10
|
+
NAME = "extraction".freeze
|
|
7
11
|
|
|
8
12
|
module_function
|
|
9
13
|
|
data/lib/hecks/ports/loading.rb
CHANGED
|
@@ -2,8 +2,11 @@ require_relative "../adapters/driven/folder"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
+
# The `loading` port: `bootstrap` hands back the Folder adapter that
|
|
6
|
+
# reads bluebook files off disk to build the very first registry, before
|
|
7
|
+
# any other port has a registry to resolve against.
|
|
5
8
|
module Loading
|
|
6
|
-
NAME = "loading"
|
|
9
|
+
NAME = "loading".freeze
|
|
7
10
|
|
|
8
11
|
module_function
|
|
9
12
|
|
|
@@ -23,12 +23,14 @@ module Hecks
|
|
|
23
23
|
@adapter = adapter
|
|
24
24
|
required = %i[append project entries]
|
|
25
25
|
missing = required.reject { |method| adapter.respond_to?(method) }
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
unless missing.empty?
|
|
27
|
+
raise Runtime::WiringError,
|
|
28
|
+
"#{adapter.class} does not implement append-only persistence: #{missing.join(', ')}"
|
|
29
|
+
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def find(id) = @adapter.find(id)
|
|
31
|
-
def all(**
|
|
33
|
+
def all(**) = @adapter.all(**)
|
|
32
34
|
def count = @adapter.count
|
|
33
35
|
def entries = @adapter.entries
|
|
34
36
|
|
|
@@ -94,7 +96,8 @@ module Hecks
|
|
|
94
96
|
|
|
95
97
|
def atomic_put(instance, insert_only: false)
|
|
96
98
|
unless capabilities.include?(:atomic_put) && @adapter.respond_to?(:atomic_put)
|
|
97
|
-
raise Runtime::WiringError,
|
|
99
|
+
raise Runtime::WiringError,
|
|
100
|
+
"#{@adapter.class} advertises no atomic_put persistence capability"
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
@@ -127,6 +130,39 @@ module Hecks
|
|
|
127
130
|
@adapter.record_event(event) if @adapter.respond_to?(:record_event)
|
|
128
131
|
end
|
|
129
132
|
|
|
133
|
+
# ONE COMMIT BOUNDARY FOR SAVE + EMIT + OUTBOX — `Interpreting#
|
|
134
|
+
# run_dispatch_order` runs the `save` and `emit` steps inside
|
|
135
|
+
# this block, so an adapter that owns a real transaction
|
|
136
|
+
# (Sqlite, Postgres) commits the aggregate row, its journal
|
|
137
|
+
# entry, and the outbox rows together or not at all. An adapter
|
|
138
|
+
# without one just yields: Memory has nothing to roll back, and
|
|
139
|
+
# a file adapter's own `with_lock` already serialises its pair
|
|
140
|
+
# of writes. Nested calls are the adapter's problem to make
|
|
141
|
+
# re-entrant (both SQL adapters check for an open transaction
|
|
142
|
+
# first) — a reaction dispatched from inside a drain never nests
|
|
143
|
+
# here anyway, because draining happens after this block returns.
|
|
144
|
+
def transaction(&)
|
|
145
|
+
return @adapter.transaction(&) if @adapter.respond_to?(:transaction)
|
|
146
|
+
|
|
147
|
+
yield
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# THE OUTBOX CONTRACT — four optional adapter methods, probed
|
|
151
|
+
# together the way `save_saga`/`delete_saga`/`each_saga` are
|
|
152
|
+
# (`Registry::SagaPersistence`): an adapter either has an outbox
|
|
153
|
+
# or it doesn't, never half of one. See `Runtime::Outbox`.
|
|
154
|
+
OUTBOX_METHODS = %i[outbox_enqueue outbox_claim outbox_settle outbox_rows].freeze
|
|
155
|
+
|
|
156
|
+
def outbox?
|
|
157
|
+
@outbox = OUTBOX_METHODS.all? { |method| @adapter.respond_to?(method) } if @outbox.nil?
|
|
158
|
+
@outbox
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def outbox_enqueue(rows) = @adapter.outbox_enqueue(rows)
|
|
162
|
+
def outbox_claim(id) = @adapter.outbox_claim(id)
|
|
163
|
+
def outbox_settle(id, status:, error: nil) = @adapter.outbox_settle(id, status: status, error: error)
|
|
164
|
+
def outbox_rows(status: nil) = @adapter.outbox_rows(status: status)
|
|
165
|
+
|
|
130
166
|
def query_read_model(domain, model, args, bluebook = nil)
|
|
131
167
|
return unless @adapter.respond_to?(:query_read_model)
|
|
132
168
|
|
|
@@ -46,7 +46,7 @@ module Hecks
|
|
|
46
46
|
|
|
47
47
|
def unsupported_roles(domain, aggregate, bindings)
|
|
48
48
|
Runtime::WiringError.new(
|
|
49
|
-
"#{domain}::#{aggregate.hecks_name} uses persistence role#{bindings.size == 1
|
|
49
|
+
"#{domain}::#{aggregate.hecks_name} uses persistence role#{'s' unless bindings.size == 1} " \
|
|
50
50
|
"#{bindings.map(&:role).map(&:inspect).join(', ')}. Only persisted_by is supported."
|
|
51
51
|
)
|
|
52
52
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Ports
|
|
3
|
+
# Reopened to hold the persistence-plugin registry (`Plugin`, below) and
|
|
4
|
+
# its call surface (`register_plugin`/`plugin?`/`each_plugin`) — see ADR
|
|
5
|
+
# 0033 and `Plugin`'s own header for the seam this fills.
|
|
3
6
|
module Persistence
|
|
4
7
|
# A tiny, real seam — ADR 0033. Core never names a persistence plugin;
|
|
5
8
|
# a plugin makes itself known as a side effect of being `require`d, the
|
|
@@ -105,7 +105,9 @@ module Hecks
|
|
|
105
105
|
def source_text_for(bluebook, directory)
|
|
106
106
|
domain_files = Dir[File.join(directory, "*.bluebook")]
|
|
107
107
|
own = domain_files.select do |path|
|
|
108
|
-
File.foreach(path, encoding: "UTF-8").any?
|
|
108
|
+
File.foreach(path, encoding: "UTF-8").any? do |line|
|
|
109
|
+
line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook.name.inspect)}/)
|
|
110
|
+
end
|
|
109
111
|
end
|
|
110
112
|
own = domain_files if own.empty? && domain_files.size == 1 && !Framework.members.key?(bluebook.name)
|
|
111
113
|
own = [Framework.members[bluebook.name]].compact if own.empty?
|
|
@@ -7,9 +7,9 @@ module Hecks
|
|
|
7
7
|
# call it too.
|
|
8
8
|
module ShapeDiff
|
|
9
9
|
def shape(aggregate)
|
|
10
|
-
aggregate.attributes.map
|
|
10
|
+
aggregate.attributes.map do |attribute|
|
|
11
11
|
[attribute.name, attribute_signature(aggregate, attribute.type)]
|
|
12
|
-
|
|
12
|
+
end.sort_by(&:first)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# A plain type name for a primitive; `[type_name, member_signatures]`
|
|
@@ -20,9 +20,9 @@ module Hecks
|
|
|
20
20
|
container = nested_type(aggregate, type_name)
|
|
21
21
|
return type_name if container.nil? || seen.include?(type_name)
|
|
22
22
|
|
|
23
|
-
members = container.attributes.map
|
|
23
|
+
members = container.attributes.map do |member|
|
|
24
24
|
[member.name, attribute_signature(aggregate, member.type, seen + [type_name])]
|
|
25
|
-
|
|
25
|
+
end.sort_by(&:first)
|
|
26
26
|
[type_name, members]
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -64,7 +64,7 @@ module Hecks
|
|
|
64
64
|
return false if attribute.list?
|
|
65
65
|
|
|
66
66
|
value_object = aggregate.value_object(attribute.type)
|
|
67
|
-
return false if value_object
|
|
67
|
+
return false if value_object&.attributes&.all? { |field| !field.default.nil? }
|
|
68
68
|
|
|
69
69
|
true
|
|
70
70
|
end
|
|
@@ -117,6 +117,15 @@ module Hecks
|
|
|
117
117
|
# outright (nothing to be a prefix of on the held side), unlike
|
|
118
118
|
# every rule above it, which explains a path that existed and
|
|
119
119
|
# moved, converted, or vanished.
|
|
120
|
+
#
|
|
121
|
+
# One `||` chain over a closed, fixed set of rule kinds (renames,
|
|
122
|
+
# moves, converts, drops, computes, backfills) — the same six this
|
|
123
|
+
# file's other methods enumerate. Splitting each disjunct into its
|
|
124
|
+
# own predicate would scatter one question ("does ANY rule explain
|
|
125
|
+
# this path") across six same-shaped methods with nothing else to
|
|
126
|
+
# do.
|
|
127
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
128
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
120
129
|
def explains?(path)
|
|
121
130
|
path = path.to_s
|
|
122
131
|
top = path.split(".").first
|
|
@@ -202,12 +211,12 @@ module Hecks
|
|
|
202
211
|
def apply_renames(state, renames)
|
|
203
212
|
snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
|
|
204
213
|
snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
|
|
205
|
-
#
|
|
206
|
-
# (:a<->:b) needs
|
|
207
|
-
#
|
|
208
|
-
#
|
|
214
|
+
# NOT combinable (Style/CombinableLoops is disabled repo-wide, see
|
|
215
|
+
# .rubocop.yml, for exactly this reason): a swap (:a<->:b) needs
|
|
216
|
+
# every delete done before any write, or the first rename's write
|
|
217
|
+
# becomes the second rename's delete target — see this method's
|
|
218
|
+
# own comment above.
|
|
209
219
|
snapshot.each { |_old_name, new_name, value| state[new_name] = value }
|
|
210
|
-
# rubocop:enable Style/CombinableLoops
|
|
211
220
|
end
|
|
212
221
|
|
|
213
222
|
def apply_drop(state, name)
|
|
@@ -6,6 +6,10 @@ module Hecks
|
|
|
6
6
|
module Adapters
|
|
7
7
|
class PostgresEra
|
|
8
8
|
class Lineage
|
|
9
|
+
# CRUD over `hecks_eras`/`hecks_era_texts`/`hecks_approvals`: reading
|
|
10
|
+
# and tamper-verifying held era text, archiving every frozen text
|
|
11
|
+
# version, recording a Layer-3 approval, and minting an era's name
|
|
12
|
+
# once (never recomputed after).
|
|
9
13
|
module EraStore
|
|
10
14
|
# Every held text is verified against its raw-byte digest on the
|
|
11
15
|
# way out — an edited storage fact refuses rather than silently
|
|
@@ -89,7 +89,7 @@ module Hecks
|
|
|
89
89
|
source_sql: lambda do |cursor|
|
|
90
90
|
<<~SQL
|
|
91
91
|
SELECT id, ordinal, value FROM (#{field_cache_source_sql(storage_name, era, value_expression)}) reduced
|
|
92
|
-
#{
|
|
92
|
+
#{"WHERE id > #{text_literal(cursor)}" if cursor}
|
|
93
93
|
ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
|
|
94
94
|
SQL
|
|
95
95
|
end,
|
|
@@ -5,6 +5,12 @@ module Hecks
|
|
|
5
5
|
module Adapters
|
|
6
6
|
class PostgresEra
|
|
7
7
|
class Lineage
|
|
8
|
+
# The read side of lineage: builds and maintains, per aggregate per
|
|
9
|
+
# era, the head-snapshot table (`ensure_head_snapshot!`/
|
|
10
|
+
# `backfill_head_snapshot!`) and the compiled materialized
|
|
11
|
+
# view/plain view (`compile_head!`) that translate and reduce the
|
|
12
|
+
# append-only journal into the single current-era head a query
|
|
13
|
+
# actually reads.
|
|
8
14
|
module HeadCompiler
|
|
9
15
|
# ── head compilation ───────────────────────────────────────────
|
|
10
16
|
|
|
@@ -124,7 +130,7 @@ module Hecks
|
|
|
124
130
|
SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, ordinal, operation, state
|
|
125
131
|
FROM #{quoted_journal}
|
|
126
132
|
WHERE era = #{era.to_i} AND aggregate = #{text_literal(storage_name)}
|
|
127
|
-
#{
|
|
133
|
+
#{"AND aggregate_id > #{text_literal(cursor)}" if cursor}
|
|
128
134
|
ORDER BY aggregate_id, ordinal DESC
|
|
129
135
|
) latest WHERE operation = 'save' ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
|
|
130
136
|
SQL
|
|
@@ -160,8 +166,8 @@ module Hecks
|
|
|
160
166
|
# re-raised) on failure, and either way the surrounding
|
|
161
167
|
# transaction is never touched — no early COMMIT, no early
|
|
162
168
|
# release of whatever advisory lock it holds.
|
|
163
|
-
def nested_transaction(name)
|
|
164
|
-
return @db.transaction
|
|
169
|
+
def nested_transaction(name, &)
|
|
170
|
+
return @db.transaction(&) if @db.transaction_status == PG::PQTRANS_IDLE
|
|
165
171
|
|
|
166
172
|
@db.exec("SAVEPOINT #{name}")
|
|
167
173
|
begin
|
|
@@ -252,8 +258,31 @@ module Hecks
|
|
|
252
258
|
# written under — so the final edge applies uniformly, with no
|
|
253
259
|
# per-era CASE. That equality is asserted, not argued: the spec
|
|
254
260
|
# builds a third era both ways and diffs them.
|
|
261
|
+
# `edges.size != era - 1` — added alongside `head_body_sql`
|
|
262
|
+
# below, not merely a pre-existing guard: `names_by_era(aggregate,
|
|
263
|
+
# edges)` sizes `names[:storage]` to `edges.size + 1`, and the
|
|
264
|
+
# union above indexes it at `names[:storage][era - 2]` — CORRECT
|
|
265
|
+
# only when `edges` is the FULL chain reaching `era` (mint's own
|
|
266
|
+
# call, and the audit's own "after" reading, both are). A caller
|
|
267
|
+
# handed a SHORTER chain against the SAME `era` — exactly what
|
|
268
|
+
# the audit's own "before" reading is, `chain[0..-2]` against the
|
|
269
|
+
# unchanged target era — would index `names[:storage]` out of
|
|
270
|
+
# its real bounds instead of falling back to `chain_sql` the way
|
|
271
|
+
# every other mismatch here already does. Guarded structurally,
|
|
272
|
+
# not by trusting every future caller to know this invariant.
|
|
273
|
+
#
|
|
274
|
+
# The guard clauses above ARE the method: each rules out one way
|
|
275
|
+
# the layered shortcut cannot honestly apply (era too young, too
|
|
276
|
+
# few edges, a mismatched chain length, no prior era, no label,
|
|
277
|
+
# no materialized prior view) before the SQL-building tail runs.
|
|
278
|
+
# Splitting them out would separate each guard from the algebra
|
|
279
|
+
# comment it protects, and thread `held`/`prior`/`prior_view`
|
|
280
|
+
# back out as return values with no simpler shape than they
|
|
281
|
+
# already have.
|
|
282
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
283
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
255
284
|
def layered_chain_sql(aggregate, era, edges)
|
|
256
|
-
return nil if era < 3 || edges.size < 2
|
|
285
|
+
return nil if era < 3 || edges.size < 2 || edges.size != era - 1
|
|
257
286
|
|
|
258
287
|
held = eras
|
|
259
288
|
prior = held.find { |candidate| candidate[:ordinal] == era - 1 }
|
|
@@ -266,7 +295,12 @@ module Hecks
|
|
|
266
295
|
cut = held.find { |candidate| candidate[:ordinal] == era }&.dig(:watermark)
|
|
267
296
|
declared = edges.last[:translation].for_aggregate(names[:current][edges.size])
|
|
268
297
|
expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
|
|
269
|
-
id_column = Translation::RuleCompiler.rekeyed?(declared)
|
|
298
|
+
id_column = if Translation::RuleCompiler.rekeyed?(declared)
|
|
299
|
+
Translation::RuleCompiler.id_case("operation = 'save'",
|
|
300
|
+
declared)
|
|
301
|
+
else
|
|
302
|
+
"aggregate_id"
|
|
303
|
+
end
|
|
270
304
|
|
|
271
305
|
<<~SQL
|
|
272
306
|
WITH layered AS (
|
|
@@ -274,7 +308,7 @@ module Hecks
|
|
|
274
308
|
SELECT ordinal, aggregate_id, operation, state FROM #{quote(prior_view)}
|
|
275
309
|
UNION ALL
|
|
276
310
|
SELECT ordinal, aggregate_id, operation, state FROM #{quoted_journal}
|
|
277
|
-
WHERE era = #{era - 1} AND aggregate = #{text_literal(names[:storage][era - 2])}#{
|
|
311
|
+
WHERE era = #{era - 1} AND aggregate = #{text_literal(names[:storage][era - 2])}#{" AND ordinal <= #{cut}" if cut}
|
|
278
312
|
) layers ORDER BY aggregate_id, ordinal DESC
|
|
279
313
|
)
|
|
280
314
|
SELECT ordinal, #{id_column}, operation,
|
|
@@ -290,7 +324,12 @@ module Hecks
|
|
|
290
324
|
declared = edge[:translation].for_aggregate(names[:current][index + 1])
|
|
291
325
|
expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
|
|
292
326
|
guard = "era <= #{index + 1} AND operation = 'save'"
|
|
293
|
-
id_column = Translation::RuleCompiler.rekeyed?(declared)
|
|
327
|
+
id_column = if Translation::RuleCompiler.rekeyed?(declared)
|
|
328
|
+
Translation::RuleCompiler.id_case(guard,
|
|
329
|
+
declared)
|
|
330
|
+
else
|
|
331
|
+
"aggregate_id"
|
|
332
|
+
end
|
|
294
333
|
"edge_#{index + 1} AS (SELECT ordinal, era, #{id_column}, operation, " \
|
|
295
334
|
"CASE WHEN #{guard} THEN #{expression} ELSE state END AS state " \
|
|
296
335
|
"FROM #{index.zero? ? 'tail' : "edge_#{index}"})"
|
|
@@ -305,9 +344,19 @@ module Hecks
|
|
|
305
344
|
|
|
306
345
|
# The translated tail as it would stand in era `era`, latest
|
|
307
346
|
# entry per id, saves only — what the audit holds up against the
|
|
308
|
-
# bluebook and the edge.
|
|
347
|
+
# bluebook and the edge. Reads through `head_body_sql`, the SAME
|
|
348
|
+
# layered-or-full choice `compile_head!` makes at real mint time
|
|
349
|
+
# (that method's own comment has the full reasoning) — so this
|
|
350
|
+
# is provably what the real mint will materialize, not a second
|
|
351
|
+
# implementation that could silently drift from it. Safe for
|
|
352
|
+
# BOTH real callers: the "after" reading (`edges` is the full
|
|
353
|
+
# chain reaching `era`) and the "before" reading (`edges` one
|
|
354
|
+
# shorter, same `era` — `audit.rb`'s own `samples_for`/`check`
|
|
355
|
+
# callers) — `layered_chain_sql`'s own `edges.size != era - 1`
|
|
356
|
+
# guard falls back to `chain_sql` exactly when the shorter
|
|
357
|
+
# chain can't honestly answer the layered question.
|
|
309
358
|
def translated_latest(aggregate, era, edges)
|
|
310
|
-
latest_of(
|
|
359
|
+
latest_of(head_body_sql(aggregate, era, edges))
|
|
311
360
|
end
|
|
312
361
|
|
|
313
362
|
# The UNtranslated ancestor tail, latest entry per id — the
|
|
@@ -363,15 +412,30 @@ module Hecks
|
|
|
363
412
|
# write count; the ancestor side was already bounded that way
|
|
364
413
|
# (the matview only ever holds the reduced tail, never raw
|
|
365
414
|
# history — see latest_per_id's own comment).
|
|
415
|
+
# THE ONE PLACE THAT PICKS layered VS full — pulled out so the
|
|
416
|
+
# audit's own preview (`translated_latest`, below: both
|
|
417
|
+
# `bin/translation_audit`'s standalone run and the real
|
|
418
|
+
# mint-time gate in coverage_check.rb#audit!) reads through the
|
|
419
|
+
# IDENTICAL branch selection a real mint's own `compile_head!`
|
|
420
|
+
# will use, rather than a second, hand-kept-in-sync copy of this
|
|
421
|
+
# same "layered when eligible, else full" choice. Before this,
|
|
422
|
+
# `translated_latest` called `chain_sql` unconditionally — for
|
|
423
|
+
# any era >= 3, the real mint (this method, `full: false` by
|
|
424
|
+
# default) could materialize via the LAYERED path while the
|
|
425
|
+
# preview that approved it never exercised that branch at all.
|
|
426
|
+
# `full:` mirrors `compile_head!`'s own default; tail_merge's
|
|
427
|
+
# `full: true` rebuild is the one caller that ever needs the
|
|
428
|
+
# non-layered branch unconditionally.
|
|
429
|
+
def head_body_sql(aggregate, era, edges, full: false)
|
|
430
|
+
return chain_sql(aggregate, era, edges) if full
|
|
431
|
+
|
|
432
|
+
layered_chain_sql(aggregate, era, edges) || chain_sql(aggregate, era, edges)
|
|
433
|
+
end
|
|
434
|
+
|
|
366
435
|
def compile_head!(aggregate, era, label, edges, full: false)
|
|
367
436
|
storage_name = aggregate.storage_name
|
|
368
437
|
view = matview(storage_name, era, label)
|
|
369
|
-
body =
|
|
370
|
-
if !full && (layered = layered_chain_sql(aggregate, era, edges))
|
|
371
|
-
layered
|
|
372
|
-
else
|
|
373
|
-
chain_sql(aggregate, era, edges)
|
|
374
|
-
end
|
|
438
|
+
body = head_body_sql(aggregate, era, edges, full: full)
|
|
375
439
|
@db.exec(<<~SQL)
|
|
376
440
|
CREATE MATERIALIZED VIEW #{quote(view)} AS
|
|
377
441
|
#{body}
|
|
@@ -445,7 +509,7 @@ module Hecks
|
|
|
445
509
|
cut = watermarks[ancestor + 1]
|
|
446
510
|
"SELECT ordinal, era, aggregate, aggregate_id, operation, state FROM #{quoted_journal} " \
|
|
447
511
|
"WHERE era = #{ancestor} AND aggregate = #{text_literal(names[:storage][ancestor - 1])}" \
|
|
448
|
-
"#{
|
|
512
|
+
"#{" AND ordinal <= #{cut}" if cut}"
|
|
449
513
|
end
|
|
450
514
|
selects.join(" UNION ALL ")
|
|
451
515
|
end
|
|
@@ -5,6 +5,11 @@ module Hecks
|
|
|
5
5
|
module Adapters
|
|
6
6
|
class PostgresEra
|
|
7
7
|
class Lineage
|
|
8
|
+
# `mint_era!` itself: one transaction that writes the new era row,
|
|
9
|
+
# attaches its partition, recompiles every aggregate's head, grants
|
|
10
|
+
# an app role, and — last, right before commit — advances the
|
|
11
|
+
# current-era RLS fence (`advance_era!`) that drops write access to
|
|
12
|
+
# the old schema the instant it commits.
|
|
8
13
|
module MintTransaction
|
|
9
14
|
# ── the mint transaction ───────────────────────────────────────
|
|
10
15
|
#
|
|
@@ -30,7 +35,8 @@ module Hecks
|
|
|
30
35
|
|
|
31
36
|
watermark = last_ordinal
|
|
32
37
|
@db.exec_params(
|
|
33
|
-
"INSERT INTO hecks_eras (domain, ordinal, hash, label, held_text, watermark, held_digest,
|
|
38
|
+
"INSERT INTO hecks_eras (domain, ordinal, hash, label, held_text, watermark, held_digest, " \
|
|
39
|
+
"held_projection, canon_form) " \
|
|
34
40
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
|
35
41
|
[@domain, ordinal, hash, label, held_text, watermark, Digest::SHA256.hexdigest(held_text),
|
|
36
42
|
projection && JSON.generate(projection), Runtime::StorageShape::FORM_VERSION]
|
|
@@ -70,13 +76,21 @@ module Hecks
|
|
|
70
76
|
@db.exec("COMMIT")
|
|
71
77
|
true
|
|
72
78
|
rescue PG::LockNotAvailable
|
|
73
|
-
|
|
79
|
+
begin
|
|
80
|
+
@db.exec("ROLLBACK")
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
74
84
|
raise Runtime::WiringError,
|
|
75
85
|
"cannot mint era #{ordinal} of #{@domain}: another mint holds the domain lock — " \
|
|
76
86
|
"waited 10s; try again shortly"
|
|
77
|
-
rescue PG::Error =>
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
rescue PG::Error => e
|
|
88
|
+
begin
|
|
89
|
+
@db.exec("ROLLBACK")
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{e.message.strip}"
|
|
80
94
|
end
|
|
81
95
|
|
|
82
96
|
# Base privileges for a deployment's app role — a NON-owner,
|
|
@@ -116,7 +130,10 @@ module Hecks
|
|
|
116
130
|
aggregates.each do |aggregate|
|
|
117
131
|
storage_name = aggregate.storage_name
|
|
118
132
|
@db.exec("GRANT SELECT, INSERT, UPDATE, DELETE ON #{quote(head_snapshot(storage_name, era))} TO #{quoted_role}")
|
|
119
|
-
|
|
133
|
+
if view_exists?(head_view(storage_name))
|
|
134
|
+
@db.exec("GRANT SELECT ON #{quote(head_view(storage_name))} " \
|
|
135
|
+
"TO #{quoted_role}")
|
|
136
|
+
end
|
|
120
137
|
end
|
|
121
138
|
end
|
|
122
139
|
|