hecks 1.2.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -19,16 +19,16 @@ module Hecks
|
|
|
19
19
|
@db.exec(
|
|
20
20
|
"CREATE TABLE IF NOT EXISTS #{quoted_table} (id text PRIMARY KEY#{', ' unless columns.empty?}#{columns.join(', ')})"
|
|
21
21
|
)
|
|
22
|
-
#
|
|
23
|
-
# `CREATE TABLE IF NOT EXISTS` above does
|
|
22
|
+
# **Self-healing, same idiom as `ensure_indexes!` below** —
|
|
23
|
+
# `CREATE TABLE IF NOT EXISTS` above does not retroactively add a
|
|
24
24
|
# column to an already-existing table (a committed database from
|
|
25
25
|
# before optimistic-concurrency CAS existed), so this runs
|
|
26
26
|
# unconditionally on every boot and no-ops once the column is
|
|
27
|
-
# there.
|
|
27
|
+
# there. Adapter bookkeeping only — never listed in
|
|
28
28
|
# `persisted_fields` (Codec), so it never appears in `decode`'s
|
|
29
29
|
# domain-state hash or `Instance#to_h`.
|
|
30
30
|
@db.exec("ALTER TABLE #{quoted_table} ADD COLUMN IF NOT EXISTS hecks_version bigint NOT NULL DEFAULT 1")
|
|
31
|
-
#
|
|
31
|
+
# Same self-healing shape, for domain attributes themselves —
|
|
32
32
|
# `hecks_version` above only heals the adapter's own bookkeeping
|
|
33
33
|
# column; a bluebook attribute added (or, via `translations/`,
|
|
34
34
|
# renamed) after this table already exists is not bookkeeping,
|
|
@@ -47,7 +47,7 @@ module Hecks
|
|
|
47
47
|
@db.exec("ALTER TABLE #{quoted_table} ADD COLUMN IF NOT EXISTS " \
|
|
48
48
|
"#{quote_ident(field[:name])} #{field[:sql_type]}")
|
|
49
49
|
end
|
|
50
|
-
#
|
|
50
|
+
# Right here, not a separate step in `Postgres#initialize` —
|
|
51
51
|
# same idiom Sqlite::SchemaBuilder's own `create_aggregate_table!`
|
|
52
52
|
# uses: index creation runs unconditionally, right after the
|
|
53
53
|
# table it indexes exists, the same self-healing shape
|
|
@@ -118,11 +118,11 @@ module Hecks
|
|
|
118
118
|
# Derived from every declared query's `where`/`order_by` fields —
|
|
119
119
|
# no bluebook author opts in, matching every other self-healing
|
|
120
120
|
# schema move this adapter makes. Walks the aggregate's own
|
|
121
|
-
# queries
|
|
121
|
+
# queries and each entity's own queries — the same enumeration
|
|
122
122
|
# `dsl/aggregate_builder.rb`'s own `query_surfaces` walks at
|
|
123
123
|
# declaration-seal time — but an entity's own fields never
|
|
124
124
|
# produce an index below: an entity has no table of its own, its
|
|
125
|
-
# rows live inside a
|
|
125
|
+
# rows live inside a list-typed attribute on the aggregate's own
|
|
126
126
|
# table (`QueryInterpreter#entity_rows` resolves it that way, and
|
|
127
127
|
# answers an entity query entirely in memory over `repository.all`
|
|
128
128
|
# — it never reaches this adapter's own `query` method at all).
|
|
@@ -154,15 +154,15 @@ module Hecks
|
|
|
154
154
|
# FROM jsonb_array_elements(...) ...)` (see `list_contains_clause`
|
|
155
155
|
# below), a SQL shape neither a plain btree on the raw jsonb
|
|
156
156
|
# column nor even a GIN jsonb index (`@>`, `?`) accelerates —
|
|
157
|
-
# Postgres's own GIN jsonb operators match a
|
|
157
|
+
# Postgres's own GIN jsonb operators match a different SQL shape
|
|
158
158
|
# than the `EXISTS` + `jsonb_array_elements` this codebase always
|
|
159
159
|
# compiles a list `contains` to. An index here would be dead
|
|
160
160
|
# weight, not free correctness, so none is attempted.
|
|
161
161
|
def index_field!(field)
|
|
162
|
-
# A
|
|
162
|
+
# A hop path ("owner/field" — `Bluebook::AggregateBuilder`'s own
|
|
163
163
|
# convention for a field reached by crossing a reference,
|
|
164
164
|
# `aggregate_builder.rb`'s own `field.to_s.include?("/")`
|
|
165
|
-
# checks)
|
|
165
|
+
# checks) is never indexed here — same reasoning as `list_of`
|
|
166
166
|
# below: `query_expression`/`nested_expression` below have no
|
|
167
167
|
# dialect for this shape at all (they'd compile the whole
|
|
168
168
|
# "owner/field" string as one bare column/jsonb-path segment,
|
|
@@ -173,10 +173,10 @@ module Hecks
|
|
|
173
173
|
# Skipping the index is always safe, the same way skipping a
|
|
174
174
|
# `list_of` index is: an index is a perf optimization, not
|
|
175
175
|
# correctness, so "never built" beats "built wrong." Whether a
|
|
176
|
-
# hop-path field can be
|
|
176
|
+
# hop-path field can be queried at all against this adapter is
|
|
177
177
|
# a separate, still-open question this skip does not answer —
|
|
178
178
|
# see docs/future-features.md's fuzzer-adapter entry, and
|
|
179
|
-
# docs/1.0-readiness.md item 2 for this gap's
|
|
179
|
+
# docs/1.0-readiness.md item 2 for this gap's other two
|
|
180
180
|
# independent failure points (Postgres querying, Rust codegen's
|
|
181
181
|
# `OpenForSuspendedCustomers` — bin/rust_coverage's own
|
|
182
182
|
# allowlist) — tracked together, not as three unrelated bugs.
|
|
@@ -186,7 +186,7 @@ module Hecks
|
|
|
186
186
|
attribute = @aggregate.attribute(name)
|
|
187
187
|
return if attribute&.list?
|
|
188
188
|
|
|
189
|
-
#
|
|
189
|
+
# The same expression the query itself compiles to — calling
|
|
190
190
|
# `query_expression`/`plain_column`, the real dialect methods,
|
|
191
191
|
# rather than a second, hand-rolled derivation of the same
|
|
192
192
|
# path that could silently drift from it. An index whose
|
|
@@ -211,7 +211,7 @@ module Hecks
|
|
|
211
211
|
@db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} ((#{expression}))")
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
-
# Postgres identifiers cap at 63 bytes. Hashing
|
|
214
|
+
# Postgres identifiers cap at 63 bytes. Hashing table + field
|
|
215
215
|
# together keeps every generated index name well under that no
|
|
216
216
|
# matter how long an aggregate or attribute name gets, and keeps
|
|
217
217
|
# two distinct fields — on the same table or different ones —
|
|
@@ -23,19 +23,24 @@ module Hecks
|
|
|
23
23
|
# actually needs to survive a live shape change. See
|
|
24
24
|
# docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
|
|
25
25
|
#
|
|
26
|
+
# ## What it is not
|
|
27
|
+
#
|
|
26
28
|
# No `hecks_eras`, no lineage, no advisory-lock-per-write for era
|
|
27
29
|
# tracking, no `lineage_capable?`/`era_check!` — this class simply
|
|
28
30
|
# doesn't define those methods at all, and the capability idiom
|
|
29
31
|
# elsewhere already treats their absence as "not lineage-capable".
|
|
30
32
|
#
|
|
31
|
-
# Storage shape
|
|
32
|
-
#
|
|
33
|
+
# ## Storage shape
|
|
34
|
+
#
|
|
35
|
+
# See postgres/schema_builder.rb for the DDL, postgres/codec.rb for the
|
|
36
|
+
# encode/decode.
|
|
37
|
+
#
|
|
33
38
|
# - One real column per attribute, typed for a scalar
|
|
34
39
|
# (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
|
|
35
|
-
# or list-typed attribute — never JSON-in-
|
|
40
|
+
# or list-typed attribute — never JSON-in-text the way `Sqlite` has
|
|
36
41
|
# to, since Postgres has a native jsonb type.
|
|
37
42
|
# - `append` and `project` are two real Postgres statements — `save`/
|
|
38
|
-
# `delete` wrap them in
|
|
43
|
+
# `delete` wrap them in one transaction, same "the journal insert
|
|
39
44
|
# and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
|
|
40
45
|
# comment gives: a crash between the two must never leave a
|
|
41
46
|
# half-written state. `append`/`project` stay plain, individually-
|
|
@@ -53,10 +58,24 @@ module Hecks
|
|
|
53
58
|
|
|
54
59
|
attr_reader :aggregate
|
|
55
60
|
|
|
61
|
+
# Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<Symbol>] `[:atomic_put, :optimistic_concurrency]`
|
|
56
64
|
def persistence_capabilities = [:atomic_put, :optimistic_concurrency]
|
|
57
65
|
|
|
66
|
+
# Opens a connection to the database a world declares, scoped to its `schema` if any.
|
|
67
|
+
#
|
|
68
|
+
# @param name [String] the aggregate's name, used only in error messages
|
|
69
|
+
# @param settings [Hash{Symbol, String => Object}] world settings for the binding;
|
|
70
|
+
# `database` (a database name or a `postgres://` URL) is required and `schema` is
|
|
71
|
+
# optional, each read under a Symbol or a String key
|
|
72
|
+
# @return [PG::Connection] a live connection with `search_path` and
|
|
73
|
+
# `client_min_messages` already set
|
|
74
|
+
# @raise [Runtime::WiringError] if the settings declare no `database`, or Postgres
|
|
75
|
+
# refuses the connection or the `SET` statements
|
|
76
|
+
# @raise [LoadError] if the `pg` gem is not installed
|
|
58
77
|
def self.connect_for(name, settings)
|
|
59
|
-
#
|
|
78
|
+
# **Lazy, on purpose** — same reasoning as PostgresEra's own
|
|
60
79
|
# connect_for: a domain that never wires Postgres should never
|
|
61
80
|
# need the gem installed.
|
|
62
81
|
require "pg"
|
|
@@ -75,7 +94,7 @@ module Hecks
|
|
|
75
94
|
PG.connect(dbname: declared)
|
|
76
95
|
end
|
|
77
96
|
|
|
78
|
-
#
|
|
97
|
+
# **Shared-instance isolation** — same as PostgresEra's own: a
|
|
79
98
|
# domain that declares `schema` is sharing its Postgres instance
|
|
80
99
|
# with other domains, so every unqualified reference this
|
|
81
100
|
# adapter constructs resolves through search_path. A domain with
|
|
@@ -83,8 +102,8 @@ module Hecks
|
|
|
83
102
|
schema = settings.key?(:schema) ? settings[:schema] : settings["schema"]
|
|
84
103
|
connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
|
|
85
104
|
|
|
86
|
-
#
|
|
87
|
-
# schema/table that already exists is the
|
|
105
|
+
# **Quiet on purpose** — same reasoning as PostgresEra's own: a
|
|
106
|
+
# schema/table that already exists is the ordinary case on every
|
|
88
107
|
# boot after the first, not news.
|
|
89
108
|
connection.exec("SET client_min_messages = warning")
|
|
90
109
|
connection
|
|
@@ -93,11 +112,22 @@ module Hecks
|
|
|
93
112
|
"cannot bind Postgres at #{declared} for #{name}: #{e.message.strip}"
|
|
94
113
|
end
|
|
95
114
|
|
|
115
|
+
# Connects and creates the aggregate, journal, event, saga and outbox tables if absent.
|
|
116
|
+
#
|
|
117
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose table this adapter owns
|
|
118
|
+
# @param settings [Hash{Symbol, String => Object}] world settings for the binding:
|
|
119
|
+
# `database` (required), `schema` and `domain` (optional; `domain` defaults to the
|
|
120
|
+
# aggregate's storage name and scopes saga rows)
|
|
121
|
+
# @param root [String, nil] project root directory; accepted for the shared adapter
|
|
122
|
+
# constructor shape and ignored
|
|
123
|
+
# @raise [Runtime::WiringError] if the settings declare no `database` or the connection
|
|
124
|
+
# is refused
|
|
125
|
+
# @raise [PG::Error] if creating a table or index fails
|
|
96
126
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
97
127
|
@aggregate = aggregate
|
|
98
128
|
@settings = settings
|
|
99
129
|
@db = self.class.connect_for(aggregate.name, settings)
|
|
100
|
-
#
|
|
130
|
+
# The optional saga-persistence capability's own scoping column
|
|
101
131
|
# (§2/§4) — falls back to the aggregate's own storage name for a
|
|
102
132
|
# directly-instantiated adapter (specs), same fallback shape
|
|
103
133
|
# Sqlite's own @domain already uses.
|
|
@@ -118,8 +148,18 @@ module Hecks
|
|
|
118
148
|
create_outbox_table!
|
|
119
149
|
end
|
|
120
150
|
|
|
151
|
+
# Names the aggregate's table; the journal table and outbox rows are keyed off it.
|
|
152
|
+
#
|
|
153
|
+
# @return [String] the aggregate's snake_case storage name, unquoted
|
|
121
154
|
def table = @aggregate.storage_name
|
|
122
155
|
|
|
156
|
+
# Reads the current row for one aggregate identity, stamped with its stored version.
|
|
157
|
+
#
|
|
158
|
+
# @param id [String, Object] the aggregate identity, bound as `id.to_s`
|
|
159
|
+
# @return [Runtime::Instance, nil] the decoded record with `version` set, or nil when
|
|
160
|
+
# no row has that id
|
|
161
|
+
# @raise [PG::Error] if the statement fails; a `PG::ConnectionBad` also triggers a
|
|
162
|
+
# reconnect for the next caller
|
|
123
163
|
def find(id)
|
|
124
164
|
result = pg_exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
|
|
125
165
|
return nil if result.ntuples.zero?
|
|
@@ -127,9 +167,20 @@ module Hecks
|
|
|
127
167
|
instance_from_row(result[0])
|
|
128
168
|
end
|
|
129
169
|
|
|
130
|
-
#
|
|
170
|
+
# Lists every stored record, ordered by id unless an ordering attribute is given.
|
|
171
|
+
#
|
|
172
|
+
# order_by is a runtime value — see Sqlite#all's own reasoning;
|
|
131
173
|
# whitelisted the identical way before it ever reaches
|
|
132
174
|
# order_expression.
|
|
175
|
+
#
|
|
176
|
+
# @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
|
|
177
|
+
# by, with id as the tie-break; nil orders by id alone
|
|
178
|
+
# @param direction [Symbol, String] `:asc` or `:desc`, case-insensitive; anything else
|
|
179
|
+
# sorts ascending
|
|
180
|
+
# @return [Array<Runtime::Instance>] the decoded records with `version` set, `[]` when
|
|
181
|
+
# the table is empty
|
|
182
|
+
# @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
|
|
183
|
+
# @raise [PG::Error] if the statement fails
|
|
133
184
|
def all(order_by: nil, direction: :asc)
|
|
134
185
|
order_sql = "ORDER BY id"
|
|
135
186
|
if order_by
|
|
@@ -146,38 +197,59 @@ module Hecks
|
|
|
146
197
|
pg_exec("SELECT * FROM #{quoted_table} #{order_sql}").map { |row| instance_from_row(row) }
|
|
147
198
|
end
|
|
148
199
|
|
|
200
|
+
# Counts the rows in the aggregate's table, deleted records excluded.
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] number of current records
|
|
203
|
+
# @raise [PG::Error] if the statement fails
|
|
149
204
|
def count = pg_exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
|
|
150
205
|
|
|
206
|
+
# Inserts one journal row, outside any transaction of its own.
|
|
207
|
+
#
|
|
208
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
|
|
209
|
+
# encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
|
|
210
|
+
# @return [Ports::Persistence::Entry] the same `entry`
|
|
211
|
+
# @raise [PG::Error] if the insert fails
|
|
151
212
|
def append(entry)
|
|
152
213
|
pg_exec_params(
|
|
153
214
|
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
|
|
154
|
-
# `mirrors` (unlike `state`) is a
|
|
215
|
+
# `mirrors` (unlike `state`) is a nullable column — an absent
|
|
155
216
|
# mirrors hash must bind a real SQL NULL, not the four-character
|
|
156
217
|
# JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
|
|
157
218
|
# check against it would never match. Same guard `sqlite.rb`/
|
|
158
219
|
# `d1.rb`/`postgres_era.rb` already use for their own journal's
|
|
159
220
|
# `mirrors` column.
|
|
160
|
-
[entry.id, entry.operation,
|
|
221
|
+
[entry.id, entry.operation, state_json(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
|
|
161
222
|
)
|
|
162
223
|
entry
|
|
163
224
|
end
|
|
164
225
|
|
|
226
|
+
# Upserts or deletes the aggregate's row for one journal entry, bumping its version.
|
|
227
|
+
#
|
|
165
228
|
# `expected_version:` requests optimistic-concurrency CAS (see
|
|
166
229
|
# `persistence_capabilities`/`Ports::Persistence::AppendOnly#save`).
|
|
167
|
-
# `hecks_version` is
|
|
230
|
+
# `hecks_version` is adapter bookkeeping — never in `persisted_fields`
|
|
168
231
|
# (Codec), so it never reaches `decode`'s domain-state hash. It goes
|
|
169
232
|
# in the INSERT column list at `1` (a genuinely new row) and bumps by
|
|
170
233
|
# one in the `ON CONFLICT DO UPDATE` branch; when `expected_version`
|
|
171
|
-
# is given, that
|
|
234
|
+
# is given, that update branch additionally requires
|
|
172
235
|
# `hecks_version = expected_version` to apply at all — Postgres's own
|
|
173
236
|
# `INSERT ... ON CONFLICT DO UPDATE ... WHERE`, which gates only
|
|
174
|
-
# whether the
|
|
237
|
+
# whether the conflict branch's update applies. A genuinely new row
|
|
175
238
|
# never reaches that branch at all, so it always inserts regardless
|
|
176
|
-
# of this
|
|
239
|
+
# of this where. `RETURNING hecks_version` plus `ntuples.zero?` is
|
|
177
240
|
# how a real version mismatch is told apart from an ordinary write:
|
|
178
|
-
# zero rows back means the conflict branch's
|
|
241
|
+
# zero rows back means the conflict branch's where excluded the row
|
|
179
242
|
# entirely — the version had already moved — so `nil` is returned
|
|
180
243
|
# for the caller (`AppendOnly#save`) to treat as "stale, no-op".
|
|
244
|
+
#
|
|
245
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to materialize
|
|
246
|
+
# @param expected_version [Integer, nil] the `hecks_version` the row must still hold for
|
|
247
|
+
# an update to apply; nil writes unconditionally. Ignored for a delete
|
|
248
|
+
# @return [Runtime::Instance, PG::Result, nil] for a save, a new instance over the
|
|
249
|
+
# entry's state with `version` set to the stored `hecks_version`, or nil when
|
|
250
|
+
# `expected_version` no longer matched and nothing was written; for a delete, the
|
|
251
|
+
# `DELETE` statement's `PG::Result`
|
|
252
|
+
# @raise [PG::Error] if the statement fails
|
|
181
253
|
# rubocop:disable Metrics/AbcSize -- the CAS/plain upsert split is one
|
|
182
254
|
# protocol; splitting it would hide the version handshake.
|
|
183
255
|
def project(entry, expected_version: nil)
|
|
@@ -206,31 +278,50 @@ module Hecks
|
|
|
206
278
|
end
|
|
207
279
|
# rubocop:enable Metrics/AbcSize
|
|
208
280
|
|
|
281
|
+
# Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
|
|
282
|
+
#
|
|
283
|
+
# @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
|
|
284
|
+
# through the state codec and `mirrors` parsed with String keys (nil when none were
|
|
285
|
+
# stored); `[]` when nothing has been appended
|
|
286
|
+
# @raise [PG::Error] if the statement fails
|
|
287
|
+
# @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
|
|
209
288
|
def entries
|
|
210
289
|
pg_exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
|
|
211
290
|
state = JSON.parse(row["state"])
|
|
212
291
|
Ports::Persistence::Entry.new(
|
|
213
292
|
operation: row["operation"] || "save",
|
|
214
293
|
id: row["aggregate_id"],
|
|
215
|
-
state: state
|
|
294
|
+
state: Ports::Persistence::StateCodec.decode(@aggregate, state),
|
|
216
295
|
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
217
296
|
)
|
|
218
297
|
end
|
|
219
298
|
end
|
|
220
299
|
|
|
300
|
+
# Deletes every row of the aggregate's table and its journal; events, saga rows and
|
|
301
|
+
# outbox rows are left in place.
|
|
302
|
+
#
|
|
303
|
+
# @return [Adapters::Postgres] self
|
|
304
|
+
# @raise [PG::Error] if a statement fails
|
|
221
305
|
def reset!
|
|
222
306
|
pg_exec("DELETE FROM #{quoted_table}")
|
|
223
307
|
pg_exec("DELETE FROM #{quoted_entry_table}")
|
|
224
308
|
self
|
|
225
309
|
end
|
|
226
310
|
|
|
227
|
-
#
|
|
311
|
+
# Journals and upserts an instance's current state atomically.
|
|
312
|
+
#
|
|
313
|
+
# One transaction, not the plain append-then-project two-step a
|
|
228
314
|
# file-based adapter needs a crash-recovery replay for (Heki) —
|
|
229
315
|
# real Postgres ACID atomicity is sitting right there, so a crash
|
|
230
316
|
# between the journal insert and the table upsert must not leave
|
|
231
317
|
# the two disagreeing. `append`/`project` themselves stay plain,
|
|
232
318
|
# transaction-free methods (see the class comment above) — the
|
|
233
319
|
# transaction lives here, the one caller that runs both together.
|
|
320
|
+
#
|
|
321
|
+
# @param instance [Runtime::Instance] the instance to store
|
|
322
|
+
# @return [Runtime::Instance] a new instance over the saved state, `version` set to the
|
|
323
|
+
# row's new `hecks_version`
|
|
324
|
+
# @raise [PG::Error] if either statement fails; the transaction is rolled back
|
|
234
325
|
def save(instance)
|
|
235
326
|
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
236
327
|
transaction do
|
|
@@ -239,6 +330,14 @@ module Hecks
|
|
|
239
330
|
end
|
|
240
331
|
end
|
|
241
332
|
|
|
333
|
+
# Stores an entry under a per-identity advisory lock and reports whether it inserted,
|
|
334
|
+
# replaced or conflicted, so two concurrent creators of one id cannot both insert.
|
|
335
|
+
#
|
|
336
|
+
# @param entry [Ports::Persistence::Entry] the save to store
|
|
337
|
+
# @param insert_only [Boolean] when true, an existing row is left untouched
|
|
338
|
+
# @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
|
|
339
|
+
# existing row and nothing was written
|
|
340
|
+
# @raise [PG::Error] if a statement fails; the transaction is rolled back
|
|
242
341
|
def atomic_put(entry, insert_only: false)
|
|
243
342
|
status = nil
|
|
244
343
|
transaction do
|
|
@@ -262,6 +361,11 @@ module Hecks
|
|
|
262
361
|
status
|
|
263
362
|
end
|
|
264
363
|
|
|
364
|
+
# Journals a delete and removes the row atomically, whether or not a row exists.
|
|
365
|
+
#
|
|
366
|
+
# @param id [String, Object] the aggregate identity, journalled as `id.to_s`
|
|
367
|
+
# @return [Boolean] always true
|
|
368
|
+
# @raise [PG::Error] if either statement fails; the transaction is rolled back
|
|
265
369
|
def delete(id)
|
|
266
370
|
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
267
371
|
transaction do
|
|
@@ -271,6 +375,11 @@ module Hecks
|
|
|
271
375
|
true
|
|
272
376
|
end
|
|
273
377
|
|
|
378
|
+
# Inserts an emitted event into the shared `events` table.
|
|
379
|
+
#
|
|
380
|
+
# @param event [Runtime::Event] the emitted event; `payload` is stored as JSON
|
|
381
|
+
# @return [PG::Result] the insert's result; callers ignore it
|
|
382
|
+
# @raise [PG::Error] if the insert fails
|
|
274
383
|
def record_event(event)
|
|
275
384
|
pg_exec_params(
|
|
276
385
|
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
|
|
@@ -278,6 +387,12 @@ module Hecks
|
|
|
278
387
|
)
|
|
279
388
|
end
|
|
280
389
|
|
|
390
|
+
# Reads back every recorded event in insertion order — the whole `events` table, not
|
|
391
|
+
# only this aggregate's rows.
|
|
392
|
+
#
|
|
393
|
+
# @return [Array<Runtime::Event>] the stored events, `payload` parsed with Symbol keys
|
|
394
|
+
# and `occurred_at` as the String Postgres returns; `[]` when none are recorded
|
|
395
|
+
# @raise [PG::Error] if the statement fails
|
|
281
396
|
def events
|
|
282
397
|
pg_exec("SELECT * FROM events ORDER BY id").map do |row|
|
|
283
398
|
Runtime::Event.new(
|
|
@@ -290,9 +405,22 @@ module Hecks
|
|
|
290
405
|
end
|
|
291
406
|
end
|
|
292
407
|
|
|
293
|
-
#
|
|
408
|
+
# Upserts one saga instance's checkpoint, keyed by domain, process manager and
|
|
409
|
+
# correlation.
|
|
410
|
+
#
|
|
411
|
+
# ── the optional saga-persistence capability (§2) — same DDL and
|
|
294
412
|
# shape as PostgresEra's own (postgres_era.rb), not lineage-
|
|
295
413
|
# specific, copied verbatim.
|
|
414
|
+
#
|
|
415
|
+
# @param process_manager [String, Symbol] the process manager's name
|
|
416
|
+
# @param correlation [String, Object] the instance's correlation value, stored as
|
|
417
|
+
# `correlation.to_s`
|
|
418
|
+
# @param state [String, Symbol] the saga's current state name
|
|
419
|
+
# @param memory [Hash] the saga's memory; must be JSON-serializable
|
|
420
|
+
# @param completed_compensations [Array] the ledger of completed compensable legs; must
|
|
421
|
+
# be JSON-serializable
|
|
422
|
+
# @return [PG::Result] the upsert's result; callers ignore it
|
|
423
|
+
# @raise [PG::Error] if the statement fails
|
|
296
424
|
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
297
425
|
pg_exec_params(
|
|
298
426
|
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
|
|
@@ -305,6 +433,13 @@ module Hecks
|
|
|
305
433
|
)
|
|
306
434
|
end
|
|
307
435
|
|
|
436
|
+
# Removes a finished saga instance's checkpoint; a missing row is not an error.
|
|
437
|
+
#
|
|
438
|
+
# @param process_manager [String, Symbol] the process manager's name
|
|
439
|
+
# @param correlation [String, Object] the instance's correlation value, matched as
|
|
440
|
+
# `correlation.to_s`
|
|
441
|
+
# @return [PG::Result] the delete's result; callers ignore it
|
|
442
|
+
# @raise [PG::Error] if the statement fails
|
|
308
443
|
def delete_saga(process_manager:, correlation:)
|
|
309
444
|
pg_exec_params(
|
|
310
445
|
"DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
|
|
@@ -312,6 +447,18 @@ module Hecks
|
|
|
312
447
|
)
|
|
313
448
|
end
|
|
314
449
|
|
|
450
|
+
# Yields every checkpointed saga instance of this adapter's domain, for
|
|
451
|
+
# `Registry#rehydrate_sagas!` to restore at boot.
|
|
452
|
+
#
|
|
453
|
+
# @yieldparam process_manager [String] the process manager's name
|
|
454
|
+
# @yieldparam correlation [String] the instance's correlation value
|
|
455
|
+
# @yieldparam state [String] the saga's state name
|
|
456
|
+
# @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every depth
|
|
457
|
+
# @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
|
|
458
|
+
# when the column is NULL
|
|
459
|
+
# @return [Enumerator, PG::Result] an enumerator over the same five values when no block
|
|
460
|
+
# is given; otherwise the query result
|
|
461
|
+
# @raise [PG::Error] if the statement fails
|
|
315
462
|
def each_saga
|
|
316
463
|
return enum_for(:each_saga) unless block_given?
|
|
317
464
|
|
|
@@ -344,7 +491,7 @@ module Hecks
|
|
|
344
491
|
"position(#{placeholder} in #{expression}) > 0"
|
|
345
492
|
end
|
|
346
493
|
|
|
347
|
-
#
|
|
494
|
+
# The list column itself is the JSONB array — no reaching into a
|
|
348
495
|
# shared blob a jsonb path has to walk into first (PostgresEra's
|
|
349
496
|
# own version does, since every attribute there shares one `state`
|
|
350
497
|
# column). Here, `column` names a real column of its own, already
|
|
@@ -357,17 +504,17 @@ module Hecks
|
|
|
357
504
|
|
|
358
505
|
def plain_column(name) = quote_ident(name)
|
|
359
506
|
|
|
360
|
-
# PostgresEra's own `jsonb_path` walks `[name, *path]` into
|
|
361
|
-
# shared `state` column — the attribute name is
|
|
362
|
-
# there. Here the attribute name
|
|
363
|
-
# is whatever is
|
|
507
|
+
# PostgresEra's own `jsonb_path` walks `[name, *path]` into one
|
|
508
|
+
# shared `state` column — the attribute name is part of the path
|
|
509
|
+
# there. Here the attribute name is the column: the path into it
|
|
510
|
+
# is whatever is left after the column, never the column name
|
|
364
511
|
# repeated inside its own path.
|
|
365
512
|
def nested_expression(name, path, member)
|
|
366
513
|
segments = path.empty? ? [(member || "value").to_s] : path
|
|
367
514
|
jsonb_path(name, segments)
|
|
368
515
|
end
|
|
369
516
|
|
|
370
|
-
# Scalar, non-value-object attributes get a
|
|
517
|
+
# Scalar, non-value-object attributes get a real typed column
|
|
371
518
|
# (bigint/double precision/text) — comparing and sorting one needs
|
|
372
519
|
# no cast at all, unlike PostgresEra's shared jsonb `state` blob,
|
|
373
520
|
# where even a top-level scalar only ever comes out of `#>>` as
|
|
@@ -428,7 +575,7 @@ module Hecks
|
|
|
428
575
|
end
|
|
429
576
|
|
|
430
577
|
# Same walk PostgresEra's own numeric_field? uses — decides
|
|
431
|
-
# numericness at
|
|
578
|
+
# numericness at any depth from the declared shape itself, not a
|
|
432
579
|
# runtime value.
|
|
433
580
|
def numeric_field?(field)
|
|
434
581
|
name, *path = field.to_s.split(".")
|
|
@@ -437,7 +584,7 @@ module Hecks
|
|
|
437
584
|
end
|
|
438
585
|
end
|
|
439
586
|
|
|
440
|
-
#
|
|
587
|
+
# Array[...] of individually-escaped literals — same escaping
|
|
441
588
|
# PostgresEra's own jsonb_path carries and the same reason: a
|
|
442
589
|
# hand-rolled '{a,b,c}' array literal has no escaping at all, and
|
|
443
590
|
# a segment is a field or value-object member name this method has
|
|
@@ -14,4 +14,9 @@ Hecks.adapter "PostgresEra" do
|
|
|
14
14
|
field :database
|
|
15
15
|
field :role
|
|
16
16
|
field :schema
|
|
17
|
+
# `allow_superuser true` — the explicit, on-the-record opt-in to boot
|
|
18
|
+
# over a connection whose role is a Postgres superuser or carries
|
|
19
|
+
# BYPASSRLS, which the era write-fence (row-level security) cannot
|
|
20
|
+
# bite. Refused by default; see `Lineage#check_fence_applies!` (BUG#24).
|
|
21
|
+
field :allow_superuser
|
|
17
22
|
end
|
|
@@ -13,7 +13,7 @@ module Hecks
|
|
|
13
13
|
# path; `forget`/`forget_all` exist for a caller that reloads an edited
|
|
14
14
|
# file in-process (see their own comment).
|
|
15
15
|
module Prism
|
|
16
|
-
#
|
|
16
|
+
# Not frozen — a real cache, keyed by file path and mutated by
|
|
17
17
|
# #tree_for below (`TREES[file] ||= ...`) and #forget/#forget_all.
|
|
18
18
|
# False positive for Style/MutableConstant.
|
|
19
19
|
# rubocop:disable-next Style/MutableConstant
|
|
@@ -58,12 +58,12 @@ module Hecks
|
|
|
58
58
|
TREES[file] ||= ::Prism.parse(File.read(file)).value
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# `TREES` caches for the life of the
|
|
61
|
+
# `TREES` caches for the life of the process, keyed by path, with
|
|
62
62
|
# no staleness check — correct for every ordinary caller (a file
|
|
63
63
|
# loads once per process: one `bin/ir` run, one rspec worker,
|
|
64
|
-
# never edited out from under it), but
|
|
64
|
+
# never edited out from under it), but wrong for anything that
|
|
65
65
|
# legitimately reloads an edited file in-process: a stale cached
|
|
66
|
-
# tree reports a `given`/`ensures` block at its
|
|
66
|
+
# tree reports a `given`/`ensures` block at its old line number,
|
|
67
67
|
# which no longer matches the freshly re-executed file's own
|
|
68
68
|
# `block.source_location` — surfacing as "did not survive
|
|
69
69
|
# extraction" on a perfectly valid file. Found for real building
|