hecks 1.3.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 +5 -4
- 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/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 +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -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 +7 -7
- 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 +101 -87
- 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 +170 -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 +180 -19
- 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 +140 -45
- 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 +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- 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 +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -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 +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- 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 +118 -66
- 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 +33 -9
- 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 +314 -90
- 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 +23 -3
- 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 +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- 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 +11 -11
- 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 +109 -27
- 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 +42 -63
- 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 +35 -35
- 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 +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- 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 +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- 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 -115
- 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 +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- 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 +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- 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 +205 -4
- 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 +22 -2
|
@@ -13,13 +13,33 @@ module Hecks
|
|
|
13
13
|
module MintTransaction
|
|
14
14
|
# ── the mint transaction ───────────────────────────────────────
|
|
15
15
|
#
|
|
16
|
+
# Makes a new era real, or reports that a concurrent minter already did.
|
|
17
|
+
#
|
|
16
18
|
# One transaction: the new era row (held text + minted name +
|
|
17
19
|
# cut watermark), the new partition, and every aggregate's
|
|
18
20
|
# recompiled matview + head view. The advisory lock is the
|
|
19
21
|
# writer fence — two concurrent minters serialize, and the
|
|
20
22
|
# second finds the era already held. Populating the matview
|
|
21
23
|
# inside the transaction means a convert meeting an unmapped
|
|
22
|
-
# value
|
|
24
|
+
# value refuses the whole mint, loudly, before anything boots.
|
|
25
|
+
#
|
|
26
|
+
# @param ordinal [Integer] ordinal of the era to mint, one past the newest held era
|
|
27
|
+
# @param hash [String] the new era's shape hash, SHA-256 hex
|
|
28
|
+
# @param label [String] the new era's short label, a prefix of `hash`
|
|
29
|
+
# @param held_text [String] the bluebook source text to freeze as this era
|
|
30
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the current bluebook's aggregates, each
|
|
31
|
+
# of which gets its head recompiled
|
|
32
|
+
# @param edges [Array<Hash{Symbol => Bluebook::Translation}>] the full edge chain in mint
|
|
33
|
+
# order, one `{ translation: }` Hash per step, as `LineageManager.edge_chain` builds it
|
|
34
|
+
# @param role [String, nil] app role to grant base and head privileges to; nil grants
|
|
35
|
+
# nothing
|
|
36
|
+
# @param projection [Hash{String => Object}, nil] the bluebook's storage-shape
|
|
37
|
+
# projection, as `Runtime::StorageShape.project` returns it; nil stores none
|
|
38
|
+
# @return [Boolean] true once the era is committed; false, with everything rolled back,
|
|
39
|
+
# when the domain already holds `ordinal`
|
|
40
|
+
# @raise [Runtime::WiringError] if another mint or merge holds the domain lock for over
|
|
41
|
+
# 10s, if Postgres refuses any statement (a convert meeting an unmapped value
|
|
42
|
+
# included), or if a held era's text fails its integrity check
|
|
23
43
|
def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
|
|
24
44
|
@db.exec("BEGIN")
|
|
25
45
|
# A concurrent minter blocks briefly, then refuses with a name
|
|
@@ -43,31 +63,31 @@ module Hecks
|
|
|
43
63
|
)
|
|
44
64
|
archive_text!(ordinal, held_text)
|
|
45
65
|
ensure_partition!(ordinal)
|
|
46
|
-
#
|
|
66
|
+
# After compile_head!, not before — this era's own snapshot
|
|
47
67
|
# table (a possibly-renamed name, per aggregate) is what
|
|
48
68
|
# grant_role! grants on, and compile_head! is what creates it.
|
|
49
69
|
# Granting first would GRANT on a relation that does not
|
|
50
70
|
# exist yet for any aggregate renamed in this very edge.
|
|
51
71
|
aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
|
|
52
72
|
grant_role!(role, aggregates: aggregates, era: ordinal) if role
|
|
53
|
-
#
|
|
54
|
-
# acquired, a lock is held until the
|
|
73
|
+
# **Last, right before `COMMIT`** — not merely unconditional. Once
|
|
74
|
+
# acquired, a lock is held until the transaction ends, not
|
|
55
75
|
# just for the statement that took it — so advance_era!'s
|
|
56
76
|
# DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
|
|
57
77
|
# as ALTER TABLE, and unavoidably so: Postgres has no lighter
|
|
58
78
|
# form for changing a policy, unlike the partition attach
|
|
59
79
|
# below) blocks every concurrent writer for as long as it sits
|
|
60
|
-
#
|
|
80
|
+
# before the expensive step. Ordered here, that block is the
|
|
61
81
|
# width of a few catalog statements plus the commit itself,
|
|
62
|
-
# not the width of compile_head!'s matview build. Measured
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
82
|
+
# not the width of compile_head!'s matview build. Measured, not
|
|
83
|
+
# assumed: placed above compile_head!, this reintroduces
|
|
84
|
+
# exactly the mint-stops-the-world cost ensure_partition!'s
|
|
85
|
+
# build-then-ATTACH exists to avoid — a cost only a genuine
|
|
86
|
+
# concurrent-write test shows.
|
|
67
87
|
#
|
|
68
|
-
#
|
|
88
|
+
# Unconditional regardless of position — this is the line that
|
|
69
89
|
# drops writing to the old schema. It does not wait for a role
|
|
70
|
-
# to be configured on
|
|
90
|
+
# to be configured on this boot, because the cutoff is a fact
|
|
71
91
|
# about the era, not about who happened to mint it: an old
|
|
72
92
|
# checkout's role, granted by some earlier boot this one knows
|
|
73
93
|
# nothing about, must lose write access the instant this
|
|
@@ -93,10 +113,12 @@ module Hecks
|
|
|
93
113
|
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{e.message.strip}"
|
|
94
114
|
end
|
|
95
115
|
|
|
96
|
-
#
|
|
116
|
+
# Grants an app role what it needs to append to the journal and read and write heads.
|
|
117
|
+
#
|
|
118
|
+
# Base privileges for a deployment's app role — a non-owner,
|
|
97
119
|
# which may append and read once the shared era fence below
|
|
98
120
|
# admits it, and owns nothing. Idempotent, and unconcerned with
|
|
99
|
-
#
|
|
121
|
+
# which era is current: that is advance_era!'s job, not this
|
|
100
122
|
# one's, so a role can be onboarded at any time without
|
|
101
123
|
# disturbing who may write what right now.
|
|
102
124
|
#
|
|
@@ -107,10 +129,10 @@ module Hecks
|
|
|
107
129
|
# `aggregates:`/`era:` cover the read-cache side of the same
|
|
108
130
|
# story: unlike the journal (immutable, owner-provisioned once),
|
|
109
131
|
# each aggregate's head_snapshot table is a table an app role
|
|
110
|
-
# must itself INSERT
|
|
132
|
+
# must itself `INSERT`/`UPDATE`/`DELETE` into — PostgresEra#append writes
|
|
111
133
|
# it directly, not through a view — so it needs real DML grants,
|
|
112
134
|
# not just the SELECT a derived read surface would need. `era:`
|
|
113
|
-
# is the ordinal
|
|
135
|
+
# is the ordinal this role is about to write under (the one
|
|
114
136
|
# hold_first!/mint_era! just made current, or the superseded one
|
|
115
137
|
# a stale checkout still speaks) — head_snapshot is era-scoped,
|
|
116
138
|
# so granting on the wrong era's table would grant on a table
|
|
@@ -119,6 +141,15 @@ module Hecks
|
|
|
119
141
|
# held-but-superseded checkout, since a role connecting to it
|
|
120
142
|
# for the first time (a new instance of an old checkout) has no
|
|
121
143
|
# privileges yet either.
|
|
144
|
+
#
|
|
145
|
+
# @param role [String] name of the Postgres role to grant to
|
|
146
|
+
# @param aggregates [Array<Bluebook::Aggregate>] aggregates whose head snapshot table,
|
|
147
|
+
# and head view where one exists, the role is granted on; ignored when `era` is nil
|
|
148
|
+
# @param era [Integer, nil] ordinal of the era whose snapshot tables to grant on; nil
|
|
149
|
+
# grants only the journal and sequence privileges
|
|
150
|
+
# @return [void]
|
|
151
|
+
# @raise [PG::Error] if Postgres refuses a grant, such as one naming a role or a
|
|
152
|
+
# snapshot table that does not exist
|
|
122
153
|
def grant_role!(role, aggregates: [], era: nil)
|
|
123
154
|
return unless provisioner?
|
|
124
155
|
|
|
@@ -137,7 +168,9 @@ module Hecks
|
|
|
137
168
|
end
|
|
138
169
|
end
|
|
139
170
|
|
|
140
|
-
#
|
|
171
|
+
# Replaces the journal's row policies: one era accepts INSERTs, every row stays readable.
|
|
172
|
+
#
|
|
173
|
+
# The current-era fence — one policy, shared by every granted
|
|
141
174
|
# role, not one per role. Advancing it is what drops writing to
|
|
142
175
|
# the old schema the instant the new one materializes: the
|
|
143
176
|
# moment this commits, no role — old or new, whether or not it
|
|
@@ -145,28 +178,33 @@ module Hecks
|
|
|
145
178
|
# era named here. There is no persisted fork: a checkout that
|
|
146
179
|
# keeps a role fenced to a stale era does not exist in this
|
|
147
180
|
# design, because there is no such thing as a role fenced to an
|
|
148
|
-
# era at all — only the
|
|
181
|
+
# era at all — only the one era everyone currently shares.
|
|
149
182
|
#
|
|
150
|
-
# A
|
|
151
|
-
# privilege on the partitioned
|
|
183
|
+
# A row policy, not a partition grant. Postgres checks INSERT
|
|
184
|
+
# privilege on the partitioned parent for a routed insert and
|
|
152
185
|
# never consults the partition's own grants, so a per-partition
|
|
153
186
|
# GRANT/REVOKE is inert in both directions: grant only on the
|
|
154
187
|
# partition and nobody can write at all; grant on the parent and
|
|
155
|
-
# they may write into
|
|
188
|
+
# they may write into every era, ancestors included. Measured,
|
|
156
189
|
# not reasoned about — see the spec, which writes through the
|
|
157
190
|
# fence rather than asserting the catalog.
|
|
158
191
|
#
|
|
159
192
|
# The table owner bypasses RLS by default, and that is load
|
|
160
193
|
# bearing: mint and merge run as the owner and must be able to
|
|
161
194
|
# write any era (the merge re-enters a winner's state into the
|
|
162
|
-
#
|
|
195
|
+
# current era, and compile_head! reads every ancestor).
|
|
163
196
|
#
|
|
164
|
-
#
|
|
165
|
-
# 1) or mint_era! (era N). Calling this with a
|
|
197
|
+
# Call only with the new current ordinal — from hold_first! (era
|
|
198
|
+
# 1) or mint_era! (era N). Calling this with a superseded
|
|
166
199
|
# ordinal — from a boot that merely recognizes an old checkout —
|
|
167
200
|
# would roll the fence backward and silently reopen the old
|
|
168
|
-
# schema for everyone. That path grants a role's
|
|
201
|
+
# schema for everyone. That path grants a role's privileges
|
|
169
202
|
# (grant_role!) and stops there on purpose.
|
|
203
|
+
#
|
|
204
|
+
# @param ordinal [Integer] ordinal of the era that becomes the only writable one
|
|
205
|
+
# @return [void]
|
|
206
|
+
# @raise [PG::Error] if Postgres refuses the policy change, as it does for a role that
|
|
207
|
+
# does not own the journal
|
|
170
208
|
def advance_era!(ordinal)
|
|
171
209
|
@db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
|
|
172
210
|
@db.exec(
|
|
@@ -9,7 +9,12 @@ module Hecks
|
|
|
9
9
|
# journal partition (`ensure_partition!`) — the build-then-ATTACH
|
|
10
10
|
# dance that keeps a mint from stopping every writer.
|
|
11
11
|
module Provisioning
|
|
12
|
-
#
|
|
12
|
+
# Builds or tops up this domain's lineage schema when the connected role
|
|
13
|
+
# owns the journal (or no journal exists yet); for any other role it only
|
|
14
|
+
# attempts the `formerly_known_as` rename bridge, when one is declared, and
|
|
15
|
+
# leaves the schema as the owner built it.
|
|
16
|
+
#
|
|
17
|
+
# Provisioning is the owner's job, and a deployment's app role is
|
|
13
18
|
# deliberately not the owner — it may append and read, and it
|
|
14
19
|
# owns nothing. By the time such a role connects, the base is
|
|
15
20
|
# already built, so its boot verifies rather than builds.
|
|
@@ -23,10 +28,15 @@ module Hecks
|
|
|
23
28
|
# every guarded ALTER (RLS, REVOKE) is guarded and placed
|
|
24
29
|
# exactly where it is for measured, documented reasons (see the
|
|
25
30
|
# inline comments on each: catalog-lock avoidance, RLS timing,
|
|
26
|
-
# FORCE semantics). Splitting this into smaller methods would
|
|
31
|
+
# `FORCE` semantics). Splitting this into smaller methods would
|
|
27
32
|
# only hide that single ordered sequence behind several call
|
|
28
33
|
# sites, with nothing gained — each statement already reads as
|
|
29
34
|
# one step, and the length here is DDL, not branching logic.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
# @raise [Runtime::WiringError] if the `formerly_known_as` rename cannot take
|
|
38
|
+
# its locks within 10s or Postgres refuses one of its statements
|
|
39
|
+
# @raise [PG::Error] if Postgres refuses a provisioning statement
|
|
30
40
|
# rubocop:disable-next Metrics/MethodLength
|
|
31
41
|
def ensure_base!
|
|
32
42
|
rename_domain! if @formerly_known_as
|
|
@@ -72,7 +82,7 @@ module Hecks
|
|
|
72
82
|
)
|
|
73
83
|
SQL
|
|
74
84
|
@db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}")
|
|
75
|
-
# GENERATED ALWAYS AS IDENTITY is the intent, but identity
|
|
85
|
+
# `GENERATED ALWAYS AS IDENTITY` is the intent, but identity
|
|
76
86
|
# columns on partitioned tables need Postgres 17 — an owned
|
|
77
87
|
# sequence default is the same spanning ordinal on any
|
|
78
88
|
# supported server.
|
|
@@ -91,29 +101,30 @@ module Hecks
|
|
|
91
101
|
# Immutability by privilege: nothing updates or deletes journal
|
|
92
102
|
# rows. The owner's implicit rights remain (Postgres has no way
|
|
93
103
|
# to revoke them from the owner itself); a deployment's app
|
|
94
|
-
# role connects as a
|
|
104
|
+
# role connects as a non-owner and gets exactly INSERT, per
|
|
95
105
|
# era, at mint time.
|
|
96
106
|
#
|
|
97
|
-
#
|
|
107
|
+
# Guarded, same reasoning as the RLS ALTER TABLE calls just
|
|
98
108
|
# below: REVOKE still writes pg_class.relacl (and takes the
|
|
99
109
|
# matching lock) even when the resulting privileges are
|
|
100
110
|
# unchanged, so an unconditional reissue on every ordinary
|
|
101
|
-
# reboot
|
|
111
|
+
# reboot races two concurrent boots into
|
|
102
112
|
# `PG::InternalError: tuple concurrently updated` — read
|
|
103
113
|
# first, touch the catalog only on the boot that actually
|
|
104
114
|
# needs to.
|
|
105
115
|
#
|
|
106
116
|
# `relacl IS NULL`, not `has_table_privilege('public', ...,
|
|
107
117
|
# 'UPDATE')` — found live, not assumed: a brand-new table's
|
|
108
|
-
# PUBLIC privilege is already "no UPDATE" by Postgres's own
|
|
118
|
+
# `PUBLIC` privilege is already "no UPDATE" by Postgres's own
|
|
109
119
|
# default (nothing has ever been explicitly granted to
|
|
110
|
-
# PUBLIC), so `has_table_privilege` answers false
|
|
111
|
-
# the REVOKE has ever run
|
|
112
|
-
# by that check alone
|
|
113
|
-
# REVOKE never actually
|
|
114
|
-
# "journal rows accept no UPDATE
|
|
115
|
-
# only by accident of Postgres's default, not
|
|
116
|
-
# privilege revocation this method exists to
|
|
120
|
+
# `PUBLIC`), so `has_table_privilege` answers false both before
|
|
121
|
+
# the REVOKE has ever run and after it has — indistinguishable
|
|
122
|
+
# by that check alone. Guarding on it means the very first
|
|
123
|
+
# boot's own REVOKE never actually runs, `relacl` stays NULL
|
|
124
|
+
# forever, and "journal rows accept no `UPDATE`/`DELETE` from
|
|
125
|
+
# `PUBLIC`" is true only by accident of Postgres's default, not
|
|
126
|
+
# by the explicit privilege revocation this method exists to
|
|
127
|
+
# record.
|
|
117
128
|
# `relacl IS NULL` means "default ACL, nothing explicit yet" —
|
|
118
129
|
# exactly the one-time signal needed, and (like the RLS flags
|
|
119
130
|
# below) `pg_table_is_visible(oid)`, not a bare relname match,
|
|
@@ -122,45 +133,45 @@ module Hecks
|
|
|
122
133
|
"SELECT relacl IS NULL FROM pg_class WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
|
|
123
134
|
).getvalue(0, 0)
|
|
124
135
|
@db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") if relacl_null == "t"
|
|
125
|
-
# RLS goes on
|
|
136
|
+
# RLS goes on at provisioning, never mid-life — enabling it
|
|
126
137
|
# later would deny every role that has no policy yet, on
|
|
127
138
|
# whatever the shape of the schema happened to be at that
|
|
128
139
|
# moment.
|
|
129
140
|
#
|
|
130
|
-
# FORCE
|
|
141
|
+
# `FORCE`, not merely `ENABLE`: without it, the table owner is
|
|
131
142
|
# exempt from every policy here, by Postgres default — which
|
|
132
143
|
# would leave the schema writable forever to whoever holds
|
|
133
144
|
# the owner's credentials, the one connection this whole
|
|
134
145
|
# design cannot fence. Checked, not assumed: mint_era! never
|
|
135
146
|
# inserts into the journal at all (only hecks_eras/
|
|
136
147
|
# hecks_era_texts, neither RLS-protected), and merge_tail!'s
|
|
137
|
-
# one journal INSERT targets the
|
|
138
|
-
# already admits for anyone with base privileges — so FORCE
|
|
148
|
+
# one journal INSERT targets the current era, which the fence
|
|
149
|
+
# already admits for anyone with base privileges — so `FORCE`
|
|
139
150
|
# costs the owner nothing operations here actually need.
|
|
140
151
|
#
|
|
141
|
-
# This still exempts an actual Postgres
|
|
142
|
-
# role granted BYPASSRLS) unconditionally — FORCE only
|
|
143
|
-
# narrows what ENABLE already narrows for the owner
|
|
152
|
+
# This still exempts an actual Postgres superuser (or any
|
|
153
|
+
# role granted BYPASSRLS) unconditionally — `FORCE` only
|
|
154
|
+
# narrows what `ENABLE` already narrows for the owner
|
|
144
155
|
# specifically, and superuser bypass sits above both. Running
|
|
145
156
|
# migrations as a real superuser (self-hosted Postgres, most
|
|
146
157
|
# commonly) leaves this gap open regardless; a managed
|
|
147
|
-
# provider's admin account is typically
|
|
148
|
-
# is exactly what FORCE closes.
|
|
158
|
+
# provider's admin account is typically not a superuser, and
|
|
159
|
+
# is exactly what `FORCE` closes.
|
|
149
160
|
#
|
|
150
|
-
#
|
|
161
|
+
# Guarded, not reissued unconditionally — measured, not
|
|
151
162
|
# assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`
|
|
152
|
-
# takes AccessExclusiveLock
|
|
153
|
-
#
|
|
154
|
-
# statement would be a no-op). ensure_base! runs on
|
|
163
|
+
# takes AccessExclusiveLock even when the setting is already
|
|
164
|
+
# correct (Postgres does not skip the lock just because the
|
|
165
|
+
# statement would be a no-op). ensure_base! runs on every
|
|
155
166
|
# boot by the owning role, not only the first — so an
|
|
156
167
|
# unconditional reissue here would mean every ordinary
|
|
157
168
|
# reboot of the deployment's own identity re-freezes every
|
|
158
|
-
# concurrent writer, on any era, for as long as that
|
|
159
|
-
# TABLE has to wait its turn. Read the current state first;
|
|
169
|
+
# concurrent writer, on any era, for as long as that
|
|
170
|
+
# ALTER TABLE has to wait its turn. Read the current state first;
|
|
160
171
|
# touch the catalog only on the boot that actually needs to.
|
|
161
|
-
# pg_table_is_visible,
|
|
172
|
+
# pg_table_is_visible, not a bare relname match — a shared
|
|
162
173
|
# instance (storehouse) can hold a same-named journal table
|
|
163
|
-
# per schema; catalog lookups here must resolve the
|
|
174
|
+
# per schema; catalog lookups here must resolve the same way
|
|
164
175
|
# search_path resolves an unqualified SQL reference, or a
|
|
165
176
|
# sibling domain's table satisfies a query meant for this
|
|
166
177
|
# domain's own.
|
|
@@ -173,39 +184,48 @@ module Hecks
|
|
|
173
184
|
install_transforms!
|
|
174
185
|
end
|
|
175
186
|
|
|
176
|
-
#
|
|
187
|
+
# Moves a renamed domain's journal, sequence, partitions and lineage rows
|
|
188
|
+
# from `formerly_known_as` to the current domain name, in one transaction.
|
|
189
|
+
#
|
|
190
|
+
# **A domain's own identity changed** — bridge its history under the
|
|
177
191
|
# new name, before `provisioner?`/the CREATE TABLE IF NOT EXISTS
|
|
178
|
-
# block
|
|
192
|
+
# block in `ensure_base!` ever run. That ordering is load-bearing, not
|
|
179
193
|
# tidiness: `provisioner?` and every statement in ensure_base!
|
|
180
|
-
# test for the journal under `journal` — the
|
|
194
|
+
# test for the journal under `journal` — the new name — and a
|
|
181
195
|
# freshly-renamed domain looks, to those checks, exactly like a
|
|
182
|
-
# domain that has never been provisioned at all.
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
# this method ever got a chance to run — and the
|
|
186
|
-
# RENAME
|
|
187
|
-
# already exists.
|
|
196
|
+
# domain that has never been provisioned at all. Called any later,
|
|
197
|
+
# ensure_base! would happily CREATE TABLE IF NOT EXISTS
|
|
198
|
+
# a brand-new, empty journal under the new name before
|
|
199
|
+
# this method ever got a chance to run — and the
|
|
200
|
+
# `ALTER TABLE ... RENAME` here would then fail, renaming onto a
|
|
201
|
+
# name that already exists.
|
|
188
202
|
#
|
|
189
203
|
# Three-way precheck, cheapest first:
|
|
190
204
|
# 1. no hecks_eras table at all yet — a genuinely fresh
|
|
191
205
|
# database; nothing to bridge, fall through to the
|
|
192
206
|
# ordinary provisioning path.
|
|
193
|
-
# 2. hecks_eras already has rows under the
|
|
207
|
+
# 2. hecks_eras already has rows under the new name — this
|
|
194
208
|
# rename already ran (a prior boot, possibly this one on a
|
|
195
209
|
# retry); idempotent no-op.
|
|
196
|
-
# 3. hecks_eras has rows under the
|
|
210
|
+
# 3. hecks_eras has rows under the old name — run the
|
|
197
211
|
# migration.
|
|
198
212
|
# Anything else (no rows under either name) falls through
|
|
199
213
|
# harmlessly — `formerly_known_as` pointing at a name with no
|
|
200
214
|
# held history is not an error, just inert.
|
|
201
|
-
#
|
|
202
|
-
#
|
|
215
|
+
#
|
|
216
|
+
# One transactional migration, with the three-way precheck above
|
|
217
|
+
# and a fixed lock-acquisition order
|
|
203
218
|
# (old before new, eras before ordinal) chosen specifically to
|
|
204
219
|
# avoid a deadlock against a concurrent rename/mint/write.
|
|
205
220
|
# Splitting this would separate the precheck from the locking
|
|
206
221
|
# from the renames from the commit/rollback handling — each a
|
|
207
|
-
# piece of
|
|
222
|
+
# piece of one transaction that must stay in this exact order
|
|
208
223
|
# or the deadlock-avoidance guarantee stops holding.
|
|
224
|
+
#
|
|
225
|
+
# @return [void]
|
|
226
|
+
# @raise [Runtime::WiringError] if the domain locks are not granted within 10s
|
|
227
|
+
# (`PG::LockNotAvailable`), or Postgres refuses any statement here, the
|
|
228
|
+
# prechecks included (`PG::Error`, message carried over); both roll back first
|
|
209
229
|
# rubocop:disable-next Metrics/AbcSize
|
|
210
230
|
# rubocop:disable-next Metrics/MethodLength
|
|
211
231
|
def rename_domain!
|
|
@@ -237,18 +257,18 @@ module Hecks
|
|
|
237
257
|
end
|
|
238
258
|
|
|
239
259
|
@db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}")
|
|
240
|
-
# An owned SERIAL
|
|
260
|
+
# An owned `SERIAL`/`IDENTITY` sequence moves automatically with
|
|
241
261
|
# its table and errors if renamed explicitly — this one is a
|
|
242
|
-
# plain CREATE SEQUENCE the journal's DEFAULT merely points
|
|
262
|
+
# plain `CREATE SEQUENCE` the journal's `DEFAULT` merely points
|
|
243
263
|
# at (see ensure_base!), so it does need its own rename, and
|
|
244
264
|
# the column default survives untouched: Postgres stores
|
|
245
265
|
# nextval('...') as a regclass reference internally, not
|
|
246
266
|
# literal text.
|
|
247
267
|
@db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}")
|
|
248
|
-
# ALTER TABLE ... RENAME on the parent does
|
|
268
|
+
# `ALTER TABLE ... RENAME` on the parent does not cascade to
|
|
249
269
|
# child partition names — each one needs its own explicit
|
|
250
|
-
# rename, sourced from the ordinals held under the
|
|
251
|
-
# captured above before the UPDATE below flips the column.
|
|
270
|
+
# rename, sourced from the ordinals held under the old name,
|
|
271
|
+
# captured above before the `UPDATE` below flips the column.
|
|
252
272
|
ordinals.each do |ordinal|
|
|
253
273
|
old_partition = "#{old_journal}_era_#{ordinal}"
|
|
254
274
|
@db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}")
|
|
@@ -260,7 +280,7 @@ module Hecks
|
|
|
260
280
|
# Unlike its siblings, hecks_attestations is not created in
|
|
261
281
|
# ensure_base! at all — only lazily, on first reattest! — so a
|
|
262
282
|
# domain that never needed one must not be forced through an
|
|
263
|
-
# UPDATE against a table that doesn't exist.
|
|
283
|
+
# `UPDATE` against a table that doesn't exist.
|
|
264
284
|
if @db.exec_params(
|
|
265
285
|
"SELECT to_regclass($1)", ["hecks_attestations"]
|
|
266
286
|
)[0]["to_regclass"]
|
|
@@ -287,32 +307,42 @@ module Hecks
|
|
|
287
307
|
raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
|
|
288
308
|
end
|
|
289
309
|
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
310
|
+
# Refuses to boot on a connection whose role Postgres exempts from row-level
|
|
311
|
+
# security — a superuser or a BYPASSRLS role — unless the caller opts in.
|
|
312
|
+
#
|
|
313
|
+
# **The fence has to be able to bite this connection, or nothing
|
|
314
|
+
# `ensure_base!` builds means anything**. The whole write-fence is
|
|
315
|
+
# row-level security (`FORCE ROW LEVEL SECURITY` above, plus
|
|
293
316
|
# advance_era!'s one INSERT policy), and Postgres exempts a
|
|
294
317
|
# superuser — or any role granted BYPASSRLS — from every policy
|
|
295
|
-
# on every table, unconditionally: FORCE only narrows the
|
|
296
|
-
#
|
|
318
|
+
# on every table, unconditionally: `FORCE` only narrows the
|
|
319
|
+
# owner's exemption and has no lever against either of those.
|
|
297
320
|
# Found live, not reasoned about (BUG#24): the QA ledger's own
|
|
298
321
|
# `.world` named a bare database, so every session connected as
|
|
299
322
|
# the machine's default Postgres user — a superuser — and an
|
|
300
323
|
# old checkout wrote its own superseded era straight through
|
|
301
324
|
# two mints, 37 rows no newer head could read, and nothing
|
|
302
|
-
# warned. Asked of the catalog
|
|
303
|
-
# is provisioned: the answer is a fact about the
|
|
325
|
+
# warned. Asked of the catalog once, at boot, before anything
|
|
326
|
+
# is provisioned: the answer is a fact about the role, not about
|
|
304
327
|
# any table, so there is nothing to wait for and nothing to
|
|
305
328
|
# half-build first.
|
|
306
329
|
#
|
|
307
330
|
# Refuses by default — quiet divergence is the enemy — naming
|
|
308
331
|
# the role and both ways out. `allow_superuser` boots anyway,
|
|
309
|
-
# but says so on stderr on
|
|
332
|
+
# but says so on stderr on every boot, because the only guard
|
|
310
333
|
# left standing then is PostgresEra#append's own in-process
|
|
311
334
|
# superseded-era check (the belt to this suspender), and an
|
|
312
335
|
# operator reading the log deserves to know which one they are
|
|
313
336
|
# relying on. Presence-over-truthiness for the setting itself
|
|
314
337
|
# is `PostgresEra.setting`'s job (the caller's); here a truthy
|
|
315
338
|
# value opts in and anything else does not.
|
|
339
|
+
#
|
|
340
|
+
# @param allow_superuser [Boolean, Object, nil] any truthy value boots anyway
|
|
341
|
+
# with a warning on stderr; `false` or `nil` refuses
|
|
342
|
+
# @return [nil] when the role is fenced, or after the warning is written
|
|
343
|
+
# @raise [Runtime::WiringError] if the connection's role is a superuser or is
|
|
344
|
+
# granted BYPASSRLS, and `allow_superuser` is not truthy
|
|
345
|
+
# @raise [PG::Error] if the `pg_roles` lookup fails
|
|
316
346
|
def check_fence_applies!(allow_superuser: false)
|
|
317
347
|
row = @db.exec("SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user")[0]
|
|
318
348
|
exempt = []
|
|
@@ -337,9 +367,16 @@ module Hecks
|
|
|
337
367
|
"superseded-era check (PostgresEra#append) stands between an old checkout and a superseded era"
|
|
338
368
|
end
|
|
339
369
|
|
|
370
|
+
# Tells whether this connection's role is the one that builds the schema:
|
|
371
|
+
# it owns the domain's journal, or no journal is visible yet.
|
|
372
|
+
#
|
|
340
373
|
# Nothing provisioned yet — build it. Provisioned and owned —
|
|
341
|
-
# keep it current. Provisioned by
|
|
374
|
+
# keep it current. Provisioned by someone else — this is an app
|
|
342
375
|
# role, and the owner has already done this work.
|
|
376
|
+
#
|
|
377
|
+
# @return [Boolean] true when no journal table is visible on the search path
|
|
378
|
+
# or `current_user` owns it; false when another role owns it
|
|
379
|
+
# @raise [PG::Error] if the catalog lookup fails
|
|
343
380
|
def provisioner?
|
|
344
381
|
rows = @db.exec_params(
|
|
345
382
|
"SELECT pg_get_userbyid(relowner) = current_user AS owned FROM pg_class " \
|
|
@@ -349,7 +386,10 @@ module Hecks
|
|
|
349
386
|
rows.ntuples.zero? || rows[0]["owned"] == "t"
|
|
350
387
|
end
|
|
351
388
|
|
|
352
|
-
#
|
|
389
|
+
# Creates and attaches one era's journal partition, unless it is already
|
|
390
|
+
# attached; finishes the attach for a table a crashed boot left detached.
|
|
391
|
+
#
|
|
392
|
+
# **Build, then ATTACH** — never CREATE ... PARTITION OF. The two
|
|
353
393
|
# produce the same partition; only the lock differs, and that
|
|
354
394
|
# difference is the whole availability story of a mint:
|
|
355
395
|
#
|
|
@@ -357,17 +397,21 @@ module Hecks
|
|
|
357
397
|
# CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
|
|
358
398
|
#
|
|
359
399
|
# AccessExclusive conflicts with every insert in the hierarchy —
|
|
360
|
-
# routed through the parent
|
|
361
|
-
# so attaching the new era inside the mint transaction
|
|
362
|
-
# every writer for the
|
|
400
|
+
# routed through the parent or addressed to an existing leaf —
|
|
401
|
+
# so attaching the new era that way inside the mint transaction
|
|
402
|
+
# stops every writer for the whole mint, tail materialization
|
|
363
403
|
# included. ShareUpdateExclusive conflicts with neither, so the
|
|
364
404
|
# old checkout keeps writing its own era straight through the
|
|
365
405
|
# build and only pauses for the head swap at the end.
|
|
366
406
|
#
|
|
367
|
-
# That is what makes the fork real
|
|
407
|
+
# That is what makes the fork real during a mint rather than
|
|
368
408
|
# merely before and after one. Measured, and pinned by the spec
|
|
369
409
|
# — which writes through a live mint rather than reading a lock
|
|
370
410
|
# mode out of the catalog.
|
|
411
|
+
#
|
|
412
|
+
# @param era [Integer] ordinal of the era whose partition is ensured
|
|
413
|
+
# @return [void]
|
|
414
|
+
# @raise [PG::Error] if Postgres refuses the create or the attach
|
|
371
415
|
def ensure_partition!(era)
|
|
372
416
|
return if partition_attached?(era)
|
|
373
417
|
|
|
@@ -382,9 +426,17 @@ module Hecks
|
|
|
382
426
|
SQL
|
|
383
427
|
end
|
|
384
428
|
|
|
385
|
-
#
|
|
429
|
+
# Tells whether an era's partition is part of the journal, by asking
|
|
430
|
+
# `pg_inherits` rather than checking that the table exists.
|
|
431
|
+
#
|
|
432
|
+
# Attached, not merely present: a crash between the `CREATE` and
|
|
386
433
|
# the ATTACH leaves a table that is not yet part of the journal,
|
|
387
434
|
# and the next boot must finish the job rather than skip it.
|
|
435
|
+
#
|
|
436
|
+
# @param era [Integer] ordinal of the era whose partition is looked up
|
|
437
|
+
# @return [Boolean] true when the partition is attached to this domain's
|
|
438
|
+
# journal; false when it is missing or exists unattached
|
|
439
|
+
# @raise [PG::Error] if the catalog lookup fails
|
|
388
440
|
def partition_attached?(era)
|
|
389
441
|
@db.exec_params(
|
|
390
442
|
"SELECT 1 FROM pg_inherits i " \
|